mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
150 Commits
timepkr
...
nativewind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3658f0349e | ||
|
|
02133165d8 | ||
|
|
837b841352 | ||
|
|
10e344dad5 | ||
|
|
848bfb358f | ||
|
|
2ca1c64532 | ||
|
|
d9a79eafbd | ||
|
|
a1dc16f4b6 | ||
|
|
674613c0d6 | ||
|
|
e4e095798a | ||
|
|
2e07185584 | ||
|
|
144001981a | ||
|
|
cd096f66ea | ||
|
|
02cffe9f8b | ||
|
|
9c52e8966f | ||
|
|
9257a949fe | ||
|
|
7528ed8e0c | ||
|
|
98b6f3eac3 | ||
|
|
a2561b3ae8 | ||
|
|
368f5c6bbc | ||
|
|
0e2e30b0a9 | ||
|
|
2664a24875 | ||
|
|
40e486669f | ||
|
|
ae64cbc718 | ||
|
|
e14f691cc4 | ||
|
|
89c607a530 | ||
|
|
90f4e7418f | ||
|
|
79fed2da09 | ||
|
|
1dd41c103f | ||
|
|
988e85affc | ||
|
|
2810516987 | ||
|
|
679c1971a2 | ||
|
|
3abdbb7ef9 | ||
|
|
0d647f5dc1 | ||
|
|
bd1df491a2 | ||
|
|
feb872d05c | ||
|
|
64b6b99a21 | ||
|
|
8046c244b1 | ||
|
|
6928316ba0 | ||
|
|
36c74642f2 | ||
|
|
5a992e99c8 | ||
|
|
ff9230f878 | ||
|
|
9afa861ff2 | ||
|
|
6d7486e76d | ||
|
|
8529012b19 | ||
|
|
f704bd47a3 | ||
|
|
239f2fc34e | ||
|
|
5d5183b08e | ||
|
|
7cad0fab0f | ||
|
|
98e0d55ee3 | ||
|
|
2d8f0d7a58 | ||
|
|
643303cd7a | ||
|
|
dc692203be | ||
|
|
648c51d007 | ||
|
|
0e5b106333 | ||
|
|
40e9ea2ab6 | ||
|
|
2157e228b9 | ||
|
|
5c5534adb8 | ||
|
|
379297cd7e | ||
|
|
b3730e9708 | ||
|
|
2fbc44d5c3 | ||
|
|
12f05937af | ||
|
|
5ea440492b | ||
|
|
57b851a498 | ||
|
|
00f806b077 | ||
|
|
975c20a788 | ||
|
|
948e2d93c1 | ||
|
|
6de3c93745 | ||
|
|
1f63110561 | ||
|
|
8a94c35450 | ||
|
|
fdc97284a5 | ||
|
|
792ecfdbdc | ||
|
|
121b01dc08 | ||
|
|
1aa49071eb | ||
|
|
66bef1284a | ||
|
|
1526f708de | ||
|
|
53198e4486 | ||
|
|
a66bf2df2a | ||
|
|
34422721c3 | ||
|
|
b5008c2363 | ||
|
|
1cbab2be08 | ||
|
|
1bdcfd71f7 | ||
|
|
792271b146 | ||
|
|
3de5fbd35c | ||
|
|
6f8dc1ed59 | ||
|
|
92309d8fb2 | ||
|
|
d3070975cb | ||
|
|
69c0da5b17 | ||
|
|
1736690f01 | ||
|
|
78a92905e9 | ||
|
|
801c1b4ccb | ||
|
|
703b1dca51 | ||
|
|
e5ef97abd9 | ||
|
|
3053856990 | ||
|
|
b8e82e2801 | ||
|
|
2187fe63f0 | ||
|
|
b1f842f9e1 | ||
|
|
43e03fb61c | ||
|
|
2327ab84c2 | ||
|
|
93034b6018 | ||
|
|
e45022b5cb | ||
|
|
c022b70e6d | ||
|
|
5615ba2df7 | ||
|
|
268960bc5b | ||
|
|
508a713ecf | ||
|
|
36e50b591a | ||
|
|
05c1f95ba4 | ||
|
|
9d355691ae | ||
|
|
73c58186b6 | ||
|
|
eb8d3e4dfd | ||
|
|
987f30e5c7 | ||
|
|
b7a0dbf81b | ||
|
|
7368a0eb7c | ||
|
|
ef0b6f3e14 | ||
|
|
af8691f61d | ||
|
|
f7aee2e253 | ||
|
|
0a1b74d911 | ||
|
|
ab48da447d | ||
|
|
1cc0630939 | ||
|
|
5391ff4688 | ||
|
|
f6d96676fe | ||
|
|
cf02842ab1 | ||
|
|
b63fcf6b99 | ||
|
|
36069c1b4e | ||
|
|
d2846decce | ||
|
|
60a8a3f052 | ||
|
|
e2d15dc2e7 | ||
|
|
7cdefdeccd | ||
|
|
684b8822af | ||
|
|
231bc47942 | ||
|
|
965a497be3 | ||
|
|
f50a7fb99b | ||
|
|
50a5e75d1a | ||
|
|
84e838a055 | ||
|
|
6d6c78e974 | ||
|
|
778d5b7c6b | ||
|
|
8051146f41 | ||
|
|
86a359a230 | ||
|
|
c16dac65cc | ||
|
|
437c9ecdbc | ||
|
|
bf4eec2bdf | ||
|
|
08367f3c7f | ||
|
|
f5d077956a | ||
|
|
ca74005262 | ||
|
|
52c936935e | ||
|
|
2951600ed9 | ||
|
|
d12c8c778c | ||
|
|
77d4527a00 | ||
|
|
1ef3353155 | ||
|
|
2c635c0d1e |
664
.github/workflows/release.yml
vendored
664
.github/workflows/release.yml
vendored
@@ -184,17 +184,20 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -257,18 +260,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -324,18 +330,21 @@ jobs:
|
||||
|
||||
# Build and deploy e2e.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -391,18 +400,21 @@ jobs:
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -458,18 +470,21 @@ jobs:
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -528,18 +543,21 @@ jobs:
|
||||
|
||||
# Build and deploy test-server.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -595,18 +613,21 @@ jobs:
|
||||
|
||||
# Build and deploy otel-collector.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -664,18 +685,21 @@ jobs:
|
||||
|
||||
# Build and deploy status-page.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -731,18 +755,21 @@ jobs:
|
||||
|
||||
# Build and deploy test.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -798,18 +825,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -866,18 +896,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -935,18 +968,21 @@ jobs:
|
||||
|
||||
# Build and deploy open-telemetry-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1003,18 +1039,21 @@ jobs:
|
||||
|
||||
# Build and deploy incoming-request-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1070,18 +1109,21 @@ jobs:
|
||||
|
||||
# Build and deploy fluent-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1137,18 +1179,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1204,18 +1249,21 @@ jobs:
|
||||
|
||||
# Build and deploy admin-dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1272,18 +1320,21 @@ jobs:
|
||||
|
||||
# Build and deploy dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1339,18 +1390,21 @@ jobs:
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1407,18 +1461,21 @@ jobs:
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1474,18 +1531,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1585,18 +1645,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1654,18 +1717,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1726,18 +1792,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1798,18 +1867,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1930,18 +2002,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1960,8 +2035,37 @@ jobs:
|
||||
.
|
||||
|
||||
push-release-tags:
|
||||
name: Push release tags after GitHub release
|
||||
needs: [github-release, read-version, generate-build-number]
|
||||
name: Push release tags before GitHub release
|
||||
needs:
|
||||
- read-version
|
||||
- generate-build-number
|
||||
- publish-mcp-server
|
||||
- nginx-docker-image-deploy
|
||||
- e2e-docker-image-deploy
|
||||
- isolated-vm-docker-image-deploy
|
||||
- home-docker-image-deploy
|
||||
- test-server-docker-image-deploy
|
||||
- otel-collector-docker-image-deploy
|
||||
- status-page-docker-image-deploy
|
||||
- test-docker-image-deploy
|
||||
- probe-ingest-docker-image-deploy
|
||||
- server-monitor-ingest-docker-image-deploy
|
||||
- open-telemetry-ingest-docker-image-deploy
|
||||
- incoming-request-ingest-docker-image-deploy
|
||||
- fluent-ingest-docker-image-deploy
|
||||
- probe-docker-image-deploy
|
||||
- admin-dashboard-docker-image-deploy
|
||||
- dashboard-docker-image-deploy
|
||||
- app-docker-image-deploy
|
||||
- copilot-docker-image-deploy
|
||||
- accounts-docker-image-deploy
|
||||
- llm-docker-image-deploy
|
||||
- docs-docker-image-deploy
|
||||
- worker-docker-image-deploy
|
||||
- workflow-docker-image-deploy
|
||||
- api-reference-docker-image-deploy
|
||||
- test-e2e-release-saas
|
||||
- test-e2e-release-self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1998,17 +2102,20 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Create Docker Hub release tag from version
|
||||
run: |
|
||||
@@ -2029,7 +2136,7 @@ jobs:
|
||||
|
||||
test-e2e-release-saas:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [open-telemetry-ingest-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, fluent-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
@@ -2039,7 +2146,7 @@ jobs:
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
@@ -2051,11 +2158,22 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- name: Preinstall and enable billing
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun && bash ./Tests/Scripts/enable-billing-env-var.sh
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run prerun
|
||||
bash ./Tests/Scripts/enable-billing-env-var.sh
|
||||
- name: Pin APP_TAG to versioned release
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}"
|
||||
if [ -f config.env ]; then
|
||||
if grep -q '^APP_TAG=' config.env; then
|
||||
sed -i "s/^APP_TAG=.*/APP_TAG=${VERSION}/" config.env
|
||||
else
|
||||
echo "APP_TAG=${VERSION}" >> config.env
|
||||
fi
|
||||
else
|
||||
echo "APP_TAG=${VERSION}" > config.env
|
||||
fi
|
||||
- name: Start Server with version tag
|
||||
run: |
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
@@ -2064,12 +2182,22 @@ jobs:
|
||||
npm run status-check
|
||||
- name: Wait for server to start
|
||||
run: bash ./Tests/Scripts/status-check.sh http://localhost
|
||||
- name: Pull E2E test image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
export APP_TAG=${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}
|
||||
docker compose -f docker-compose.e2e.yml pull e2e
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: 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)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
export APP_TAG=${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}
|
||||
trap 'docker compose -f docker-compose.e2e.yml down -v || true' EXIT
|
||||
if ! docker compose -f docker-compose.e2e.yml up --exit-code-from e2e --abort-on-container-exit e2e; then
|
||||
docker compose -f docker-compose.e2e.yml logs e2e
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
# Run this on failure
|
||||
@@ -2105,7 +2233,7 @@ jobs:
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
@@ -2117,11 +2245,21 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- name: Preinstall
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run prerun
|
||||
- name: Pin APP_TAG to versioned release
|
||||
run: |
|
||||
VERSION="${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}"
|
||||
if [ -f config.env ]; then
|
||||
if grep -q '^APP_TAG=' config.env; then
|
||||
sed -i "s/^APP_TAG=.*/APP_TAG=${VERSION}/" config.env
|
||||
else
|
||||
echo "APP_TAG=${VERSION}" >> config.env
|
||||
fi
|
||||
else
|
||||
echo "APP_TAG=${VERSION}" > config.env
|
||||
fi
|
||||
- name: Start Server with version tag
|
||||
run: |
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
@@ -2130,12 +2268,22 @@ jobs:
|
||||
npm run status-check
|
||||
- name: Wait for server to start
|
||||
run: bash ./Tests/Scripts/status-check.sh http://localhost
|
||||
- name: Pull E2E test image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
export APP_TAG=${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}
|
||||
docker compose -f docker-compose.e2e.yml pull e2e
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: 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)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export $(grep -v '^#' config.env | xargs)
|
||||
export APP_TAG=${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}
|
||||
trap 'docker compose -f docker-compose.e2e.yml down -v || true' EXIT
|
||||
if ! docker compose -f docker-compose.e2e.yml up --exit-code-from e2e --abort-on-container-exit e2e; then
|
||||
docker compose -f docker-compose.e2e.yml logs e2e
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
# Run this on failure
|
||||
@@ -2157,8 +2305,9 @@ jobs:
|
||||
# Optional. Defaults to repository settings.
|
||||
retention-days: 7
|
||||
|
||||
github-release:
|
||||
needs: [test-e2e-release-saas, test-e2e-release-self-hosted, generate-build-number, read-version]
|
||||
draft-github-release:
|
||||
name: Create draft GitHub release
|
||||
needs: [test-e2e-release-saas, test-e2e-release-self-hosted, generate-build-number, read-version, push-release-tags]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/release'
|
||||
permissions:
|
||||
@@ -2216,12 +2365,15 @@ jobs:
|
||||
with:
|
||||
tag: "${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}"
|
||||
artifactErrorsFailBuild: true
|
||||
draft: true
|
||||
allowUpdates: true
|
||||
prerelease: false
|
||||
body: |
|
||||
${{steps.fallback_changelog.outputs.changelog}}
|
||||
|
||||
|
||||
infrastructure-agent-deploy:
|
||||
needs: [github-release, generate-build-number, read-version]
|
||||
needs: [draft-github-release, generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -2259,7 +2411,49 @@ jobs:
|
||||
files: |
|
||||
InfrastructureAgent/dist/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: false
|
||||
draft: true
|
||||
prerelease: false
|
||||
tag_name: ${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}
|
||||
|
||||
|
||||
finalize-github-release:
|
||||
name: Publish GitHub release
|
||||
needs: [infrastructure-agent-deploy, generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/release'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const tag = '${{needs.read-version.outputs.major_minor}}.${{needs.generate-build-number.outputs.build_number}}';
|
||||
try {
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const release = releases.find((item) => item.tag_name === tag);
|
||||
|
||||
if (!release) {
|
||||
throw new Error(`Release with tag ${tag} not found in repository ${context.repo.owner}/${context.repo.repo}`);
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.id,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
make_latest: 'true',
|
||||
});
|
||||
|
||||
console.log(`Published release for ${tag}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to publish release for tag ${tag}: ${error.message ?? error}`);
|
||||
}
|
||||
|
||||
|
||||
475
.github/workflows/test-release.yaml
vendored
475
.github/workflows/test-release.yaml
vendored
@@ -176,17 +176,20 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push Docker images (test)
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -276,18 +279,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -347,18 +353,21 @@ jobs:
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -418,18 +427,21 @@ jobs:
|
||||
|
||||
# Build and deploy e2e.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -487,18 +499,21 @@ jobs:
|
||||
|
||||
# Build and deploy test-server.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -557,18 +572,21 @@ jobs:
|
||||
|
||||
# Build and deploy otel-collector.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -627,18 +645,21 @@ jobs:
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -697,18 +718,21 @@ jobs:
|
||||
|
||||
# Build and deploy isolated-vm.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -769,18 +793,21 @@ jobs:
|
||||
|
||||
# Build and deploy status-page.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -841,18 +868,21 @@ jobs:
|
||||
|
||||
# Build and deploy test.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -911,18 +941,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -983,18 +1016,21 @@ jobs:
|
||||
|
||||
# Build and deploy ServerMonitorIngest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1056,18 +1092,21 @@ jobs:
|
||||
|
||||
# Build and deploy incoming-request-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1127,18 +1166,21 @@ jobs:
|
||||
|
||||
# Build and deploy incoming-request-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1197,18 +1239,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe-ingest.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1267,18 +1312,21 @@ jobs:
|
||||
|
||||
# Build and deploy probe.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1337,18 +1385,21 @@ jobs:
|
||||
|
||||
# Build and deploy dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1407,18 +1458,21 @@ jobs:
|
||||
|
||||
# Build and deploy admin-dashboard.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1477,18 +1531,21 @@ jobs:
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1550,18 +1607,21 @@ jobs:
|
||||
|
||||
# Build and deploy app.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1623,18 +1683,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1694,18 +1757,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1764,18 +1830,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1835,18 +1904,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1907,18 +1979,21 @@ jobs:
|
||||
|
||||
# Build and deploy accounts.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -1997,6 +2072,7 @@ jobs:
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 90
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: 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)
|
||||
@@ -2054,6 +2130,7 @@ jobs:
|
||||
- name: Run E2E Tests. Run docker container e2e in docker compose file
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 90
|
||||
max_attempts: 3
|
||||
on_retry_command: docker compose -f docker-compose.dev.yml down -v || true
|
||||
command: 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)
|
||||
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -5,6 +5,7 @@ import Projects from "./Pages/Projects/Index";
|
||||
import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
|
||||
import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
|
||||
import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
|
||||
import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
|
||||
// Settings Pages.
|
||||
import SettingsEmail from "./Pages/Settings/Email/Index";
|
||||
import SettingsProbes from "./Pages/Settings/Probes/Index";
|
||||
@@ -105,6 +106,11 @@ const App: () => JSX.Element = () => {
|
||||
element={<SettingsCallSMS />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SETTINGS_WHATSAPP]?.toString() || ""}
|
||||
element={<SettingsWhatsApp />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ""}
|
||||
element={<SettingsProbes />}
|
||||
|
||||
@@ -50,6 +50,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
}}
|
||||
icon={IconProp.Call}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "WhatsApp",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
|
||||
),
|
||||
}}
|
||||
icon={IconProp.WhatsApp}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Monitoring">
|
||||
|
||||
454
AdminDashboard/src/Pages/Settings/WhatsApp/Index.tsx
Normal file
454
AdminDashboard/src/Pages/Settings/WhatsApp/Index.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import DashboardSideMenu from "../SideMenu";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import Page from "Common/UI/Components/Page/Page";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import WhatsAppTemplateMessages, {
|
||||
WhatsAppTemplateId,
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
|
||||
type ToFriendlyName = (value: string) => string;
|
||||
|
||||
const toFriendlyName: ToFriendlyName = (value: string): string => {
|
||||
return value
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
};
|
||||
|
||||
type ExtractTemplateVariables = (template: string) => Array<string>;
|
||||
|
||||
const extractTemplateVariables: ExtractTemplateVariables = (
|
||||
template: string,
|
||||
): Array<string> => {
|
||||
const matches: RegExpMatchArray | null = template.match(/\{\{(.*?)\}\}/g);
|
||||
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueVariables: Set<string> = new Set<string>();
|
||||
|
||||
for (const match of matches) {
|
||||
const variable: string = match.replace("{{", "").replace("}}", "").trim();
|
||||
|
||||
if (variable) {
|
||||
uniqueVariables.add(variable);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueVariables).sort((a: string, b: string) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
};
|
||||
|
||||
type BuildWhatsAppSetupMarkdown = () => string;
|
||||
|
||||
const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
|
||||
const templateKeys: Array<keyof typeof WhatsAppTemplateIds> = Object.keys(
|
||||
WhatsAppTemplateIds,
|
||||
) as Array<keyof typeof WhatsAppTemplateIds>;
|
||||
|
||||
const description: string =
|
||||
"Follow these steps to connect Meta WhatsApp with OneUptime so notifications can be delivered via WhatsApp.";
|
||||
|
||||
const prerequisitesList: Array<string> = [
|
||||
"Meta Business Manager admin access for the WhatsApp Business Account.",
|
||||
"A WhatsApp Business phone number approved for API messaging.",
|
||||
"Admin access to OneUptime with permission to edit global notification settings.",
|
||||
];
|
||||
|
||||
const setupStepsList: Array<string> = [
|
||||
"Sign in to the [Meta Business Manager](https://business.facebook.com/) with admin access to your WhatsApp Business Account.",
|
||||
"From **Business Settings → Accounts → WhatsApp Accounts**, create or select the account that owns your sender phone number.",
|
||||
"In Buisness Portfolio, create a system user and assign it to the WhatsApp Business Account with the role of **Admin**.",
|
||||
"Generate a token for this system user and this will be your long-lived access token. Make sure to select the **whatsapp_business_management** and **whatsapp_business_messaging** permissions when generating the token.",
|
||||
"Paste the access token and phone number ID into the **Meta WhatsApp Settings** card above, then save.",
|
||||
"For the **Business Account ID**, go to **Business Settings → Business Info** (or **Business Settings → WhatsApp Accounts → Settings**) and copy the **WhatsApp Business Account ID** value.",
|
||||
"To locate the **App ID** and **App Secret**, open [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app, then navigate to **Settings → Basic**. The App ID is shown at the top; click **Show** next to **App Secret** to reveal and copy it.",
|
||||
"Create each template listed below in the Meta WhatsApp Manager. Make sure the template name, language, and variables match exactly. You can however change the content to your preference. Please make sure it's approved by Meta.",
|
||||
"Send a test notification from OneUptime to confirm that WhatsApp delivery succeeds.",
|
||||
];
|
||||
|
||||
const prerequisitesMarkdown: string = prerequisitesList
|
||||
.map((item: string) => {
|
||||
return `- ${item}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const setupStepsMarkdown: string = setupStepsList
|
||||
.map((item: string, index: number) => {
|
||||
return `${index + 1}. ${item}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const tableRows: string = templateKeys
|
||||
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
|
||||
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
|
||||
const friendlyName: string = toFriendlyName(enumKey.toString());
|
||||
const templateMessage: string = WhatsAppTemplateMessages[templateId];
|
||||
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
|
||||
const variables: Array<string> =
|
||||
extractTemplateVariables(templateMessage);
|
||||
const variableList: string =
|
||||
variables.length > 0
|
||||
? variables
|
||||
.map((variable: string) => {
|
||||
return `\`${variable}\``;
|
||||
})
|
||||
.join(", ")
|
||||
: "_None_";
|
||||
|
||||
return `| ${friendlyName} | \`${templateId}\` | ${language} | ${variableList} |`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const templateBodies: string = templateKeys
|
||||
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
|
||||
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
|
||||
const friendlyName: string = toFriendlyName(enumKey.toString());
|
||||
const templateMessage: string = WhatsAppTemplateMessages[templateId];
|
||||
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
|
||||
const variables: Array<string> =
|
||||
extractTemplateVariables(templateMessage);
|
||||
const variableMarkdown: string =
|
||||
variables.length > 0
|
||||
? variables
|
||||
.map((variable: string) => {
|
||||
return `- \`${variable}\``;
|
||||
})
|
||||
.join("\n")
|
||||
: "_None_";
|
||||
const variablesHeading: string = variables.length
|
||||
? `**Variables (${variables.length})**`
|
||||
: "**Variables**";
|
||||
|
||||
return [
|
||||
`#### ${friendlyName}`,
|
||||
"",
|
||||
`**Template Name:** \`${templateId}\``,
|
||||
`**Language:** ${language}`,
|
||||
"",
|
||||
variablesHeading,
|
||||
variableMarkdown,
|
||||
"",
|
||||
"**Body**",
|
||||
"```plaintext",
|
||||
templateMessage,
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
].join("\n");
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const templateSummaryTable: string = [
|
||||
"| Friendly Name | Template Name | Language | Variables |",
|
||||
"| --- | --- | --- | --- |",
|
||||
tableRows,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
description,
|
||||
"### Prerequisites",
|
||||
prerequisitesMarkdown,
|
||||
"### Setup Steps",
|
||||
setupStepsMarkdown,
|
||||
"### Required WhatsApp Templates",
|
||||
templateSummaryTable,
|
||||
"### Template Bodies",
|
||||
"> Copy the exact template body below—including punctuation and spacing—when creating each template inside Meta. The variables list shows every placeholder that must be configured in WhatsApp Manager.",
|
||||
templateBodies,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
};
|
||||
|
||||
const whatsappSetupMarkdown: string = buildWhatsAppSetupMarkdown();
|
||||
|
||||
const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
|
||||
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
|
||||
const [testError, setTestError] = useState<string>("");
|
||||
const [testSuccess, setTestSuccess] = useState<string>("");
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={"Admin Settings"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS] as Route,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "WhatsApp",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
<CardModelDetail
|
||||
name="Meta WhatsApp Settings"
|
||||
cardProps={{
|
||||
title: "Meta WhatsApp Settings",
|
||||
description:
|
||||
"Configure Meta WhatsApp credentials. These values are used to send WhatsApp notifications from OneUptime.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Meta WhatsApp Config"
|
||||
formSteps={[
|
||||
{
|
||||
title: "Credentials",
|
||||
id: "meta-credentials",
|
||||
},
|
||||
{
|
||||
title: "Meta App",
|
||||
id: "meta-app",
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
},
|
||||
title: "Access Token",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.EncryptedText,
|
||||
required: true,
|
||||
description:
|
||||
"Long-lived access token generated in the Meta WhatsApp Business Platform.",
|
||||
placeholder: "EAAG...",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
},
|
||||
title: "Phone Number ID",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
description:
|
||||
"The WhatsApp Business phone number ID associated with your sending number.",
|
||||
placeholder: "123456789012345",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
},
|
||||
title: "Business Account ID",
|
||||
stepId: "meta-credentials",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Business Account ID that owns the WhatsApp templates.",
|
||||
placeholder: "123456789012345",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppId: true,
|
||||
},
|
||||
title: "App ID",
|
||||
stepId: "meta-app",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Facebook App ID tied to your WhatsApp integration.",
|
||||
placeholder: "987654321098765",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
title: "App Secret",
|
||||
stepId: "meta-app",
|
||||
fieldType: FormFieldSchemaType.EncryptedText,
|
||||
required: false,
|
||||
description:
|
||||
"Optional Facebook App Secret used for webhook signature verification.",
|
||||
placeholder: "Facebook App Secret",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: GlobalConfig,
|
||||
id: "model-detail-global-config-meta-whatsapp",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
},
|
||||
title: "Access Token",
|
||||
fieldType: FieldType.HiddenText,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
},
|
||||
title: "Phone Number ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
},
|
||||
title: "Business Account ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppId: true,
|
||||
},
|
||||
title: "App ID",
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
title: "App Secret",
|
||||
fieldType: FieldType.HiddenText,
|
||||
placeholder: "Not Configured",
|
||||
},
|
||||
],
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="Send Test WhatsApp Message"
|
||||
description="Send a test WhatsApp template message to confirm your Meta configuration."
|
||||
>
|
||||
{testSuccess ? (
|
||||
<Alert
|
||||
type={AlertType.SUCCESS}
|
||||
title={testSuccess}
|
||||
className="mb-4"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<BasicForm
|
||||
id="send-test-whatsapp-form"
|
||||
name="Send Test WhatsApp Message"
|
||||
isLoading={isSendingTest}
|
||||
error={testError || ""}
|
||||
submitButtonText="Send Test Message"
|
||||
maxPrimaryButtonWidth={true}
|
||||
initialValues={{
|
||||
phoneNumber: "",
|
||||
}}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
phoneNumber: true,
|
||||
},
|
||||
title: "Recipient WhatsApp Number",
|
||||
description:
|
||||
"Enter the full international phone number (including country code) that should receive the test message.",
|
||||
placeholder: "+11234567890",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.Phone,
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
onSubmit={async (
|
||||
values: JSONObject,
|
||||
onSubmitSuccessful?: () => void,
|
||||
) => {
|
||||
const toPhone: string = String(values["phoneNumber"] || "").trim();
|
||||
|
||||
if (!toPhone) {
|
||||
setTestSuccess("");
|
||||
setTestError(
|
||||
"Please enter a WhatsApp number before sending a test message.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSendingTest(true);
|
||||
setTestError("");
|
||||
setTestSuccess("");
|
||||
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/notification/whatsapp/test",
|
||||
),
|
||||
data: {
|
||||
toPhone,
|
||||
templateKey: WhatsAppTemplateIds.TestNotification,
|
||||
templateLanguageCode:
|
||||
WhatsAppTemplateLanguage[
|
||||
WhatsAppTemplateIds.TestNotification
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (response.isFailure()) {
|
||||
throw new Error("Failed to send test WhatsApp message.");
|
||||
}
|
||||
|
||||
setTestSuccess(
|
||||
"Test WhatsApp message sent successfully. Check the recipient device to confirm delivery.",
|
||||
);
|
||||
|
||||
if (onSubmitSuccessful) {
|
||||
onSubmitSuccessful();
|
||||
}
|
||||
} catch (err) {
|
||||
setTestError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsSendingTest(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Meta WhatsApp Setup Guide"
|
||||
description="Steps to connect Meta WhatsApp and the templates you must provision."
|
||||
>
|
||||
<MarkdownViewer text={whatsappSetupMarkdown} />
|
||||
</Card>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsWhatsApp;
|
||||
@@ -15,6 +15,7 @@ enum PageMap {
|
||||
SETTINGS_HOST = "SETTINGS_HOST",
|
||||
SETTINGS_SMTP = "SETTINGS_SMTP",
|
||||
SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
|
||||
SETTINGS_WHATSAPP = "SETTINGS_WHATSAPP",
|
||||
SETTINGS_PROBES = "SETTINGS_PROBES",
|
||||
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
|
||||
SETTINGS_API_KEY = "SETTINGS_API_KEY",
|
||||
|
||||
@@ -25,6 +25,7 @@ const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`),
|
||||
[PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`),
|
||||
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
|
||||
[PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`),
|
||||
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
|
||||
[PageMap.SETTINGS_AUTHENTICATION]: new Route(
|
||||
`/admin/settings/authentication`,
|
||||
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -31,6 +31,7 @@ import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
|
||||
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
|
||||
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
|
||||
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
|
||||
import UserPushAPI from "Common/Server/API/UserPushAPI";
|
||||
import ApiKeyPermissionService, {
|
||||
Service as ApiKeyPermissionServiceType,
|
||||
@@ -284,6 +285,9 @@ import ShortLinkService, {
|
||||
import SmsLogService, {
|
||||
Service as SmsLogServiceType,
|
||||
} from "Common/Server/Services/SmsLogService";
|
||||
import WhatsAppLogService, {
|
||||
Service as WhatsAppLogServiceType,
|
||||
} from "Common/Server/Services/WhatsAppLogService";
|
||||
import PushNotificationLogService, {
|
||||
Service as PushNotificationLogServiceType,
|
||||
} from "Common/Server/Services/PushNotificationLogService";
|
||||
@@ -458,6 +462,7 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
|
||||
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
|
||||
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
|
||||
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
|
||||
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
|
||||
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
|
||||
// Custom Fields API
|
||||
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
|
||||
@@ -1538,6 +1543,14 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<WhatsAppLog, WhatsAppLogServiceType>(
|
||||
WhatsAppLog,
|
||||
WhatsAppLogService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
|
||||
@@ -1675,6 +1688,10 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new UserWhatsAppAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -166,13 +167,7 @@ const formatTeamForSCIM: (
|
||||
}
|
||||
|
||||
return {
|
||||
// Include both SCIM 2.0 Group schema and SCIM 1.1 core schema for broader compatibility
|
||||
// Some provisioning agents (e.g., Okta On-Prem) expect 'urn:scim:schemas:core:1.0'
|
||||
// to be present even when using SCIM v2 endpoints.
|
||||
schemas: [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
"urn:scim:schemas:core:1.0",
|
||||
],
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
id: team.id?.toString(),
|
||||
displayName: team.name?.toString(),
|
||||
members: members,
|
||||
@@ -189,7 +184,11 @@ const formatTeamForSCIM: (
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/ServiceProviderConfig",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Project SCIM ServiceProviderConfig - scimId: ${req.params["projectScimId"]!}`,
|
||||
@@ -208,7 +207,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -217,7 +216,11 @@ router.get(
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Project SCIM Users list - scimId: ${req.params["projectScimId"]!}`,
|
||||
@@ -390,7 +393,7 @@ router.get(
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -399,7 +402,11 @@ router.get(
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -458,7 +465,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -467,7 +474,11 @@ router.get(
|
||||
router.put(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -624,7 +635,7 @@ router.put(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -633,7 +644,11 @@ router.put(
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Groups",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -713,7 +728,7 @@ router.get(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -722,7 +737,11 @@ router.get(
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Groups/:groupId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Get individual group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -777,7 +796,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, group);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -786,7 +805,11 @@ router.get(
|
||||
router.post(
|
||||
"/scim/v2/:projectScimId/Groups",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Create group request for projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -909,7 +932,7 @@ router.post(
|
||||
return Response.sendJsonObjectResponse(req, res, createdGroup);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -918,7 +941,11 @@ router.post(
|
||||
router.put(
|
||||
"/scim/v2/:projectScimId/Groups/:groupId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Update group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -1072,7 +1099,7 @@ router.put(
|
||||
throw new NotFoundException("Failed to retrieve updated group");
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1081,7 +1108,11 @@ router.put(
|
||||
router.delete(
|
||||
"/scim/v2/:projectScimId/Groups/:groupId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Delete group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -1159,7 +1190,7 @@ router.delete(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1168,7 +1199,11 @@ router.delete(
|
||||
router.patch(
|
||||
"/scim/v2/:projectScimId/Groups/:groupId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Patch group request for groupId: ${req.params["groupId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -1403,7 +1438,7 @@ router.patch(
|
||||
throw new NotFoundException("Failed to retrieve updated group");
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1412,7 +1447,11 @@ router.patch(
|
||||
router.post(
|
||||
"/scim/v2/:projectScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Create user request for projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -1499,7 +1538,7 @@ router.post(
|
||||
return Response.sendJsonObjectResponse(req, res, createdUser);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1508,7 +1547,11 @@ router.post(
|
||||
router.delete(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
@@ -1560,7 +1603,7 @@ router.delete(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -47,7 +47,11 @@ const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.get(
|
||||
"/service-provider-login",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.query["email"]) {
|
||||
return Response.sendErrorResponse(
|
||||
@@ -152,7 +156,11 @@ router.get(
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
Response.sendErrorResponse(req, res, err as Exception);
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -162,7 +170,7 @@ router.get(
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
_next: NextFunction,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.params["projectId"]) {
|
||||
@@ -238,22 +246,42 @@ router.get(
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
Response.sendErrorResponse(req, res, err as Exception);
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/idp-login/:projectId/:projectSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
return await loginUserWithSso(req, res);
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await loginUserWithSso(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/idp-login/:projectId/:projectSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
return await loginUserWithSso(req, res);
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await loginUserWithSso(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -29,7 +30,11 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM ServiceProviderConfig - scimId: ${req.params["statusPageScimId"]!}`,
|
||||
@@ -44,7 +49,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -53,7 +58,11 @@ router.get(
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -164,7 +173,7 @@ router.get(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -173,7 +182,11 @@ router.get(
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -231,7 +244,7 @@ router.get(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -240,7 +253,11 @@ router.get(
|
||||
router.post(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -333,7 +350,7 @@ router.post(
|
||||
return Response.sendJsonObjectResponse(req, res, createdUser);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -342,7 +359,11 @@ router.post(
|
||||
router.put(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -489,7 +510,7 @@ router.put(
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -498,7 +519,11 @@ router.put(
|
||||
router.delete(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
@@ -562,7 +587,7 @@ router.delete(
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -115,7 +115,11 @@ router.get(
|
||||
|
||||
router.post(
|
||||
"/status-page-idp-login/:statusPageId/:statusPageSsoId",
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const samlResponseBase64: string = req.body.SAMLResponse;
|
||||
|
||||
@@ -312,7 +316,11 @@ router.post(
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
Response.sendErrorResponse(req, res, new ServerException());
|
||||
if (err instanceof Exception) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(new ServerException());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -22,139 +23,146 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/make-call",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
await CallService.makeCall(body["callRequest"] as CallRequest, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
await CallService.makeCall(body["callRequest"] as CallRequest, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
}
|
||||
|
||||
const testCallRequest: CallRequest = {
|
||||
data: [
|
||||
{
|
||||
sayMessage: "This is a test call from OneUptime.",
|
||||
},
|
||||
],
|
||||
to: toPhone,
|
||||
};
|
||||
|
||||
await CallService.makeCall(testCallRequest, {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Error making test call. Please check the twilio logs for more details",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const testCallRequest: CallRequest = {
|
||||
data: [
|
||||
{
|
||||
sayMessage: "This is a test call from OneUptime.",
|
||||
},
|
||||
],
|
||||
to: toPhone,
|
||||
};
|
||||
|
||||
await CallService.makeCall(testCallRequest, {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Error making test call. Please check the twilio logs for more details",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
|
||||
@@ -19,70 +20,74 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const mail: EmailMessage = {
|
||||
templateType: body["templateType"] as EmailTemplateType,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: body["subject"] as string,
|
||||
vars: body["vars"] as Dictionary<string>,
|
||||
body: (body["body"] as string) || "",
|
||||
};
|
||||
const mail: EmailMessage = {
|
||||
templateType: body["templateType"] as EmailTemplateType,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: body["subject"] as string,
|
||||
vars: body["vars"] as Dictionary<string>,
|
||||
body: (body["body"] as string) || "",
|
||||
};
|
||||
|
||||
let mailServer: EmailServer | undefined = undefined;
|
||||
let mailServer: EmailServer | undefined = undefined;
|
||||
|
||||
if (hasMailServerSettingsInBody(body)) {
|
||||
mailServer = MailService.getEmailServer(req.body);
|
||||
if (hasMailServerSettingsInBody(body)) {
|
||||
mailServer = MailService.getEmailServer(req.body);
|
||||
}
|
||||
|
||||
await MailService.send(mail, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
emailServer: mailServer,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"].toString())
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"].toString())
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"].toString())
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"].toString())
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"].toString())
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"].toString())
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"].toString())
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await MailService.send(mail, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
emailServer: mailServer,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"].toString())
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"].toString())
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"].toString())
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"].toString())
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"].toString())
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"].toString())
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"].toString())
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"].toString())
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
@@ -15,50 +16,54 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
// Support both new devices format and legacy deviceTokens/deviceNames format
|
||||
let devices: Array<{ token: string; name?: string }> = [];
|
||||
// Support both new devices format and legacy deviceTokens/deviceNames format
|
||||
let devices: Array<{ token: string; name?: string }> = [];
|
||||
|
||||
if (body["devices"]) {
|
||||
// New format: devices as array of objects
|
||||
devices = body["devices"] as Array<{ token: string; name?: string }>;
|
||||
} else {
|
||||
throw new Error("Invalid request format: 'devices' array is required.");
|
||||
if (body["devices"]) {
|
||||
// New format: devices as array of objects
|
||||
devices = body["devices"] as Array<{ token: string; name?: string }>;
|
||||
} else {
|
||||
throw new Error("Invalid request format: 'devices' array is required.");
|
||||
}
|
||||
|
||||
await PushService.send(
|
||||
{
|
||||
devices,
|
||||
deviceType: (body["deviceType"] as any) || "web",
|
||||
message: body["message"] as any,
|
||||
},
|
||||
{
|
||||
projectId: (body["projectId"] as ObjectID) || undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await PushService.send(
|
||||
{
|
||||
devices,
|
||||
deviceType: (body["deviceType"] as any) || "web",
|
||||
message: body["message"] as any,
|
||||
},
|
||||
{
|
||||
projectId: (body["projectId"] as ObjectID) || undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -21,130 +22,141 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = JSONFunctions.deserialize(req.body);
|
||||
|
||||
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
|
||||
projectId: body["projectId"] as ObjectID,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId:
|
||||
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
|
||||
customTwilioConfig: body["customTwilioConfig"] as any,
|
||||
incidentId: (body["incidentId"] as ObjectID) || undefined,
|
||||
alertId: (body["alertId"] as ObjectID) || undefined,
|
||||
scheduledMaintenanceId:
|
||||
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
|
||||
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
|
||||
statusPageAnnouncementId:
|
||||
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
|
||||
userId: (body["userId"] as ObjectID) || undefined,
|
||||
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
|
||||
onCallPolicyEscalationRuleId:
|
||||
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
|
||||
undefined,
|
||||
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
|
||||
teamId: (body["teamId"] as ObjectID) || undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
const callSMSConfigId: ObjectID = new ObjectID(
|
||||
body["callSMSConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectCallSMSConfig | null =
|
||||
await ProjectCallSMSConfigService.findOneById({
|
||||
id: callSMSConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
twilioAccountSID: true,
|
||||
twilioAuthToken: true,
|
||||
twilioPrimaryPhoneNumber: true,
|
||||
twilioSecondaryPhoneNumbers: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"call and sms config not found for id" + callSMSConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
if (!toPhone) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toPhone is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
// if any of the twilio config is missing, we will not send make the call
|
||||
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAccountSID) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAccountSID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioAuthToken is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
if (!config.twilioPrimaryPhoneNumber) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("twilioPrimaryPhoneNumber is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
const twilioConfig: TwilioConfig | undefined =
|
||||
ProjectCallSMSConfigService.toTwilioConfig(config);
|
||||
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
try {
|
||||
if (!twilioConfig) {
|
||||
throw new BadDataException("twilioConfig is undefined");
|
||||
}
|
||||
|
||||
await SmsService.sendSms(
|
||||
toPhone,
|
||||
"This is a test SMS from OneUptime.",
|
||||
{
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Failed to send test SMS. Please check the twilio logs for more details.",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await SmsService.sendSms(toPhone, "This is a test SMS from OneUptime.", {
|
||||
projectId: config.projectId,
|
||||
customTwilioConfig: twilioConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Failed to send test SMS. Please check the twilio logs for more details.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,6 +11,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -18,87 +19,92 @@ import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const body: JSONObject = req.body;
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body;
|
||||
|
||||
const smtpConfigId: ObjectID = new ObjectID(body["smtpConfigId"] as string);
|
||||
const smtpConfigId: ObjectID = new ObjectID(
|
||||
body["smtpConfigId"] as string,
|
||||
);
|
||||
|
||||
const config: ProjectSmtpConfig | null =
|
||||
await ProjectSMTPConfigService.findOneById({
|
||||
id: smtpConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
const config: ProjectSmtpConfig | null =
|
||||
await ProjectSMTPConfigService.findOneById({
|
||||
id: smtpConfigId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
hostname: true,
|
||||
port: true,
|
||||
username: true,
|
||||
password: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
secure: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"smtp-config not found for id" + smtpConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!config) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"smtp-config not found for id" + smtpConfigId.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const toEmail: Email = new Email(body["toEmail"] as string);
|
||||
const toEmail: Email = new Email(body["toEmail"] as string);
|
||||
|
||||
if (!toEmail) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toEmail is required"),
|
||||
);
|
||||
}
|
||||
if (!toEmail) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("toEmail is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const mail: EmailMessage = {
|
||||
templateType: EmailTemplateType.SMTPTest,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: "Test Email from OneUptime",
|
||||
vars: {},
|
||||
body: "",
|
||||
};
|
||||
const mail: EmailMessage = {
|
||||
templateType: EmailTemplateType.SMTPTest,
|
||||
toEmail: new Email(body["toEmail"] as string),
|
||||
subject: "Test Email from OneUptime",
|
||||
vars: {},
|
||||
body: "",
|
||||
};
|
||||
|
||||
const mailServer: EmailServer = {
|
||||
id: config.id!,
|
||||
host: config.hostname!,
|
||||
port: config.port!,
|
||||
username: config.username!,
|
||||
password: config.password!,
|
||||
fromEmail: config.fromEmail!,
|
||||
fromName: config.fromName!,
|
||||
secure: Boolean(config.secure),
|
||||
};
|
||||
const mailServer: EmailServer = {
|
||||
id: config.id!,
|
||||
host: config.hostname!,
|
||||
port: config.port!,
|
||||
username: config.username!,
|
||||
password: config.password!,
|
||||
fromEmail: config.fromEmail!,
|
||||
fromName: config.fromName!,
|
||||
secure: Boolean(config.secure),
|
||||
};
|
||||
|
||||
try {
|
||||
await MailService.send(mail, {
|
||||
emailServer: mailServer,
|
||||
projectId: config.projectId!,
|
||||
timeout: 4000,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
|
||||
),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await MailService.send(mail, {
|
||||
emailServer: mailServer,
|
||||
projectId: config.projectId!,
|
||||
timeout: 4000,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new BadDataException(
|
||||
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
});
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
171
App/FeatureSet/Notification/API/WhatsApp.ts
Normal file
171
App/FeatureSet/Notification/API/WhatsApp.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import WhatsAppService from "../Services/WhatsAppService";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Phone from "Common/Types/Phone";
|
||||
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
|
||||
import {
|
||||
WhatsAppTemplateId,
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const toTemplateVariables: (
|
||||
rawVariables: JSONObject | undefined,
|
||||
) => Record<string, string> | undefined = (
|
||||
rawVariables: JSONObject | undefined,
|
||||
): Record<string, string> | undefined => {
|
||||
if (!rawVariables) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const key of Object.keys(rawVariables)) {
|
||||
const value: unknown = rawVariables[key];
|
||||
if (value !== null && value !== undefined) {
|
||||
result[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/send",
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
if (!body["to"]) {
|
||||
throw new BadDataException("`to` phone number is required");
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["to"] as string);
|
||||
|
||||
const message: WhatsAppMessage = {
|
||||
to: toPhone,
|
||||
body: (body["body"] as string) || "",
|
||||
templateKey: (body["templateKey"] as string) || undefined,
|
||||
templateVariables: toTemplateVariables(
|
||||
body["templateVariables"] as JSONObject | undefined,
|
||||
),
|
||||
templateLanguageCode:
|
||||
(body["templateLanguageCode"] as string) || undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
await WhatsAppService.sendWhatsApp(message, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
isSensitive: (body["isSensitive"] as boolean) || false,
|
||||
userOnCallLogTimelineId: body["userOnCallLogTimelineId"]
|
||||
? new ObjectID(body["userOnCallLogTimelineId"] as string)
|
||||
: undefined,
|
||||
incidentId: body["incidentId"]
|
||||
? new ObjectID(body["incidentId"] as string)
|
||||
: undefined,
|
||||
alertId: body["alertId"]
|
||||
? new ObjectID(body["alertId"] as string)
|
||||
: undefined,
|
||||
scheduledMaintenanceId: body["scheduledMaintenanceId"]
|
||||
? new ObjectID(body["scheduledMaintenanceId"] as string)
|
||||
: undefined,
|
||||
statusPageId: body["statusPageId"]
|
||||
? new ObjectID(body["statusPageId"] as string)
|
||||
: undefined,
|
||||
statusPageAnnouncementId: body["statusPageAnnouncementId"]
|
||||
? new ObjectID(body["statusPageAnnouncementId"] as string)
|
||||
: undefined,
|
||||
userId: body["userId"]
|
||||
? new ObjectID(body["userId"] as string)
|
||||
: undefined,
|
||||
onCallPolicyId: body["onCallPolicyId"]
|
||||
? new ObjectID(body["onCallPolicyId"] as string)
|
||||
: undefined,
|
||||
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
|
||||
? new ObjectID(body["onCallPolicyEscalationRuleId"] as string)
|
||||
: undefined,
|
||||
onCallDutyPolicyExecutionLogTimelineId: body[
|
||||
"onCallDutyPolicyExecutionLogTimelineId"
|
||||
]
|
||||
? new ObjectID(
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"] as string,
|
||||
)
|
||||
: undefined,
|
||||
onCallScheduleId: body["onCallScheduleId"]
|
||||
? new ObjectID(body["onCallScheduleId"] as string)
|
||||
: undefined,
|
||||
teamId: body["teamId"]
|
||||
? new ObjectID(body["teamId"] as string)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/test",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
if (!body["toPhone"]) {
|
||||
throw new BadDataException("toPhone is required");
|
||||
}
|
||||
|
||||
const toPhone: Phone = new Phone(body["toPhone"] as string);
|
||||
|
||||
const templateKey: WhatsAppTemplateId =
|
||||
WhatsAppTemplateIds.TestNotification;
|
||||
|
||||
const templateLanguageCode: string =
|
||||
WhatsAppTemplateLanguage[templateKey] || "en";
|
||||
|
||||
const message: WhatsAppMessage = {
|
||||
to: toPhone,
|
||||
body: "",
|
||||
templateKey,
|
||||
templateVariables: undefined,
|
||||
templateLanguageCode,
|
||||
};
|
||||
|
||||
try {
|
||||
await WhatsAppService.sendWhatsApp(message, {
|
||||
projectId: body["projectId"]
|
||||
? new ObjectID(body["projectId"] as string)
|
||||
: undefined,
|
||||
isSensitive: false,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMsg: string =
|
||||
err instanceof Error && err.message
|
||||
? err.message
|
||||
: "Failed to send test WhatsApp message.";
|
||||
|
||||
throw new BadDataException(errorMsg);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -12,6 +12,8 @@ import Phone from "Common/Types/Phone";
|
||||
|
||||
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
|
||||
|
||||
export const DEFAULT_META_WHATSAPP_API_VERSION: string = "v23.0";
|
||||
|
||||
export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
|
||||
async (): Promise<EmailServer | null> => {
|
||||
const globalConfig: GlobalConfig | null =
|
||||
@@ -222,6 +224,83 @@ export const SMSHighRiskCostInCents: number = process.env[
|
||||
? parseInt(process.env["SMS_HIGH_RISK_COST_IN_CENTS"])
|
||||
: 0;
|
||||
|
||||
export interface MetaWhatsAppConfig {
|
||||
accessToken: string;
|
||||
phoneNumberId: string;
|
||||
businessAccountId?: string | undefined;
|
||||
appId?: string | undefined;
|
||||
appSecret?: string | undefined;
|
||||
apiVersion?: string | undefined;
|
||||
}
|
||||
|
||||
type GetMetaWhatsAppConfigFunction = () => Promise<MetaWhatsAppConfig>;
|
||||
|
||||
export const getMetaWhatsAppConfig: GetMetaWhatsAppConfigFunction =
|
||||
async (): Promise<MetaWhatsAppConfig> => {
|
||||
const globalConfig: GlobalConfig | null =
|
||||
await GlobalConfigService.findOneBy({
|
||||
query: {
|
||||
_id: ObjectID.getZeroObjectID().toString(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
metaWhatsAppAccessToken: true,
|
||||
metaWhatsAppPhoneNumberId: true,
|
||||
metaWhatsAppBusinessAccountId: true,
|
||||
metaWhatsAppAppId: true,
|
||||
metaWhatsAppAppSecret: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new BadDataException("Global Config not found");
|
||||
}
|
||||
|
||||
const accessToken: string | undefined =
|
||||
globalConfig.metaWhatsAppAccessToken?.trim();
|
||||
const phoneNumberId: string | undefined =
|
||||
globalConfig.metaWhatsAppPhoneNumberId?.trim();
|
||||
|
||||
if (!accessToken) {
|
||||
throw new BadDataException(
|
||||
"Meta WhatsApp access token not configured. Please set it in the Admin Dashboard: " +
|
||||
AdminDashboardClientURL.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
if (!phoneNumberId) {
|
||||
throw new BadDataException(
|
||||
"Meta WhatsApp phone number ID not configured. Please set it in the Admin Dashboard: " +
|
||||
AdminDashboardClientURL.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const businessAccountId: string | undefined =
|
||||
globalConfig.metaWhatsAppBusinessAccountId?.trim() || undefined;
|
||||
const appId: string | undefined =
|
||||
globalConfig.metaWhatsAppAppId?.trim() || undefined;
|
||||
const appSecret: string | undefined =
|
||||
globalConfig.metaWhatsAppAppSecret?.trim() || undefined;
|
||||
const apiVersion: string = DEFAULT_META_WHATSAPP_API_VERSION;
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
phoneNumberId,
|
||||
businessAccountId,
|
||||
appId,
|
||||
appSecret,
|
||||
apiVersion,
|
||||
};
|
||||
};
|
||||
|
||||
export const WhatsAppTextDefaultCostInCents: number = process.env[
|
||||
"WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"
|
||||
]
|
||||
? parseInt(process.env["WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"])
|
||||
: 0;
|
||||
|
||||
export const CallHighRiskCostInCentsPerMinute: number = process.env[
|
||||
"CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ import CallAPI from "./API/Call";
|
||||
// API
|
||||
import MailAPI from "./API/Mail";
|
||||
import SmsAPI from "./API/SMS";
|
||||
import WhatsAppAPI from "./API/WhatsApp";
|
||||
import PushNotificationAPI from "./API/PushNotification";
|
||||
import SMTPConfigAPI from "./API/SMTPConfig";
|
||||
import "./Utils/Handlebars";
|
||||
@@ -16,6 +17,7 @@ const NotificationFeatureSet: FeatureSet = {
|
||||
|
||||
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
|
||||
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
|
||||
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
|
||||
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
|
||||
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
|
||||
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
|
||||
|
||||
@@ -37,11 +37,49 @@ class TransporterPool {
|
||||
private static semaphore: Map<string, number> = new Map();
|
||||
private static readonly MAX_CONCURRENT_CONNECTIONS = 100;
|
||||
|
||||
private static resolveConnectionSettings(emailServer: EmailServer): {
|
||||
portNumber: number;
|
||||
wantsSecureConnection: boolean;
|
||||
secureConnection: boolean;
|
||||
requireTLS: boolean;
|
||||
mode: "implicit-tls" | "starttls" | "plain";
|
||||
} {
|
||||
const portNumber: number = emailServer.port.toNumber();
|
||||
const wantsSecureConnection: boolean = emailServer.secure;
|
||||
const isImplicitTLSPort: boolean = portNumber === 465;
|
||||
|
||||
const secureConnection: boolean = isImplicitTLSPort;
|
||||
const requireTLS: boolean = wantsSecureConnection && !isImplicitTLSPort;
|
||||
|
||||
let mode: "implicit-tls" | "starttls" | "plain" = "plain";
|
||||
|
||||
if (secureConnection) {
|
||||
mode = "implicit-tls";
|
||||
} else if (requireTLS) {
|
||||
mode = "starttls";
|
||||
}
|
||||
|
||||
return {
|
||||
portNumber,
|
||||
wantsSecureConnection,
|
||||
secureConnection,
|
||||
requireTLS,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
private static getPoolKey(emailServer: EmailServer): string {
|
||||
const { portNumber, mode } = this.resolveConnectionSettings(emailServer);
|
||||
const username: string = emailServer.username || "noauth";
|
||||
|
||||
return `${emailServer.host.toString()}:${portNumber}:${username}:${mode}`;
|
||||
}
|
||||
|
||||
public static getTransporter(
|
||||
emailServer: EmailServer,
|
||||
options: { timeout?: number | undefined },
|
||||
): Transporter {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
|
||||
if (!this.pools.has(key)) {
|
||||
const transporter: Transporter = this.createTransporter(
|
||||
@@ -59,9 +97,12 @@ class TransporterPool {
|
||||
emailServer: EmailServer,
|
||||
options: { timeout?: number | undefined },
|
||||
): Transporter {
|
||||
const { portNumber, wantsSecureConnection, secureConnection, requireTLS } =
|
||||
this.resolveConnectionSettings(emailServer);
|
||||
|
||||
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
|
||||
|
||||
if (!emailServer.secure) {
|
||||
if (!wantsSecureConnection) {
|
||||
tlsOptions = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
@@ -69,8 +110,9 @@ class TransporterPool {
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: emailServer.host.toString(),
|
||||
port: emailServer.port.toNumber(),
|
||||
secure: emailServer.secure,
|
||||
port: portNumber,
|
||||
secure: secureConnection,
|
||||
requireTLS,
|
||||
tls: tlsOptions,
|
||||
auth:
|
||||
emailServer.username && emailServer.password
|
||||
@@ -88,7 +130,7 @@ class TransporterPool {
|
||||
public static async acquireConnection(
|
||||
emailServer: EmailServer,
|
||||
): Promise<void> {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
|
||||
while ((this.semaphore.get(key) || 0) >= this.MAX_CONCURRENT_CONNECTIONS) {
|
||||
await new Promise<void>((resolve: () => void) => {
|
||||
@@ -100,7 +142,7 @@ class TransporterPool {
|
||||
}
|
||||
|
||||
public static releaseConnection(emailServer: EmailServer): void {
|
||||
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
|
||||
const key: string = this.getPoolKey(emailServer);
|
||||
const current: number = this.semaphore.get(key) || 0;
|
||||
this.semaphore.set(key, Math.max(0, current - 1));
|
||||
}
|
||||
|
||||
489
App/FeatureSet/Notification/Services/WhatsAppService.ts
Normal file
489
App/FeatureSet/Notification/Services/WhatsAppService.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import {
|
||||
WhatsAppTextDefaultCostInCents,
|
||||
getMetaWhatsAppConfig,
|
||||
MetaWhatsAppConfig,
|
||||
DEFAULT_META_WHATSAPP_API_VERSION,
|
||||
} from "../Config";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
|
||||
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
|
||||
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
|
||||
import {
|
||||
AuthenticationTemplates,
|
||||
WhatsAppTemplateId,
|
||||
} from "Common/Types/WhatsApp/WhatsAppTemplates";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
|
||||
import NotificationService from "Common/Server/Services/NotificationService";
|
||||
import ProjectService from "Common/Server/Services/ProjectService";
|
||||
import UserOnCallLogTimelineService from "Common/Server/Services/UserOnCallLogTimelineService";
|
||||
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Project from "Common/Models/DatabaseModels/Project";
|
||||
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
|
||||
import API from "Common/Utils/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
const SENSITIVE_MESSAGE_PLACEHOLDER: string =
|
||||
"This message is sensitive and is not logged";
|
||||
|
||||
export default class WhatsAppService {
|
||||
public static async sendWhatsApp(
|
||||
message: WhatsAppMessage,
|
||||
options: {
|
||||
projectId?: ObjectID | undefined;
|
||||
isSensitive?: boolean | undefined;
|
||||
userOnCallLogTimelineId?: ObjectID | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
userId?: ObjectID | undefined;
|
||||
onCallPolicyId?: ObjectID | undefined;
|
||||
onCallPolicyEscalationRuleId?: ObjectID | undefined;
|
||||
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
|
||||
onCallScheduleId?: ObjectID | undefined;
|
||||
teamId?: ObjectID | undefined;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
let sendError: Error | null = null;
|
||||
const whatsAppLog: WhatsAppLog = new WhatsAppLog();
|
||||
|
||||
try {
|
||||
if (!message.to) {
|
||||
throw new BadDataException(
|
||||
"WhatsApp recipient phone number is required",
|
||||
);
|
||||
}
|
||||
|
||||
if (!message.body && !message.templateKey) {
|
||||
throw new BadDataException(
|
||||
"Either WhatsApp message body or template key must be provided",
|
||||
);
|
||||
}
|
||||
|
||||
const config: MetaWhatsAppConfig = await getMetaWhatsAppConfig();
|
||||
|
||||
const isSensitiveMessage: boolean = Boolean(options.isSensitive);
|
||||
const messageSummary: string = isSensitiveMessage
|
||||
? SENSITIVE_MESSAGE_PLACEHOLDER
|
||||
: message.body ||
|
||||
(message.templateKey
|
||||
? `Template: ${message.templateKey}${
|
||||
message.templateVariables
|
||||
? " Variables: " + JSON.stringify(message.templateVariables)
|
||||
: ""
|
||||
}`
|
||||
: "");
|
||||
|
||||
whatsAppLog.toNumber = message.to;
|
||||
whatsAppLog.messageText = messageSummary;
|
||||
whatsAppLog.whatsAppCostInUSDCents = 0;
|
||||
|
||||
if (options.projectId) {
|
||||
whatsAppLog.projectId = options.projectId;
|
||||
}
|
||||
|
||||
if (options.incidentId) {
|
||||
whatsAppLog.incidentId = options.incidentId;
|
||||
}
|
||||
|
||||
if (options.alertId) {
|
||||
whatsAppLog.alertId = options.alertId;
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
|
||||
}
|
||||
|
||||
if (options.statusPageId) {
|
||||
whatsAppLog.statusPageId = options.statusPageId;
|
||||
}
|
||||
|
||||
if (options.statusPageAnnouncementId) {
|
||||
whatsAppLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
|
||||
}
|
||||
|
||||
if (options.userId) {
|
||||
whatsAppLog.userId = options.userId;
|
||||
}
|
||||
|
||||
if (options.teamId) {
|
||||
whatsAppLog.teamId = options.teamId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyId) {
|
||||
whatsAppLog.onCallDutyPolicyId = options.onCallPolicyId;
|
||||
}
|
||||
|
||||
if (options.onCallPolicyEscalationRuleId) {
|
||||
whatsAppLog.onCallDutyPolicyEscalationRuleId =
|
||||
options.onCallPolicyEscalationRuleId;
|
||||
}
|
||||
|
||||
if (options.onCallScheduleId) {
|
||||
whatsAppLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
|
||||
}
|
||||
|
||||
let messageCost: number = 0;
|
||||
const shouldChargeForMessage: boolean = IsBillingEnabled;
|
||||
|
||||
if (shouldChargeForMessage) {
|
||||
messageCost = WhatsAppTextDefaultCostInCents / 100;
|
||||
}
|
||||
|
||||
let project: Project | null = null;
|
||||
|
||||
if (options.projectId) {
|
||||
project = await ProjectService.findOneById({
|
||||
id: options.projectId,
|
||||
select: {
|
||||
smsOrCallCurrentBalanceInUSDCents: true,
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
name: true,
|
||||
notEnabledSmsOrCallNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
whatsAppLog.status = WhatsAppStatus.Error;
|
||||
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldChargeForMessage) {
|
||||
let updatedBalance: number =
|
||||
project.smsOrCallCurrentBalanceInUSDCents || 0;
|
||||
|
||||
try {
|
||||
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
|
||||
project.id!,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
|
||||
|
||||
if (!project.smsOrCallCurrentBalanceInUSDCents) {
|
||||
whatsAppLog.status = WhatsAppStatus.LowBalance;
|
||||
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} does not have enough balance for WhatsApp messages.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await ProjectService.sendEmailToProjectOwners(
|
||||
project.id!,
|
||||
`Low WhatsApp message balance for ${project.name || ""}`,
|
||||
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
|
||||
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
|
||||
} USD. Required balance for this message is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.smsOrCallCurrentBalanceInUSDCents < messageCost * 100) {
|
||||
whatsAppLog.status = WhatsAppStatus.LowBalance;
|
||||
whatsAppLog.statusMessage = `Project does not have enough balance to send WhatsApp message. Current balance is ${
|
||||
project.smsOrCallCurrentBalanceInUSDCents / 100
|
||||
} USD. Required balance is ${messageCost} USD.`;
|
||||
logger.error(whatsAppLog.statusMessage);
|
||||
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
await ProjectService.sendEmailToProjectOwners(
|
||||
project.id!,
|
||||
`Low WhatsApp message balance for ${project.name || ""}`,
|
||||
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
|
||||
project.smsOrCallCurrentBalanceInUSDCents / 100
|
||||
} USD. Required balance is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload: JSONObject = {
|
||||
messaging_product: "whatsapp",
|
||||
recipient_type: "individual",
|
||||
to: message.to.toString(),
|
||||
} as JSONObject;
|
||||
|
||||
if (!message.templateKey) {
|
||||
throw new BadDataException("WhatsApp message template key is required");
|
||||
}
|
||||
|
||||
if (message.templateKey) {
|
||||
const template: JSONObject = {
|
||||
name: message.templateKey,
|
||||
language: {
|
||||
code: message.templateLanguageCode || "en",
|
||||
},
|
||||
} as JSONObject;
|
||||
|
||||
const components: JSONArray = [];
|
||||
|
||||
if (
|
||||
message.templateVariables &&
|
||||
Object.keys(message.templateVariables).length > 0
|
||||
) {
|
||||
const parameters: JSONArray = [];
|
||||
|
||||
for (const [key, value] of Object.entries(
|
||||
message.templateVariables,
|
||||
)) {
|
||||
parameters.push({
|
||||
type: "text",
|
||||
parameter_name: key,
|
||||
text: value,
|
||||
} as JSONObject);
|
||||
}
|
||||
|
||||
if (parameters.length > 0) {
|
||||
components.push({
|
||||
type: "body",
|
||||
parameters,
|
||||
} as JSONObject);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if this is an authentication template
|
||||
* Authentication templates may have special requirements including button components
|
||||
*/
|
||||
const isAuthTemplate: boolean = AuthenticationTemplates.has(
|
||||
message.templateKey as WhatsAppTemplateId,
|
||||
);
|
||||
|
||||
if (isAuthTemplate) {
|
||||
logger.info(
|
||||
`Sending authentication template: ${message.templateKey}`,
|
||||
);
|
||||
|
||||
/*
|
||||
* Authentication templates in WhatsApp may have a button component for "Copy Code"
|
||||
* If the template was created with a button, we need to provide button parameters
|
||||
*/
|
||||
if (message.templateVariables) {
|
||||
const otpCode: string | undefined =
|
||||
message.templateVariables["1"] ||
|
||||
message.templateVariables["otp"] ||
|
||||
message.templateVariables["code"];
|
||||
|
||||
if (otpCode) {
|
||||
/*
|
||||
* Add button component - the index should match the button position in the template
|
||||
* Usually authentication templates have the button as the first (and only) button
|
||||
*/
|
||||
components.push({
|
||||
type: "button",
|
||||
sub_type: "url",
|
||||
index: 0,
|
||||
parameters: [
|
||||
{
|
||||
type: "text",
|
||||
text: otpCode,
|
||||
},
|
||||
],
|
||||
} as JSONObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (components.length > 0) {
|
||||
template["components"] = components;
|
||||
}
|
||||
|
||||
payload["type"] = "template";
|
||||
payload["template"] = template;
|
||||
} else {
|
||||
payload["type"] = "text";
|
||||
payload["text"] = {
|
||||
body: message.body || "",
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const apiVersion: string =
|
||||
config.apiVersion?.trim() || DEFAULT_META_WHATSAPP_API_VERSION;
|
||||
|
||||
const url: URL = new URL(
|
||||
Protocol.HTTPS,
|
||||
"graph.facebook.com",
|
||||
new Route(`${apiVersion}/${config.phoneNumberId}/messages`),
|
||||
);
|
||||
|
||||
logger.debug(`WhatsApp API request: ${JSON.stringify(payload)}`);
|
||||
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post<JSONObject>({
|
||||
url,
|
||||
data: payload,
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Failed to send WhatsApp message.");
|
||||
logger.error(response);
|
||||
const responseDataAsJSONObject: JSONObject = response.data;
|
||||
const responseJsonAsJSONObject: JSONObject | undefined =
|
||||
(response.jsonData as JSONObject | undefined) || undefined;
|
||||
|
||||
// Log full error details for debugging
|
||||
const errorObject: JSONObject | undefined =
|
||||
(responseDataAsJSONObject["error"] as JSONObject | undefined) ||
|
||||
(responseJsonAsJSONObject?.["error"] as JSONObject | undefined);
|
||||
|
||||
if (errorObject) {
|
||||
logger.error("WhatsApp API Error Details:");
|
||||
logger.error(JSON.stringify(errorObject, null, 2));
|
||||
}
|
||||
|
||||
const detailedErrorMessage: string | undefined =
|
||||
((responseDataAsJSONObject["error"] as JSONObject | undefined)?.[
|
||||
"message"
|
||||
] as string | undefined) ||
|
||||
((responseJsonAsJSONObject?.["error"] as JSONObject | undefined)?.[
|
||||
"message"
|
||||
] as string | undefined);
|
||||
|
||||
throw new BadDataException(
|
||||
detailedErrorMessage || "Failed to send WhatsApp message.",
|
||||
);
|
||||
}
|
||||
|
||||
const responseData: JSONObject = (response.jsonData || {}) as JSONObject;
|
||||
|
||||
let messageId: string | undefined = undefined;
|
||||
const messagesArray: JSONArray | undefined =
|
||||
(responseData["messages"] as JSONArray) || undefined;
|
||||
|
||||
if (Array.isArray(messagesArray) && messagesArray.length > 0) {
|
||||
const firstMessage: JSONObject = messagesArray[0] as JSONObject;
|
||||
if (firstMessage["id"]) {
|
||||
messageId = firstMessage["id"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
whatsAppLog.status = WhatsAppStatus.Success;
|
||||
whatsAppLog.statusMessage = messageId
|
||||
? `Message ID: ${messageId}`
|
||||
: "WhatsApp message sent successfully";
|
||||
|
||||
if (shouldChargeForMessage && project) {
|
||||
const deduction: number = Math.floor(messageCost * 100);
|
||||
whatsAppLog.whatsAppCostInUSDCents = deduction;
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(project.smsOrCallCurrentBalanceInUSDCents || 0) - deduction,
|
||||
),
|
||||
);
|
||||
|
||||
await ProjectService.updateOneById({
|
||||
id: project.id!,
|
||||
data: {
|
||||
smsOrCallCurrentBalanceInUSDCents:
|
||||
project.smsOrCallCurrentBalanceInUSDCents,
|
||||
notEnabledSmsOrCallNotificationSentToOwners: false,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to send WhatsApp message.");
|
||||
logger.error(error);
|
||||
whatsAppLog.whatsAppCostInUSDCents = 0;
|
||||
whatsAppLog.status = WhatsAppStatus.Error;
|
||||
const errorMessage: string =
|
||||
error && error.message ? error.message.toString() : `${error}`;
|
||||
whatsAppLog.statusMessage = errorMessage;
|
||||
|
||||
sendError = error;
|
||||
}
|
||||
|
||||
if (options.projectId) {
|
||||
await WhatsAppLogService.create({
|
||||
data: whatsAppLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (options.userOnCallLogTimelineId) {
|
||||
await UserOnCallLogTimelineService.updateOneById({
|
||||
id: options.userOnCallLogTimelineId,
|
||||
data: {
|
||||
status:
|
||||
whatsAppLog.status === WhatsAppStatus.Success
|
||||
? UserNotificationStatus.Sent
|
||||
: UserNotificationStatus.Error,
|
||||
statusMessage: whatsAppLog.statusMessage,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (sendError) {
|
||||
throw sendError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,96 @@ export default class GlobalConfig extends GlobalConfigModel {
|
||||
})
|
||||
public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers separated by comma
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Meta WhatsApp Access Token",
|
||||
description:
|
||||
"Access token generated from Meta for sending WhatsApp messages.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAccessToken?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp Phone Number ID",
|
||||
description: "The WhatsApp Business phone number ID from Meta.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppPhoneNumberId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp Business Account ID",
|
||||
description: "Business account ID associated with your WhatsApp setup.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppBusinessAccountId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Meta WhatsApp App ID",
|
||||
description:
|
||||
"Facebook App ID used for the WhatsApp Business Platform integration.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAppId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Meta WhatsApp App Secret",
|
||||
description: "Facebook App Secret for the WhatsApp Business Platform.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: true,
|
||||
})
|
||||
public metaWhatsAppAppSecret?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
|
||||
@@ -100,6 +100,7 @@ import ServiceCopilotCodeRepository from "./ServiceCopilotCodeRepository";
|
||||
import ShortLink from "./ShortLink";
|
||||
// SMS
|
||||
import SmsLog from "./SmsLog";
|
||||
import WhatsAppLog from "./WhatsAppLog";
|
||||
import PushNotificationLog from "./PushNotificationLog";
|
||||
import WorkspaceNotificationLog from "./WorkspaceNotificationLog";
|
||||
// Status Page
|
||||
@@ -131,6 +132,7 @@ import UserCall from "./UserCall";
|
||||
// Notification Methods
|
||||
import UserEmail from "./UserEmail";
|
||||
import UserPush from "./UserPush";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
// User Notification Rules
|
||||
import UserNotificationRule from "./UserNotificationRule";
|
||||
import UserNotificationSetting from "./UserNotificationSetting";
|
||||
@@ -297,6 +299,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageOwnerUser,
|
||||
|
||||
SmsLog,
|
||||
WhatsAppLog,
|
||||
PushNotificationLog,
|
||||
WorkspaceNotificationLog,
|
||||
CallLog,
|
||||
@@ -306,6 +309,7 @@ const AllModelTypes: Array<{
|
||||
UserSms,
|
||||
UserCall,
|
||||
UserPush,
|
||||
UserWhatsApp,
|
||||
|
||||
UserNotificationRule,
|
||||
UserOnCallLog,
|
||||
|
||||
@@ -798,6 +798,33 @@ export default class Project extends TenantModel {
|
||||
})
|
||||
public enableSmsNotifications?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProject,
|
||||
Permission.UnAuthorizedSsoUser,
|
||||
Permission.ProjectUser,
|
||||
],
|
||||
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable WhatsApp Notifications",
|
||||
description: "Enable WhatsApp notifications for this project.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: false,
|
||||
type: ColumnType.Boolean,
|
||||
})
|
||||
public enableWhatsAppNotifications?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserCall from "./UserCall";
|
||||
import UserEmail from "./UserEmail";
|
||||
import UserPush from "./UserPush";
|
||||
import UserSMS from "./UserSMS";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -383,6 +384,53 @@ class UserNotificationRule extends BaseModel {
|
||||
})
|
||||
public userSmsId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userWhatsAppId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: UserWhatsApp,
|
||||
title: "User WhatsApp",
|
||||
description:
|
||||
"Relation to User WhatsApp Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return UserWhatsApp;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userWhatsAppId" })
|
||||
public userWhatsApp?: UserWhatsApp = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User WhatsApp ID",
|
||||
description: "ID of User WhatsApp in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userWhatsAppId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
@@ -273,6 +273,22 @@ class UserNotificationSetting extends BaseModel {
|
||||
})
|
||||
public alertBySMS?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public alertByWhatsApp?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
@@ -13,6 +13,7 @@ import UserNotificationRule from "./UserNotificationRule";
|
||||
import UserPush from "./UserPush";
|
||||
import UserOnCallLog from "./UserOnCallLog";
|
||||
import UserSMS from "./UserSMS";
|
||||
import UserWhatsApp from "./UserWhatsApp";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
@@ -834,6 +835,53 @@ export default class UserOnCallLogTimeline extends BaseModel {
|
||||
})
|
||||
public userSmsId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userWhatsAppId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: UserWhatsApp,
|
||||
title: "User WhatsApp",
|
||||
description:
|
||||
"Relation to User WhatsApp Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return UserWhatsApp;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userWhatsAppId" })
|
||||
public userWhatsApp?: UserWhatsApp = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User WhatsApp ID",
|
||||
description: "ID of User WhatsApp in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userWhatsAppId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
|
||||
288
Common/Models/DatabaseModels/UserWhatsApp.ts
Normal file
288
Common/Models/DatabaseModels/UserWhatsApp.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
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 Phone from "../../Types/Phone";
|
||||
import Text from "../../Types/Text";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-whatsapp"))
|
||||
@Entity({
|
||||
name: "UserWhatsApp",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserWhatsApp",
|
||||
singularName: "WhatsApp Number",
|
||||
pluralName: "WhatsApp Numbers",
|
||||
icon: IconProp.WhatsApp,
|
||||
tableDescription: "WhatsApp numbers used for WhatsApp notifications.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserWhatsApp 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: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "WhatsApp Number",
|
||||
required: true,
|
||||
unique: false,
|
||||
type: TableColumnType.Phone,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public phone?: Phone = 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 WhatsApp number 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 WhatsApp number 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",
|
||||
modelType: 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;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Is Verified",
|
||||
description: "Is this WhatsApp number verified?",
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public isVerified?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Verification Code",
|
||||
description: "Temporary Verification Code",
|
||||
isDefaultValueColumn: true,
|
||||
computed: true,
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
forceGetDefaultValueOnCreate: () => {
|
||||
return Text.generateRandomNumber(6);
|
||||
},
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
nullable: false,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public verificationCode?: string = undefined;
|
||||
}
|
||||
|
||||
export default UserWhatsApp;
|
||||
884
Common/Models/DatabaseModels/WhatsAppLog.ts
Normal file
884
Common/Models/DatabaseModels/WhatsAppLog.ts
Normal file
@@ -0,0 +1,884 @@
|
||||
import Project from "./Project";
|
||||
import Incident from "./Incident";
|
||||
import Alert from "./Alert";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPageAnnouncement from "./StatusPageAnnouncement";
|
||||
import User from "./User";
|
||||
import OnCallDutyPolicy from "./OnCallDutyPolicy";
|
||||
import OnCallDutyPolicyEscalationRule from "./OnCallDutyPolicyEscalationRule";
|
||||
import OnCallDutyPolicySchedule from "./OnCallDutyPolicySchedule";
|
||||
import Team from "./Team";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
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 EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
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 Phone from "../../Types/Phone";
|
||||
import WhatsAppStatus from "../../Types/WhatsAppStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/whatsapp-log"))
|
||||
@Entity({
|
||||
name: "WhatsAppLog",
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: false,
|
||||
update: false,
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "WhatsAppLog",
|
||||
singularName: "WhatsApp Log",
|
||||
pluralName: "WhatsApp Logs",
|
||||
icon: IconProp.WhatsApp,
|
||||
tableDescription:
|
||||
"Logs of all the WhatsApp messages sent out to all users and subscribers for this project.",
|
||||
})
|
||||
export default class WhatsAppLog extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
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: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
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: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Phone,
|
||||
title: "To Number",
|
||||
description: "Phone Number WhatsApp message was sent to",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public toNumber?: Phone = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Phone,
|
||||
title: "From Number",
|
||||
description:
|
||||
"Phone Number WhatsApp message was sent from (Business Number ID)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Phone,
|
||||
length: ColumnLength.Phone,
|
||||
transformer: Phone.getDatabaseTransformer(),
|
||||
})
|
||||
public fromNumber?: Phone = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Message Text",
|
||||
description: "Text content of the WhatsApp message",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public messageText?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Status Message",
|
||||
description: "Status Message (if any)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public statusMessage?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Status of the WhatsApp Message",
|
||||
description: "Status of the WhatsApp message sent",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public status?: WhatsAppStatus = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "WhatsApp Cost",
|
||||
description: "WhatsApp Message Cost in USD Cents",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public whatsAppCostInUSDCents?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "incidentId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Incident,
|
||||
title: "Incident",
|
||||
description: "Incident associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Incident;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "incidentId" })
|
||||
public incident?: Incident = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Incident ID",
|
||||
description: "ID of Incident associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public incidentId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User who initiated this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User ID",
|
||||
description: "ID of User who initiated this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "alertId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Alert,
|
||||
title: "Alert",
|
||||
description: "Alert associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Alert;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "alertId" })
|
||||
public alert?: Alert = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Alert ID",
|
||||
description: "ID of Alert associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public alertId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "scheduledMaintenanceId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: ScheduledMaintenance,
|
||||
title: "Scheduled Maintenance",
|
||||
description: "Scheduled Maintenance associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return ScheduledMaintenance;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "scheduledMaintenanceId" })
|
||||
public scheduledMaintenance?: ScheduledMaintenance = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Scheduled Maintenance ID",
|
||||
description:
|
||||
"ID of Scheduled Maintenance associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public scheduledMaintenanceId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Status Page associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Status Page ID",
|
||||
description: "ID of Status Page associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageAnnouncementId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPageAnnouncement,
|
||||
title: "Status Page Announcement",
|
||||
description:
|
||||
"Status Page Announcement associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPageAnnouncement;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageAnnouncementId" })
|
||||
public statusPageAnnouncement?: StatusPageAnnouncement = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Status Page Announcement ID",
|
||||
description:
|
||||
"ID of Status Page Announcement associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageAnnouncementId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "teamId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Team,
|
||||
title: "Team",
|
||||
description: "Team associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Team;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "teamId" })
|
||||
public team?: Team = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Team ID",
|
||||
description: "ID of Team associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public teamId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicy,
|
||||
title: "On-Call Duty Policy",
|
||||
description: "On-Call Duty Policy associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicy;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyId" })
|
||||
public onCallDutyPolicy?: OnCallDutyPolicy = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyEscalationRuleId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicyEscalationRule,
|
||||
title: "On-Call Duty Policy Escalation Rule",
|
||||
description:
|
||||
"On-Call Duty Policy Escalation Rule associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicyEscalationRule;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyEscalationRuleId" })
|
||||
public onCallDutyPolicyEscalationRule?: OnCallDutyPolicyEscalationRule =
|
||||
undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy Escalation Rule ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy Escalation Rule associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyEscalationRuleId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "onCallDutyPolicyScheduleId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: OnCallDutyPolicySchedule,
|
||||
title: "On-Call Duty Policy Schedule",
|
||||
description:
|
||||
"On-Call Duty Policy Schedule associated with this message (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return OnCallDutyPolicySchedule;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "onCallDutyPolicyScheduleId" })
|
||||
public onCallDutyPolicySchedule?: OnCallDutyPolicySchedule = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadSmsLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "On-Call Duty Policy Schedule ID",
|
||||
description:
|
||||
"ID of On-Call Duty Policy Schedule associated with this message (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public onCallDutyPolicyScheduleId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: 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;
|
||||
}
|
||||
@@ -58,22 +58,30 @@ export default class MicrosoftTeamsAPI {
|
||||
"https://developer.microsoft.com/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
|
||||
manifestVersion: "1.23",
|
||||
version: AppVersion.toLowerCase().includes("unknown")
|
||||
? "1.1.0"
|
||||
? "1.3.0"
|
||||
: AppVersion,
|
||||
id: MicrosoftTeamsAppClientId,
|
||||
developer: {
|
||||
name: "OneUptime",
|
||||
name: "HackerBay Inc",
|
||||
websiteUrl: "https://oneuptime.com",
|
||||
privacyUrl: "https://oneuptime.com/legal/privacy",
|
||||
termsOfUseUrl: "https://oneuptime.com/legal/terms",
|
||||
},
|
||||
publisherDocsUrl:
|
||||
"https://oneuptime.com/docs/workspace-connections/microsoft-teams",
|
||||
name: {
|
||||
short: "OneUptime",
|
||||
full: "OneUptime - Complete Observability Platform",
|
||||
},
|
||||
description: {
|
||||
short: "Monitor your apps, websites, APIs, and more with OneUptime",
|
||||
full: "OneUptime is a complete open-source observability platform that helps you monitor your applications, websites, APIs, and infrastructure. Get alerted when things go wrong and maintain your SLAs.",
|
||||
short: "Complete open-source monitoring and observability platform. ",
|
||||
full: `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.
|
||||
|
||||
In order to use the app, you need to have an active account with OneUptime at https://oneuptime.com. Please send an email to support@oneupitme.com if you need more details.
|
||||
|
||||
Create a new OneUptime Account: If you wish to sign up for a new account, you can do so at https://oneuptime.com and click on Sign up.
|
||||
Help and Support: You can reach out to help and support here: https://oneuptime.com/support or contact support@oneuptime.com
|
||||
`,
|
||||
},
|
||||
// Default to size-specific names; route will adjust if fallbacks are used
|
||||
icons: {
|
||||
|
||||
@@ -4,11 +4,11 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import JSONFunctions from "../../Types/JSONFunctions";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission, { UserPermission } from "../../Types/Permission";
|
||||
@@ -19,7 +19,7 @@ const router: ExpressRouter = Express.getRouter();
|
||||
router.post(
|
||||
"/notification/recharge",
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
let amount: number | PositiveNumber = JSONFunctions.deserializeValue(
|
||||
req.body.amount,
|
||||
@@ -106,8 +106,8 @@ router.post(
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
return Response.sendErrorResponse(req, res, err as Exception);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import ProjectSsoService, {
|
||||
Service as ProjectSsoServiceType,
|
||||
} from "../Services/ProjectSsoService";
|
||||
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
@@ -22,43 +26,47 @@ export default class ProjectSsoAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/:projectId/sso-list`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params["projectId"] as string,
|
||||
);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params["projectId"] as string,
|
||||
);
|
||||
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
if (!projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid project id."),
|
||||
);
|
||||
}
|
||||
|
||||
const sso: Array<ProjectSSO> = await this.service.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
isEnabled: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid project id."),
|
||||
sso,
|
||||
new PositiveNumber(sso.length),
|
||||
ProjectSSO,
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const sso: Array<ProjectSSO> = await this.service.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
isEnabled: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
sso,
|
||||
new PositiveNumber(sso.length),
|
||||
ProjectSSO,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import ShortLinkService, {
|
||||
Service as ShortLinkServiceType,
|
||||
} from "../Services/ShortLinkService";
|
||||
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
@@ -18,34 +22,38 @@ export default class ShortLinkAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/redirect-to-shortlink/:id`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
if (!req.params["id"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("id is required"),
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.params["id"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("id is required"),
|
||||
);
|
||||
}
|
||||
|
||||
if (req.params["id"] === "status") {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
status: "ok",
|
||||
});
|
||||
}
|
||||
|
||||
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
|
||||
req.params["id"],
|
||||
);
|
||||
|
||||
if (!link || !link.link) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("This URL is invalid or expired"),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, link.link);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (req.params["id"] === "status") {
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
status: "ok",
|
||||
});
|
||||
}
|
||||
|
||||
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
|
||||
req.params["id"],
|
||||
);
|
||||
|
||||
if (!link || !link.link) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("This URL is invalid or expired"),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, link.link);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserCallService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -23,97 +24,105 @@ export default class UserCallAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-call/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-call/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserEmailService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -22,77 +23,81 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the email.
|
||||
const item: UserEmail | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the email.
|
||||
const item: UserEmail | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -101,20 +106,24 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
.getCrudApiPath()
|
||||
?.toString()}/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserOnCallLogTimelineService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -34,62 +35,66 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
.getCrudApiPath()
|
||||
?.toString()}/call/gather-input/:itemId`,
|
||||
NotificationMiddleware.isValidCallNotificationRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
const token: JSONObject = (req as any).callTokenData;
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
// check digits.
|
||||
|
||||
if (req.body["Digits"] === "1") {
|
||||
// then ack incident
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NotificationMiddleware.sendResponse(req, res, token as any);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const token: JSONObject = (req as any).callTokenData;
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
// check digits.
|
||||
|
||||
if (req.body["Digits"] === "1") {
|
||||
// then ack incident
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NotificationMiddleware.sendResponse(req, res, token as any);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -102,73 +107,77 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge-page/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
const notificationType: string = timelineItem.triggeredByIncidentId
|
||||
? "Incident"
|
||||
: "Alert";
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
description: true,
|
||||
},
|
||||
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
|
||||
{
|
||||
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `Do you want to acknowledge this ${notificationType}?`,
|
||||
acknowledgeText: `Acknowledge ${notificationType}`,
|
||||
acknowledgeUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + itemId.toString()),
|
||||
).toString(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const notificationType: string = timelineItem.triggeredByIncidentId
|
||||
? "Incident"
|
||||
: "Alert";
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
|
||||
{
|
||||
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `Do you want to acknowledge this ${notificationType}?`,
|
||||
acknowledgeText: `Acknowledge ${notificationType}`,
|
||||
acknowledgeUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + itemId.toString()),
|
||||
).toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -177,124 +186,128 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/acknowledge/:itemId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
if (!req.params["itemId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
|
||||
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
const timelineItem: UserOnCallLogTimeline | null =
|
||||
await this.service.findOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
},
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
},
|
||||
acknowledgedAt: true,
|
||||
isAcknowledged: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
}
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
if (timelineItem.isAcknowledged) {
|
||||
// already acknowledged. Then show already acknowledged page with view details button.
|
||||
|
||||
const viewDetailsUrl: URL = new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/ViewMessage.ejs",
|
||||
{
|
||||
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `This notification has already been acknowledged.`,
|
||||
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
|
||||
viewDetailsUrl: viewDetailsUrl.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
triggeredByIncidentId: true,
|
||||
triggeredByAlertId: true,
|
||||
triggeredByAlert: {
|
||||
title: true,
|
||||
},
|
||||
triggeredByIncident: {
|
||||
title: true,
|
||||
},
|
||||
acknowledgedAt: true,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!timelineItem) {
|
||||
// redirect to dashboard to incidents page.
|
||||
|
||||
if (timelineItem.triggeredByIncidentId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (timelineItem.triggeredByAlertId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
if (timelineItem.isAcknowledged) {
|
||||
// already acknowledged. Then show already acknowledged page with view details button.
|
||||
|
||||
const viewDetailsUrl: URL = new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
"/usr/src/Common/Server/Views/ViewMessage.ejs",
|
||||
{
|
||||
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
|
||||
message: `This notification has already been acknowledged.`,
|
||||
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
|
||||
viewDetailsUrl: viewDetailsUrl.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: itemId,
|
||||
data: {
|
||||
acknowledgedAt: OneUptimeDate.getCurrentDate(),
|
||||
isAcknowledged: true,
|
||||
status: UserNotificationStatus.Acknowledged,
|
||||
statusMessage: "Notification Acknowledged",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// redirect to dashboard to incidents page.
|
||||
|
||||
if (timelineItem.triggeredByIncidentId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (timelineItem.triggeredByAlertId) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
DashboardRoute.addRoute(
|
||||
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item Id"),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,104 +108,104 @@ export default class UserPushAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/test-notification`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the device
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
deviceName: true,
|
||||
deviceToken: true,
|
||||
deviceType: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!device.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device is not verified"),
|
||||
);
|
||||
}
|
||||
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
// Send test notification
|
||||
const testMessage: PushNotificationMessage =
|
||||
PushNotificationUtil.createGenericNotification({
|
||||
title: "Test Notification from OneUptime",
|
||||
body: "This is a test notification to verify your device is working correctly.",
|
||||
clickAction: "/dashboard",
|
||||
tag: "test-notification",
|
||||
requireInteraction: false,
|
||||
});
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
await PushNotificationService.sendPushNotification(
|
||||
{
|
||||
devices: [
|
||||
{
|
||||
token: device.deviceToken!,
|
||||
...(device.deviceName && {
|
||||
name: device.deviceName,
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: testMessage,
|
||||
deviceType: device.deviceType!,
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the device
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
{
|
||||
isSensitive: false,
|
||||
projectId: device.projectId!,
|
||||
userId: device.userId!,
|
||||
select: {
|
||||
userId: true,
|
||||
deviceName: true,
|
||||
deviceToken: true,
|
||||
deviceType: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!device.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device is not verified"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Send test notification
|
||||
const testMessage: PushNotificationMessage =
|
||||
PushNotificationUtil.createGenericNotification({
|
||||
title: "Test Notification from OneUptime",
|
||||
body: "This is a test notification to verify your device is working correctly.",
|
||||
clickAction: "/dashboard",
|
||||
tag: "test-notification",
|
||||
requireInteraction: false,
|
||||
});
|
||||
|
||||
await PushNotificationService.sendPushNotification(
|
||||
{
|
||||
devices: [
|
||||
{
|
||||
token: device.deviceToken!,
|
||||
...(device.deviceName && {
|
||||
name: device.deviceName,
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: testMessage,
|
||||
deviceType: device.deviceType!,
|
||||
},
|
||||
{
|
||||
isSensitive: false,
|
||||
projectId: device.projectId!,
|
||||
userId: device.userId!,
|
||||
},
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new BadDataException(
|
||||
`Failed to send test notification: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
message: "Test notification sent successfully",
|
||||
});
|
||||
} catch (error: any) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
`Failed to send test notification: ${error.message}`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -213,100 +213,108 @@ export default class UserPushAPI extends BaseAPI<
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.verifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.verifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-push/:deviceId/unverify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.unverifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const device: UserPush | null = await this.service.findOneById({
|
||||
id: new ObjectID(req.params["deviceId"]),
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Device not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Unauthorized access to device"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.unverifyDevice(device._id!.toString());
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserSMSService, {
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
@@ -19,97 +20,105 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
|
||||
this.router.post(
|
||||
`/user-sms/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the code matches and verify the phone number.
|
||||
const item: UserSMS | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
//check user id
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`/user-sms/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
req = req as OneUptimeRequest;
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
136
Common/Server/API/UserWhatsAppAPI.ts
Normal file
136
Common/Server/API/UserWhatsAppAPI.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserWhatsAppService, {
|
||||
Service as UserWhatsAppServiceType,
|
||||
} from "../Services/UserWhatsAppService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
||||
|
||||
export default class UserWhatsAppAPI extends BaseAPI<
|
||||
UserWhatsApp,
|
||||
UserWhatsAppServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(UserWhatsApp, UserWhatsAppService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/verify`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.body.code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
const item: UserWhatsApp | null = await this.service.findOneById({
|
||||
id: req.body["itemId"],
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Item not found"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.isVerified) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("WhatsApp number already verified"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
item.userId?.toString() !==
|
||||
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid user ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (item.verificationCode !== req.body["code"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid code"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/resend-verification-code`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (!req.body.itemId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid item ID"),
|
||||
);
|
||||
}
|
||||
|
||||
await this.service.resendVerificationCode(req.body.itemId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,331 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1759943124812 implements MigrationInterface {
|
||||
public name = "MigrationName1759943124812";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "WhatsAppLog" ("_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, "toNumber" character varying(30) NOT NULL, "fromNumber" character varying(30), "messageText" text, "statusMessage" character varying(500), "status" character varying(100) NOT NULL, "whatsAppCostInUSDCents" integer NOT NULL DEFAULT '0', "incidentId" uuid, "userId" uuid, "alertId" uuid, "scheduledMaintenanceId" uuid, "statusPageId" uuid, "statusPageAnnouncementId" uuid, "teamId" uuid, "onCallDutyPolicyId" uuid, "onCallDutyPolicyEscalationRuleId" uuid, "onCallDutyPolicyScheduleId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_9800b27ad5072db21ff1e453300" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_20e246495b31ec9720529ec13a" ON "WhatsAppLog" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ee24b8a69670171de6c19fdcaf" ON "WhatsAppLog" ("toNumber") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f9cf8acb2f63698431f4f18f48" ON "WhatsAppLog" ("fromNumber") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_98e71cf97956e7938195be8451" ON "WhatsAppLog" ("whatsAppCostInUSDCents") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2f4c03d17243b8b3ddae2677ae" ON "WhatsAppLog" ("incidentId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_020a8349a02b6cfc79129a8deb" ON "WhatsAppLog" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_431a6f11acfd2795b17c652fbb" ON "WhatsAppLog" ("alertId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_048963c43c534478290408bdd7" ON "WhatsAppLog" ("scheduledMaintenanceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_16379405298dc2b312ff456fc8" ON "WhatsAppLog" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4136974e8f1832f03df27057a7" ON "WhatsAppLog" ("statusPageAnnouncementId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a638be5c4d5e8551f7be91dd8b" ON "WhatsAppLog" ("teamId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_dc907429d63e2d860bb290124e" ON "WhatsAppLog" ("onCallDutyPolicyId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_486f56105b72ee019cd9634272" ON "WhatsAppLog" ("onCallDutyPolicyEscalationRuleId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f78a2014c70e4846df79f9e681" ON "WhatsAppLog" ("onCallDutyPolicyScheduleId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserWhatsApp" ("_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, "phone" character varying(30) NOT NULL, "userId" uuid, "createdByUserId" uuid, "deletedByUserId" uuid, "isVerified" boolean NOT NULL DEFAULT false, "verificationCode" character varying(100) NOT NULL, CONSTRAINT "PK_19ab8aa5949cb38d08930e959ad" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_cacaefed4f479bf300d4065c80" ON "UserWhatsApp" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b99e3db0cecd0e5f15b1f6738a" ON "UserWhatsApp" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "enableWhatsAppNotifications" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAccessToken" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_83bd5d0c54a21bfe12316fa6520" UNIQUE ("metaWhatsAppAccessToken")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppPhoneNumberId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_ef032cda9dd2fac68daeedd7bd2" UNIQUE ("metaWhatsAppPhoneNumberId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppBusinessAccountId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_607e6e88215689951d9b3645f00" UNIQUE ("metaWhatsAppBusinessAccountId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAppId" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_e67fd0998ca781ec7db0e573e91" UNIQUE ("metaWhatsAppAppId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppAppSecret" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_d4669bf754f937bae16c4a1837c" UNIQUE ("metaWhatsAppAppSecret")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" ADD "userWhatsAppId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationSetting" ADD "alertByWhatsApp" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" ADD "userWhatsAppId" uuid`,
|
||||
);
|
||||
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(
|
||||
`CREATE INDEX "IDX_73297560a1a70e4fe47eac2986" ON "UserNotificationRule" ("userWhatsAppId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0a67c82e4e093ae5c89d2d76bd" ON "UserOnCallLogTimeline" ("userWhatsAppId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_20e246495b31ec9720529ec13a6" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_2f4c03d17243b8b3ddae2677ae1" FOREIGN KEY ("incidentId") REFERENCES "Incident"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_020a8349a02b6cfc79129a8deba" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_431a6f11acfd2795b17c652fbb5" FOREIGN KEY ("alertId") REFERENCES "Alert"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_048963c43c534478290408bdd78" FOREIGN KEY ("scheduledMaintenanceId") REFERENCES "ScheduledMaintenance"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_16379405298dc2b312ff456fc88" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_4136974e8f1832f03df27057a7e" FOREIGN KEY ("statusPageAnnouncementId") REFERENCES "StatusPageAnnouncement"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_a638be5c4d5e8551f7be91dd8be" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_dc907429d63e2d860bb290124e3" FOREIGN KEY ("onCallDutyPolicyId") REFERENCES "OnCallDutyPolicy"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_486f56105b72ee019cd96342723" FOREIGN KEY ("onCallDutyPolicyEscalationRuleId") REFERENCES "OnCallDutyPolicyEscalationRule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_f78a2014c70e4846df79f9e681a" FOREIGN KEY ("onCallDutyPolicyScheduleId") REFERENCES "OnCallDutyPolicySchedule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" ADD CONSTRAINT "FK_2f75dca0a039aa9384de646f759" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_cacaefed4f479bf300d4065c802" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_b99e3db0cecd0e5f15b1f6738aa" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_57d2f22db228562775e3274975a" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" ADD CONSTRAINT "FK_e90592dde8357dd1afbf19073d8" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" ADD CONSTRAINT "FK_73297560a1a70e4fe47eac29861" FOREIGN KEY ("userWhatsAppId") REFERENCES "UserWhatsApp"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" ADD CONSTRAINT "FK_0a67c82e4e093ae5c89d2d76bdf" FOREIGN KEY ("userWhatsAppId") REFERENCES "UserWhatsApp"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" DROP CONSTRAINT "FK_0a67c82e4e093ae5c89d2d76bdf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" DROP CONSTRAINT "FK_73297560a1a70e4fe47eac29861"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_e90592dde8357dd1afbf19073d8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_57d2f22db228562775e3274975a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_b99e3db0cecd0e5f15b1f6738aa"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserWhatsApp" DROP CONSTRAINT "FK_cacaefed4f479bf300d4065c802"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_2f75dca0a039aa9384de646f759"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_f78a2014c70e4846df79f9e681a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_486f56105b72ee019cd96342723"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_dc907429d63e2d860bb290124e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_a638be5c4d5e8551f7be91dd8be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_4136974e8f1832f03df27057a7e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_16379405298dc2b312ff456fc88"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_048963c43c534478290408bdd78"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_431a6f11acfd2795b17c652fbb5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_020a8349a02b6cfc79129a8deba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_2f4c03d17243b8b3ddae2677ae1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "WhatsAppLog" DROP CONSTRAINT "FK_20e246495b31ec9720529ec13a6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0a67c82e4e093ae5c89d2d76bd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_73297560a1a70e4fe47eac2986"`,
|
||||
);
|
||||
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(
|
||||
`ALTER TABLE "UserOnCallLogTimeline" DROP COLUMN "userWhatsAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationSetting" DROP COLUMN "alertByWhatsApp"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserNotificationRule" DROP COLUMN "userWhatsAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_d4669bf754f937bae16c4a1837c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAppSecret"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_e67fd0998ca781ec7db0e573e91"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAppId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_607e6e88215689951d9b3645f00"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppBusinessAccountId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_ef032cda9dd2fac68daeedd7bd2"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppPhoneNumberId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_83bd5d0c54a21bfe12316fa6520"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppAccessToken"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "enableWhatsAppNotifications"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b99e3db0cecd0e5f15b1f6738a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_cacaefed4f479bf300d4065c80"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserWhatsApp"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f78a2014c70e4846df79f9e681"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_486f56105b72ee019cd9634272"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_dc907429d63e2d860bb290124e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_a638be5c4d5e8551f7be91dd8b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_4136974e8f1832f03df27057a7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_16379405298dc2b312ff456fc8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_048963c43c534478290408bdd7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_431a6f11acfd2795b17c652fbb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_020a8349a02b6cfc79129a8deb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2f4c03d17243b8b3ddae2677ae"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_98e71cf97956e7938195be8451"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f9cf8acb2f63698431f4f18f48"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ee24b8a69670171de6c19fdcaf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_20e246495b31ec9720529ec13a"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "WhatsAppLog"`);
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,7 @@ import { MigrationName1758798730753 } from "./1758798730753-MigrationName";
|
||||
import { MigrationName1759175457008 } from "./1759175457008-MigrationName";
|
||||
import { MigrationName1759232954703 } from "./1759232954703-MigrationName";
|
||||
import { RenameUserTwoFactorAuthToUserTotpAuth1759234532998 } from "./1759234532998-MigrationName";
|
||||
import { MigrationName1759943124812 } from "./1759943124812-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -352,4 +353,5 @@ export default [
|
||||
MigrationName1759175457008,
|
||||
MigrationName1759232954703,
|
||||
RenameUserTwoFactorAuthToUserTotpAuth1759234532998,
|
||||
MigrationName1759943124812,
|
||||
];
|
||||
|
||||
@@ -91,6 +91,7 @@ import ServiceCopilotCodeRepositoryService from "./ServiceCopilotCodeRepositoryS
|
||||
import ShortLinkService from "./ShortLinkService";
|
||||
// SMS Log Service
|
||||
import SmsLogService from "./SmsLogService";
|
||||
import WhatsAppLogService from "./WhatsAppLogService";
|
||||
import SmsService from "./SmsService";
|
||||
import SpanService from "./SpanService";
|
||||
import StatusPageAnnouncementService from "./StatusPageAnnouncementService";
|
||||
@@ -126,6 +127,7 @@ import UserService from "./UserService";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
import UserWhatsAppService from "./UserWhatsAppService";
|
||||
import WorkflowLogService from "./WorkflowLogService";
|
||||
// Workflows.
|
||||
import WorkflowService from "./WorkflowService";
|
||||
@@ -250,6 +252,7 @@ const services: Array<BaseService> = [
|
||||
|
||||
ShortLinkService,
|
||||
SmsLogService,
|
||||
WhatsAppLogService,
|
||||
SmsService,
|
||||
|
||||
StatusPageAnnouncementService,
|
||||
@@ -281,6 +284,7 @@ const services: Array<BaseService> = [
|
||||
UserOnCallLogService,
|
||||
UserOnCallLogTimelineService,
|
||||
UserSmsService,
|
||||
UserWhatsAppService,
|
||||
UserTotpAuthService,
|
||||
UserWebAuthnService,
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ import logger from "../Utils/Logger";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -1191,6 +1193,19 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
],
|
||||
};
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_NO_PROBES_ARE_MONITORING_THE_MONITOR;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
monitor_name: monitor.name!,
|
||||
probe_status: enabledStatus,
|
||||
monitor_link: vars["monitorViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: owner.id!,
|
||||
projectId: monitor.projectId!,
|
||||
@@ -1205,8 +1220,8 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
monitorId: monitor.id!.toString(),
|
||||
monitorName: monitor.name!,
|
||||
}),
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_NO_PROBES_ARE_MONITORING_THE_MONITOR,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1298,6 +1313,19 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
],
|
||||
};
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_PORBE_STATUS_CHANGES;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
monitor_name: monitor.name!,
|
||||
probe_status: status,
|
||||
monitor_link: vars["monitorViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: owner.id!,
|
||||
projectId: monitor.projectId!,
|
||||
@@ -1309,8 +1337,8 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
monitorName: monitor.name!,
|
||||
monitorId: monitor.id!.toString(),
|
||||
}),
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_PORBE_STATUS_CHANGES,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCal
|
||||
import { Gray500, Red500 } from "../../Types/BrandColors";
|
||||
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -135,6 +137,21 @@ export class Service extends DatabaseService<Model> {
|
||||
policyName: createdModel.onCallDutyPolicy?.name || "No name provided",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
createdModel.onCallDutyPolicy?.name || "No name provided",
|
||||
schedule_name: scheduleName,
|
||||
on_call_context: `schedule ${scheduleName}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: createdModel!.projectId!,
|
||||
@@ -142,8 +159,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
|
||||
// add workspace message.
|
||||
@@ -317,6 +334,21 @@ export class Service extends DatabaseService<Model> {
|
||||
policyName: deletedItem.onCallDutyPolicy?.name || "No name provided",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
deletedItem.onCallDutyPolicy?.name || "No name provided",
|
||||
schedule_name: scheduleName,
|
||||
on_call_context: `schedule ${scheduleName}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: deletedItem!.projectId!,
|
||||
@@ -324,8 +356,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCal
|
||||
import { Gray500, Red500 } from "../../Types/BrandColors";
|
||||
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import Team from "../../Models/DatabaseModels/Team";
|
||||
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
@@ -134,6 +136,20 @@ export class Service extends DatabaseService<Model> {
|
||||
policyName: createdModel.onCallDutyPolicy?.name || "No name provided",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
createdModel.onCallDutyPolicy?.name || "No name provided",
|
||||
on_call_context: `team ${temaName}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: createdModel!.projectId!,
|
||||
@@ -141,8 +157,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
|
||||
// add start log
|
||||
@@ -322,6 +338,20 @@ export class Service extends DatabaseService<Model> {
|
||||
deletedItem.onCallDutyPolicy?.name || "No name provided",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
deletedItem.onCallDutyPolicy?.name || "No name provided",
|
||||
on_call_context: `team ${teamName}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: deletedItem!.projectId!,
|
||||
@@ -329,8 +359,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
|
||||
// end time log
|
||||
|
||||
@@ -22,7 +22,9 @@ import PushNotificationMessage from "../../Types/PushNotification/PushNotificati
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import logger from "../Utils/Logger";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -117,6 +119,20 @@ export class Service extends DatabaseService<Model> {
|
||||
policyName: createdModel.onCallDutyPolicy?.name || "",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
createdModel.onCallDutyPolicy?.name || "No name provided",
|
||||
on_call_context: `escalation rule ${createdModel.onCallDutyPolicyEscalationRule?.name || "No name provided"}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: createdModel!.projectId!,
|
||||
@@ -124,8 +140,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
onCallPolicyId: createdModel.onCallDutyPolicy!.id!,
|
||||
onCallPolicyEscalationRuleId:
|
||||
createdModel.onCallDutyPolicyEscalationRule!.id!,
|
||||
@@ -322,6 +338,20 @@ export class Service extends DatabaseService<Model> {
|
||||
policyName: deletedItem.onCallDutyPolicy?.name || "",
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name:
|
||||
deletedItem.onCallDutyPolicy?.name || "No name provided",
|
||||
on_call_context: `escalation rule ${deletedItem.onCallDutyPolicyEscalationRule?.name || "No name provided"}`,
|
||||
policy_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: deletedItem!.projectId!,
|
||||
@@ -329,8 +359,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
onCallPolicyId: deletedItem.onCallDutyPolicy!.id!,
|
||||
onCallPolicyEscalationRuleId:
|
||||
deletedItem.onCallDutyPolicyEscalationRule!.id!,
|
||||
|
||||
@@ -36,6 +36,8 @@ import DeleteBy from "../Types/Database/DeleteBy";
|
||||
import { OnDelete } from "../Types/Database/Hooks";
|
||||
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
private layerUtil = new LayerUtil();
|
||||
@@ -265,6 +267,19 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
requireInteraction: false,
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_NO_LONGER_ACTIVE_ON_ON_CALL_ROSTER;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name: onCallPolicy.name!,
|
||||
schedule_name: onCallSchedule.name!,
|
||||
schedule_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: projectId,
|
||||
@@ -272,8 +287,8 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_NO_LONGER_ACTIVE_ON_ON_CALL_ROSTER,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
||||
onCallPolicyEscalationRuleId:
|
||||
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
||||
@@ -385,6 +400,19 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
requireInteraction: true,
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ON_CALL_ROSTER;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name: onCallPolicy.name!,
|
||||
schedule_name: onCallSchedule.name!,
|
||||
schedule_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: projectId,
|
||||
@@ -392,8 +420,8 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_ON_CALL_ROSTER,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
||||
onCallPolicyEscalationRuleId:
|
||||
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
||||
@@ -525,6 +553,19 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
requireInteraction: false,
|
||||
});
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_NEXT_ON_CALL_ROSTER;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
on_call_policy_name: onCallPolicy.name!,
|
||||
schedule_name: onCallSchedule.name!,
|
||||
schedule_link: vars["onCallPolicyViewLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: sendEmailToUserId,
|
||||
projectId: projectId,
|
||||
@@ -532,8 +573,8 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_WHEN_USER_IS_NEXT_ON_CALL_ROSTER,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
||||
onCallPolicyEscalationRuleId:
|
||||
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
||||
|
||||
@@ -33,6 +33,8 @@ import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import GlobalCache from "../Infrastructure/GlobalCache";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -387,6 +389,19 @@ export class Service extends DatabaseService<Model> {
|
||||
pushMessageParams,
|
||||
);
|
||||
|
||||
const eventType: NotificationSettingEventType =
|
||||
NotificationSettingEventType.SEND_PROBE_STATUS_CHANGED_OWNER_NOTIFICATION;
|
||||
|
||||
const whatsAppMessage: WhatsAppMessagePayload =
|
||||
createWhatsAppMessageFromTemplate({
|
||||
eventType,
|
||||
templateVariables: {
|
||||
probe_name: probe.name!,
|
||||
probe_status: connectionStatus,
|
||||
probe_link: vars["viewProbesLink"] || "",
|
||||
},
|
||||
});
|
||||
|
||||
await UserNotificationSettingService.sendUserNotification({
|
||||
userId: user.id!,
|
||||
projectId: probe.projectId!,
|
||||
@@ -394,8 +409,8 @@ export class Service extends DatabaseService<Model> {
|
||||
smsMessage: sms,
|
||||
callRequestMessage: callMessage,
|
||||
pushNotificationMessage: pushMessage,
|
||||
eventType:
|
||||
NotificationSettingEventType.SEND_PROBE_STATUS_CHANGED_OWNER_NOTIFICATION,
|
||||
whatsAppMessage,
|
||||
eventType,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error in sending incident created resource notification");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import DeleteBy from "../Types/Database/DeleteBy";
|
||||
import { OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
||||
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
||||
import UpdateBy from "../Types/Database/UpdateBy";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
||||
@@ -7,8 +8,10 @@ import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Model from "../../Models/DatabaseModels/Team";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
||||
import TeamMemberService from "./TeamMemberService";
|
||||
import ProjectSCIMService from "./ProjectSCIMService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -48,6 +51,70 @@ export class Service extends DatabaseService<Model> {
|
||||
return teams;
|
||||
}
|
||||
|
||||
private async assertScimAllowsTeamMutation(data: {
|
||||
projectIds: Array<ObjectID>;
|
||||
action: "create" | "delete";
|
||||
}): Promise<void> {
|
||||
if (!data.projectIds || data.projectIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueProjectIds: Map<string, ObjectID> = new Map();
|
||||
|
||||
for (const projectId of data.projectIds) {
|
||||
if (projectId) {
|
||||
uniqueProjectIds.set(projectId.toString(), new ObjectID(projectId));
|
||||
}
|
||||
}
|
||||
|
||||
for (const projectId of uniqueProjectIds.values()) {
|
||||
const scimCount: PositiveNumber = await ProjectSCIMService.countBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
},
|
||||
skip: new PositiveNumber(0),
|
||||
limit: new PositiveNumber(1),
|
||||
props: {
|
||||
isRoot: true,
|
||||
tenantId: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (scimCount.toNumber() > 0) {
|
||||
throw new BadDataException(
|
||||
`Cannot ${data.action} teams when SCIM is enabled for this project.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>,
|
||||
): Promise<OnCreate<Model>> {
|
||||
let projectId: ObjectID | undefined = createBy.data.projectId;
|
||||
|
||||
if (!projectId && createBy.props.tenantId) {
|
||||
projectId = new ObjectID(createBy.props.tenantId);
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadDataException("Project ID cannot be null");
|
||||
}
|
||||
|
||||
projectId = new ObjectID(projectId);
|
||||
createBy.data.projectId = projectId;
|
||||
|
||||
if (!createBy.props.isRoot) {
|
||||
await this.assertScimAllowsTeamMutation({
|
||||
projectIds: [projectId],
|
||||
action: "create",
|
||||
});
|
||||
}
|
||||
|
||||
return { createBy, carryForward: null };
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeUpdate(
|
||||
updateBy: UpdateBy<Model>,
|
||||
@@ -90,11 +157,27 @@ export class Service extends DatabaseService<Model> {
|
||||
select: {
|
||||
name: true,
|
||||
isTeamDeleteable: true,
|
||||
projectId: true,
|
||||
},
|
||||
|
||||
props: deleteBy.props,
|
||||
});
|
||||
|
||||
const projectIds: Array<ObjectID> = teams
|
||||
.map((team: Model) => {
|
||||
return team.projectId;
|
||||
})
|
||||
.filter((projectId: ObjectID | undefined): projectId is ObjectID => {
|
||||
return Boolean(projectId);
|
||||
});
|
||||
|
||||
if (deleteBy.props.isRoot !== true) {
|
||||
await this.assertScimAllowsTeamMutation({
|
||||
projectIds: projectIds,
|
||||
action: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
for (const team of teams) {
|
||||
if (!team.isTeamDeleteable) {
|
||||
throw new BadDataException(
|
||||
|
||||
@@ -9,6 +9,7 @@ import IncidentSeverityService from "./IncidentSeverityService";
|
||||
import MailService from "./MailService";
|
||||
import ShortLinkService from "./ShortLinkService";
|
||||
import SmsService from "./SmsService";
|
||||
import WhatsAppService from "./WhatsAppService";
|
||||
import UserEmailService from "./UserEmailService";
|
||||
import UserOnCallLogService from "./UserOnCallLogService";
|
||||
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
||||
@@ -29,6 +30,13 @@ import NotificationRuleType from "../../Types/NotificationRule/NotificationRuleT
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Phone from "../../Types/Phone";
|
||||
import SMS from "../../Types/SMS/SMS";
|
||||
import WhatsAppMessage from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import {
|
||||
renderWhatsAppTemplate,
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
WhatsAppTemplateId,
|
||||
} from "../../Types/WhatsApp/WhatsAppTemplates";
|
||||
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
|
||||
import UserNotificationExecutionStatus from "../../Types/UserNotification/UserNotificationExecutionStatus";
|
||||
import UserNotificationStatus from "../../Types/UserNotification/UserNotificationStatus";
|
||||
@@ -135,6 +143,10 @@ export class Service extends DatabaseService<Model> {
|
||||
phone: true,
|
||||
isVerified: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
isVerified: true,
|
||||
},
|
||||
userEmail: {
|
||||
email: true,
|
||||
isVerified: true,
|
||||
@@ -224,6 +236,7 @@ export class Service extends DatabaseService<Model> {
|
||||
name: true,
|
||||
},
|
||||
rootCause: true,
|
||||
incidentNumber: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -252,6 +265,7 @@ export class Service extends DatabaseService<Model> {
|
||||
alertSeverity: {
|
||||
name: true,
|
||||
},
|
||||
alertNumber: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -516,6 +530,125 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
notificationRuleItem.userWhatsApp?.phone &&
|
||||
notificationRuleItem.userWhatsApp?.isVerified
|
||||
) {
|
||||
if (
|
||||
options.userNotificationEventType ===
|
||||
UserNotificationEventType.AlertCreated &&
|
||||
alert
|
||||
) {
|
||||
logTimelineItem.status = UserNotificationStatus.Sending;
|
||||
logTimelineItem.statusMessage = `Sending WhatsApp message to ${notificationRuleItem.userWhatsApp?.phone.toString()}.`;
|
||||
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id!;
|
||||
|
||||
const updatedLog: UserOnCallLogTimeline =
|
||||
await UserOnCallLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const whatsAppMessage: WhatsAppMessage =
|
||||
await this.generateWhatsAppTemplateForAlertCreated(
|
||||
notificationRuleItem.userWhatsApp.phone,
|
||||
alert,
|
||||
updatedLog.id!,
|
||||
);
|
||||
|
||||
WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
|
||||
projectId: alert.projectId,
|
||||
alertId: alert.id!,
|
||||
userOnCallLogTimelineId: updatedLog.id!,
|
||||
userId: notificationRuleItem.userId!,
|
||||
onCallPolicyId: options.onCallPolicyId,
|
||||
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
||||
teamId: options.userBelongsToTeamId,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
options.onCallDutyPolicyExecutionLogTimelineId,
|
||||
onCallScheduleId: options.onCallScheduleId,
|
||||
}).catch(async (err: Error) => {
|
||||
await UserOnCallLogTimelineService.updateOneById({
|
||||
id: updatedLog.id!,
|
||||
data: {
|
||||
status: UserNotificationStatus.Error,
|
||||
statusMessage: err.message || "Error sending WhatsApp message.",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
options.userNotificationEventType ===
|
||||
UserNotificationEventType.IncidentCreated &&
|
||||
incident
|
||||
) {
|
||||
logTimelineItem.status = UserNotificationStatus.Sending;
|
||||
logTimelineItem.statusMessage = `Sending WhatsApp message to ${notificationRuleItem.userWhatsApp?.phone.toString()}.`;
|
||||
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id!;
|
||||
|
||||
const updatedLog: UserOnCallLogTimeline =
|
||||
await UserOnCallLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const whatsAppMessage: WhatsAppMessage =
|
||||
await this.generateWhatsAppTemplateForIncidentCreated(
|
||||
notificationRuleItem.userWhatsApp.phone,
|
||||
incident,
|
||||
updatedLog.id!,
|
||||
);
|
||||
|
||||
WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
|
||||
projectId: incident.projectId,
|
||||
incidentId: incident.id!,
|
||||
userOnCallLogTimelineId: updatedLog.id!,
|
||||
userId: notificationRuleItem.userId!,
|
||||
onCallPolicyId: options.onCallPolicyId,
|
||||
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
||||
teamId: options.userBelongsToTeamId,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
options.onCallDutyPolicyExecutionLogTimelineId,
|
||||
onCallScheduleId: options.onCallScheduleId,
|
||||
}).catch(async (err: Error) => {
|
||||
await UserOnCallLogTimelineService.updateOneById({
|
||||
id: updatedLog.id!,
|
||||
data: {
|
||||
status: UserNotificationStatus.Error,
|
||||
statusMessage: err.message || "Error sending WhatsApp message.",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
notificationRuleItem.userWhatsApp?.phone &&
|
||||
!notificationRuleItem.userWhatsApp?.isVerified
|
||||
) {
|
||||
logTimelineItem.status = UserNotificationStatus.Error;
|
||||
logTimelineItem.statusMessage = `WhatsApp message not sent because phone ${notificationRuleItem.userWhatsApp?.phone.toString()} is not verified.`;
|
||||
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id!;
|
||||
|
||||
await UserOnCallLogTimelineService.create({
|
||||
data: logTimelineItem,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// send call.
|
||||
if (
|
||||
notificationRuleItem.userCall?.phone &&
|
||||
@@ -925,16 +1058,19 @@ export class Service extends DatabaseService<Model> {
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + userOnCallLogTimelineId.toString()),
|
||||
.addRoute("/acknowledge-page/" + userOnCallLogTimelineId.toString()),
|
||||
),
|
||||
);
|
||||
const url: URL = await ShortLinkService.getShortenedUrl(shortUrl);
|
||||
|
||||
const alertIdentifier: string =
|
||||
alert.alertNumber !== undefined
|
||||
? `#${alert.alertNumber} (${alert.title || "Alert"})`
|
||||
: alert.title || "Alert";
|
||||
|
||||
const sms: SMS = {
|
||||
to,
|
||||
message: `This is a message from OneUptime. A new alert has been created. ${
|
||||
alert.title
|
||||
}. To acknowledge this alert, please click on the following link ${url.toString()}`,
|
||||
message: `This is a message from OneUptime. A new alert has been created: ${alertIdentifier}. To acknowledge this alert, please click on the following link ${url.toString()}`,
|
||||
};
|
||||
|
||||
return sms;
|
||||
@@ -955,21 +1091,138 @@ export class Service extends DatabaseService<Model> {
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute("/acknowledge/" + userOnCallLogTimelineId.toString()),
|
||||
.addRoute("/acknowledge-page/" + userOnCallLogTimelineId.toString()),
|
||||
),
|
||||
);
|
||||
const url: URL = await ShortLinkService.getShortenedUrl(shortUrl);
|
||||
|
||||
const incidentIdentifier: string =
|
||||
incident.incidentNumber !== undefined
|
||||
? `#${incident.incidentNumber} (${incident.title || "Incident"})`
|
||||
: incident.title || "Incident";
|
||||
|
||||
const sms: SMS = {
|
||||
to,
|
||||
message: `This is a message from OneUptime. A new incident has been created. ${
|
||||
incident.title
|
||||
}. To acknowledge this incident, please click on the following link ${url.toString()}`,
|
||||
message: `This is a message from OneUptime. A new incident has been created: ${incidentIdentifier}. To acknowledge this incident, please click on the following link ${url.toString()}`,
|
||||
};
|
||||
|
||||
return sms;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generateWhatsAppTemplateForAlertCreated(
|
||||
to: Phone,
|
||||
alert: Alert,
|
||||
userOnCallLogTimelineId: ObjectID,
|
||||
): Promise<WhatsAppMessage> {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
const acknowledgeShortLink: ShortLink =
|
||||
await ShortLinkService.saveShortLinkFor(
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute(
|
||||
"/acknowledge-page/" + userOnCallLogTimelineId.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const acknowledgeUrl: URL =
|
||||
await ShortLinkService.getShortenedUrl(acknowledgeShortLink);
|
||||
|
||||
const alertLinkOnDashboard: string =
|
||||
alert.projectId && alert.id
|
||||
? (
|
||||
await AlertService.getAlertLinkInDashboard(
|
||||
alert.projectId,
|
||||
alert.id,
|
||||
)
|
||||
).toString()
|
||||
: acknowledgeUrl.toString();
|
||||
|
||||
const templateKey: WhatsAppTemplateId = WhatsAppTemplateIds.AlertCreated;
|
||||
const templateVariables: Record<string, string> = {
|
||||
project_name: alert.project?.name || "OneUptime",
|
||||
alert_title: alert.title || "",
|
||||
acknowledge_url: acknowledgeUrl.toString(),
|
||||
alert_number:
|
||||
alert.alertNumber !== undefined ? alert.alertNumber.toString() : "",
|
||||
alert_link: alertLinkOnDashboard,
|
||||
};
|
||||
|
||||
const body: string = renderWhatsAppTemplate(templateKey, templateVariables);
|
||||
|
||||
return {
|
||||
to,
|
||||
body,
|
||||
templateKey,
|
||||
templateVariables,
|
||||
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generateWhatsAppTemplateForIncidentCreated(
|
||||
to: Phone,
|
||||
incident: Incident,
|
||||
userOnCallLogTimelineId: ObjectID,
|
||||
): Promise<WhatsAppMessage> {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
const acknowledgeShortLink: ShortLink =
|
||||
await ShortLinkService.saveShortLinkFor(
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AppApiRoute.toString())
|
||||
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
|
||||
.addRoute(
|
||||
"/acknowledge-page/" + userOnCallLogTimelineId.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const acknowledgeUrl: URL =
|
||||
await ShortLinkService.getShortenedUrl(acknowledgeShortLink);
|
||||
|
||||
const incidentLinkOnDashboard: string =
|
||||
incident.projectId && incident.id
|
||||
? (
|
||||
await IncidentService.getIncidentLinkInDashboard(
|
||||
incident.projectId,
|
||||
incident.id,
|
||||
)
|
||||
).toString()
|
||||
: acknowledgeUrl.toString();
|
||||
|
||||
const templateKey: WhatsAppTemplateId = WhatsAppTemplateIds.IncidentCreated;
|
||||
const templateVariables: Record<string, string> = {
|
||||
project_name: incident.project?.name || "OneUptime",
|
||||
incident_title: incident.title || "",
|
||||
acknowledge_url: acknowledgeUrl.toString(),
|
||||
incident_number:
|
||||
incident.incidentNumber !== undefined
|
||||
? incident.incidentNumber.toString()
|
||||
: "",
|
||||
incident_link: incidentLinkOnDashboard,
|
||||
};
|
||||
|
||||
const body: string = renderWhatsAppTemplate(templateKey, templateVariables);
|
||||
|
||||
return {
|
||||
to,
|
||||
body,
|
||||
templateKey,
|
||||
templateVariables,
|
||||
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async generateEmailTemplateForAlertCreated(
|
||||
to: Email,
|
||||
@@ -1210,12 +1463,14 @@ export class Service extends DatabaseService<Model> {
|
||||
!createBy.data.userEmail &&
|
||||
!createBy.data.userSms &&
|
||||
!createBy.data.userSmsId &&
|
||||
!createBy.data.userWhatsApp &&
|
||||
!createBy.data.userWhatsAppId &&
|
||||
!createBy.data.userEmailId &&
|
||||
!createBy.data.userPushId &&
|
||||
!createBy.data.userPush
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Call, SMS, Email, or Push notification is required",
|
||||
"Call, SMS, WhatsApp, Email, or Push notification is required",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import UserCallService from "./UserCallService";
|
||||
import UserEmailService from "./UserEmailService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
import PushNotificationService from "./PushNotificationService";
|
||||
import UserWhatsAppService from "./UserWhatsAppService";
|
||||
import WhatsAppService from "./WhatsAppService";
|
||||
import { CallRequestMessage } from "../../Types/Call/CallRequest";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import { EmailEnvelope } from "../../Types/Email/EmailMessage";
|
||||
@@ -19,11 +21,16 @@ import ObjectID from "../../Types/ObjectID";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import { SMSMessage } from "../../Types/SMS/SMS";
|
||||
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
||||
import WhatsAppMessage, {
|
||||
WhatsAppMessagePayload,
|
||||
} from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import UserCall from "../../Models/DatabaseModels/UserCall";
|
||||
import UserEmail from "../../Models/DatabaseModels/UserEmail";
|
||||
import UserNotificationSetting from "../../Models/DatabaseModels/UserNotificationSetting";
|
||||
import UserSMS from "../../Models/DatabaseModels/UserSMS";
|
||||
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import { appendRecipientToWhatsAppMessage } from "../Utils/WhatsAppTemplateUtil";
|
||||
|
||||
export class Service extends DatabaseService<UserNotificationSetting> {
|
||||
public constructor() {
|
||||
@@ -39,6 +46,7 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
||||
smsMessage: SMSMessage;
|
||||
callRequestMessage: CallRequestMessage;
|
||||
pushNotificationMessage: PushNotificationMessage;
|
||||
whatsAppMessage: WhatsAppMessagePayload;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
@@ -67,6 +75,7 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
||||
select: {
|
||||
alertByEmail: true,
|
||||
alertBySMS: true,
|
||||
alertByWhatsApp: true,
|
||||
alertByCall: true,
|
||||
alertByPush: true,
|
||||
},
|
||||
@@ -167,6 +176,57 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationSettings.alertByWhatsApp) {
|
||||
const userWhatsApps: Array<UserWhatsApp> =
|
||||
await UserWhatsAppService.findBy({
|
||||
query: {
|
||||
userId: data.userId,
|
||||
projectId: data.projectId,
|
||||
isVerified: true,
|
||||
},
|
||||
select: {
|
||||
phone: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.whatsAppMessage) {
|
||||
logger.warn(
|
||||
"Skipping WhatsApp notification because WhatsApp template payload is missing.",
|
||||
);
|
||||
} else {
|
||||
for (const userWhatsApp of userWhatsApps) {
|
||||
const whatsAppMessage: WhatsAppMessage =
|
||||
appendRecipientToWhatsAppMessage(
|
||||
data.whatsAppMessage,
|
||||
userWhatsApp.phone!,
|
||||
);
|
||||
|
||||
WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
|
||||
projectId: data.projectId,
|
||||
incidentId: data.incidentId,
|
||||
alertId: data.alertId,
|
||||
scheduledMaintenanceId: data.scheduledMaintenanceId,
|
||||
statusPageId: data.statusPageId,
|
||||
statusPageAnnouncementId: data.statusPageAnnouncementId,
|
||||
userId: data.userId,
|
||||
teamId: data.teamId,
|
||||
onCallPolicyId: data.onCallPolicyId,
|
||||
onCallPolicyEscalationRuleId: data.onCallPolicyEscalationRuleId,
|
||||
onCallDutyPolicyExecutionLogTimelineId:
|
||||
data.onCallDutyPolicyExecutionLogTimelineId,
|
||||
onCallScheduleId: data.onCallScheduleId,
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationSettings.alertByCall) {
|
||||
const userCalls: Array<UserCall> = await UserCallService.findBy({
|
||||
query: {
|
||||
|
||||
203
Common/Server/Services/UserWhatsAppService.ts
Normal file
203
Common/Server/Services/UserWhatsAppService.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import DeleteBy from "../Types/Database/DeleteBy";
|
||||
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
||||
import logger from "../Utils/Logger";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import ProjectService from "./ProjectService";
|
||||
import UserNotificationRuleService from "./UserNotificationRuleService";
|
||||
import WhatsAppService from "./WhatsAppService";
|
||||
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Text from "../../Types/Text";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import Model from "../../Models/DatabaseModels/UserWhatsApp";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import WhatsAppMessage from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import {
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateLanguage,
|
||||
WhatsAppTemplateId,
|
||||
} from "../../Types/WhatsApp/WhatsAppTemplates";
|
||||
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../Types/API/HTTPResponse";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeDelete(
|
||||
deleteBy: DeleteBy<Model>,
|
||||
): Promise<OnDelete<Model>> {
|
||||
const itemsToDelete: Array<Model> = await this.findBy({
|
||||
query: deleteBy.query,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_MAX,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
await UserNotificationRuleService.deleteBy({
|
||||
query: {
|
||||
userWhatsAppId: item.id!,
|
||||
projectId: item.projectId!,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>,
|
||||
): Promise<OnCreate<Model>> {
|
||||
if (!createBy.props.isRoot && createBy.data.isVerified) {
|
||||
throw new BadDataException("isVerified cannot be set to true");
|
||||
}
|
||||
|
||||
const project: Project | null = await ProjectService.findOneById({
|
||||
id: createBy.data.projectId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
enableWhatsAppNotifications: true,
|
||||
smsOrCallCurrentBalanceInUSDCents: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BadDataException("Project not found");
|
||||
}
|
||||
|
||||
if (!project.enableWhatsAppNotifications) {
|
||||
throw new BadDataException(
|
||||
"WhatsApp notifications are disabled for this project. Please enable them in Project Settings > Notification Settings.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(project.smsOrCallCurrentBalanceInUSDCents as number) <= 100 &&
|
||||
IsBillingEnabled
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Your WhatsApp balance is low. Please recharge your balance in Project Settings > Notification Settings.",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
createBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onCreateSuccess(
|
||||
_onCreate: OnCreate<Model>,
|
||||
createdItem: Model,
|
||||
): Promise<Model> {
|
||||
if (!createdItem.isVerified) {
|
||||
this.sendVerificationCode(createdItem).catch((error: Error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async resendVerificationCode(itemId: ObjectID): Promise<void> {
|
||||
const item: Model | null = await this.findOneById({
|
||||
id: itemId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
phone: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new BadDataException(
|
||||
"Item with ID " + itemId.toString() + " not found",
|
||||
);
|
||||
}
|
||||
|
||||
if (item.isVerified) {
|
||||
throw new BadDataException("WhatsApp number already verified");
|
||||
}
|
||||
|
||||
item.verificationCode = Text.generateRandomNumber(6);
|
||||
|
||||
await this.updateOneById({
|
||||
id: item.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
data: {
|
||||
verificationCode: item.verificationCode,
|
||||
},
|
||||
});
|
||||
|
||||
await this.sendVerificationCode(item);
|
||||
}
|
||||
|
||||
public async sendVerificationCode(item: Model): Promise<void> {
|
||||
if (!item.projectId || !item.userId || !item.phone) {
|
||||
logger.warn("Cannot send WhatsApp verification code. Missing data.");
|
||||
throw new BadDataException(
|
||||
"Unable to send WhatsApp verification code. Please remove this number and add it again.",
|
||||
);
|
||||
}
|
||||
|
||||
const templateKey: WhatsAppTemplateId =
|
||||
WhatsAppTemplateIds.VerificationCode;
|
||||
const templateVariables: Record<string, string> = {
|
||||
"1": item.verificationCode || "",
|
||||
};
|
||||
|
||||
const whatsAppMessage: WhatsAppMessage = {
|
||||
to: item.phone,
|
||||
body: "",
|
||||
templateKey,
|
||||
templateVariables,
|
||||
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
|
||||
};
|
||||
|
||||
const response: HTTPResponse<JSONObject> =
|
||||
await WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
|
||||
projectId: item.projectId,
|
||||
isSensitive: true,
|
||||
userId: item.userId,
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
15
Common/Server/Services/WhatsAppLogService.ts
Normal file
15
Common/Server/Services/WhatsAppLogService.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/WhatsAppLog";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
this.hardDeleteItemsOlderThanInDays("createdAt", 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
141
Common/Server/Services/WhatsAppService.ts
Normal file
141
Common/Server/Services/WhatsAppService.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { AppApiHostname } from "../EnvironmentConfig";
|
||||
import ClusterKeyAuthorization from "../Middleware/ClusterKeyAuthorization";
|
||||
import BaseService from "./BaseService";
|
||||
import EmptyResponseData from "../../Types/API/EmptyResponse";
|
||||
import HTTPResponse from "../../Types/API/HTTPResponse";
|
||||
import Protocol from "../../Types/API/Protocol";
|
||||
import Route from "../../Types/API/Route";
|
||||
import URL from "../../Types/API/URL";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WhatsAppMessage from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import API from "../../Utils/API";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
export class WhatsAppService extends BaseService {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async sendWhatsAppMessage(
|
||||
message: WhatsAppMessage,
|
||||
options: {
|
||||
projectId?: ObjectID | undefined;
|
||||
isSensitive?: boolean | undefined;
|
||||
userOnCallLogTimelineId?: ObjectID | undefined;
|
||||
incidentId?: ObjectID | undefined;
|
||||
alertId?: ObjectID | undefined;
|
||||
scheduledMaintenanceId?: ObjectID | undefined;
|
||||
statusPageId?: ObjectID | undefined;
|
||||
statusPageAnnouncementId?: ObjectID | undefined;
|
||||
userId?: ObjectID | undefined;
|
||||
onCallPolicyId?: ObjectID | undefined;
|
||||
onCallPolicyEscalationRuleId?: ObjectID | undefined;
|
||||
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
|
||||
onCallScheduleId?: ObjectID | undefined;
|
||||
teamId?: ObjectID | undefined;
|
||||
} = {},
|
||||
): Promise<HTTPResponse<EmptyResponseData>> {
|
||||
const body: JSONObject = {
|
||||
to: message.to.toString(),
|
||||
};
|
||||
|
||||
if (message.body) {
|
||||
body["body"] = message.body;
|
||||
}
|
||||
|
||||
if (message.templateKey) {
|
||||
body["templateKey"] = message.templateKey;
|
||||
}
|
||||
|
||||
if (message.templateVariables) {
|
||||
const templateVariables: JSONObject = {};
|
||||
|
||||
for (const [key, value] of Object.entries(message.templateVariables)) {
|
||||
templateVariables[key] = value;
|
||||
}
|
||||
|
||||
body["templateVariables"] = templateVariables;
|
||||
}
|
||||
|
||||
if (message.templateLanguageCode) {
|
||||
body["templateLanguageCode"] = message.templateLanguageCode;
|
||||
}
|
||||
|
||||
if (options.projectId) {
|
||||
body["projectId"] = options.projectId.toString();
|
||||
}
|
||||
|
||||
if (options.isSensitive !== undefined) {
|
||||
body["isSensitive"] = options.isSensitive;
|
||||
}
|
||||
|
||||
if (options.userOnCallLogTimelineId) {
|
||||
body["userOnCallLogTimelineId"] =
|
||||
options.userOnCallLogTimelineId.toString();
|
||||
}
|
||||
|
||||
if (options.incidentId) {
|
||||
body["incidentId"] = options.incidentId.toString();
|
||||
}
|
||||
|
||||
if (options.alertId) {
|
||||
body["alertId"] = options.alertId.toString();
|
||||
}
|
||||
|
||||
if (options.scheduledMaintenanceId) {
|
||||
body["scheduledMaintenanceId"] =
|
||||
options.scheduledMaintenanceId.toString();
|
||||
}
|
||||
|
||||
if (options.statusPageId) {
|
||||
body["statusPageId"] = options.statusPageId.toString();
|
||||
}
|
||||
|
||||
if (options.statusPageAnnouncementId) {
|
||||
body["statusPageAnnouncementId"] =
|
||||
options.statusPageAnnouncementId.toString();
|
||||
}
|
||||
|
||||
if (options.userId) {
|
||||
body["userId"] = options.userId.toString();
|
||||
}
|
||||
|
||||
if (options.onCallPolicyId) {
|
||||
body["onCallPolicyId"] = options.onCallPolicyId.toString();
|
||||
}
|
||||
|
||||
if (options.onCallPolicyEscalationRuleId) {
|
||||
body["onCallPolicyEscalationRuleId"] =
|
||||
options.onCallPolicyEscalationRuleId.toString();
|
||||
}
|
||||
|
||||
if (options.onCallDutyPolicyExecutionLogTimelineId) {
|
||||
body["onCallDutyPolicyExecutionLogTimelineId"] =
|
||||
options.onCallDutyPolicyExecutionLogTimelineId.toString();
|
||||
}
|
||||
|
||||
if (options.onCallScheduleId) {
|
||||
body["onCallScheduleId"] = options.onCallScheduleId.toString();
|
||||
}
|
||||
|
||||
if (options.teamId) {
|
||||
body["teamId"] = options.teamId.toString();
|
||||
}
|
||||
|
||||
return await API.post<EmptyResponseData>({
|
||||
url: new URL(
|
||||
Protocol.HTTP,
|
||||
AppApiHostname,
|
||||
new Route("/api/notification/whatsapp/send"),
|
||||
),
|
||||
data: body,
|
||||
headers: {
|
||||
...ClusterKeyAuthorization.getClusterKeyHeaders(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WhatsAppService();
|
||||
@@ -1,7 +1,11 @@
|
||||
import ClusterKeyAuthorization from "../../../../Middleware/ClusterKeyAuthorization";
|
||||
import DatabaseService from "../../../../Services/DatabaseService";
|
||||
import WorkflowService from "../../../../Services/WorkflowService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../../Utils/Express";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../../../../Utils/Express";
|
||||
import logger from "../../../../Utils/Logger";
|
||||
import Response from "../../../../Utils/Response";
|
||||
import Select from "../../../Database/Select";
|
||||
@@ -60,16 +64,24 @@ export default class OnTriggerBaseModel<
|
||||
props.router.get(
|
||||
`/model/:projectId/${this.modelId}/${this.type}`,
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
await this.initTrigger(req, res, props);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
props.router.post(
|
||||
`/model/:projectId/${this.modelId}/${this.type}`,
|
||||
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
await this.initTrigger(req, res, props);
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class WebhookTrigger extends TriggerCode {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -70,7 +70,7 @@ export default class WebhookTrigger extends TriggerCode {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import "./Process";
|
||||
import Response from "./Response";
|
||||
import { api } from "@opentelemetry/sdk-node";
|
||||
import StatusCode from "../../Types/API/StatusCode";
|
||||
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ServerException from "../../Types/Exception/ServerException";
|
||||
@@ -182,14 +183,15 @@ const init: InitFunction = async (
|
||||
|
||||
app.get(
|
||||
[`/${appName}/env.js`, "/env.js"],
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
// ping api server for database config.
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
// ping api server for database config.
|
||||
|
||||
const env: JSONObject = {
|
||||
...process.env,
|
||||
};
|
||||
const env: JSONObject = {
|
||||
...process.env,
|
||||
};
|
||||
|
||||
const script: string = `
|
||||
const script: string = `
|
||||
if(!window.process){
|
||||
window.process = {}
|
||||
}
|
||||
@@ -201,7 +203,10 @@ const init: InitFunction = async (
|
||||
window.process.env = JSON.parse(envVars);
|
||||
`;
|
||||
|
||||
Response.sendJavaScriptResponse(req, res, script);
|
||||
Response.sendJavaScriptResponse(req, res, script);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -216,32 +221,40 @@ const init: InitFunction = async (
|
||||
|
||||
app.get(
|
||||
["/*", `/${appName}/*`],
|
||||
async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
logger.debug("Rendering index page");
|
||||
async (
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
logger.debug("Rendering index page");
|
||||
|
||||
let variables: JSONObject = {};
|
||||
let variables: JSONObject = {};
|
||||
|
||||
if (data.getVariablesToRenderIndexPage) {
|
||||
logger.debug("Getting variables to render index page");
|
||||
try {
|
||||
const variablesToRenderIndexPage: JSONObject =
|
||||
await data.getVariablesToRenderIndexPage(_req, res);
|
||||
variables = {
|
||||
...variables,
|
||||
...variablesToRenderIndexPage,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (data.getVariablesToRenderIndexPage) {
|
||||
logger.debug("Getting variables to render index page");
|
||||
try {
|
||||
const variablesToRenderIndexPage: JSONObject =
|
||||
await data.getVariablesToRenderIndexPage(_req, res);
|
||||
variables = {
|
||||
...variables,
|
||||
...variablesToRenderIndexPage,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Rendering index page with variables: ");
|
||||
logger.debug(variables);
|
||||
|
||||
return res.render("/usr/src/app/views/index.ejs", {
|
||||
enableGoogleTagManager: IsBillingEnabled || false,
|
||||
...variables,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
logger.debug("Rendering index page with variables: ");
|
||||
logger.debug(variables);
|
||||
|
||||
return res.render("/usr/src/app/views/index.ejs", {
|
||||
enableGoogleTagManager: IsBillingEnabled || false,
|
||||
...variables,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -285,7 +298,7 @@ const addDefaultRoutes: PromiseVoidFunction = async (): Promise<void> => {
|
||||
// Attach Error Handler.
|
||||
app.use(
|
||||
(
|
||||
err: Error | Exception,
|
||||
err: Error | Exception | HTTPErrorResponse,
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
@@ -323,6 +336,19 @@ const addDefaultRoutes: PromiseVoidFunction = async (): Promise<void> => {
|
||||
res.send({ error: "Server Error" });
|
||||
}
|
||||
});
|
||||
} else if (err instanceof HTTPErrorResponse) {
|
||||
const errorStatusCode: number = StatusCode.isValidStatusCode(
|
||||
err.statusCode,
|
||||
)
|
||||
? err.statusCode
|
||||
: 500;
|
||||
|
||||
const payload: unknown = err.jsonData ?? {
|
||||
error: err.message || "Server Error",
|
||||
};
|
||||
|
||||
res.status(errorStatusCode);
|
||||
res.send(payload);
|
||||
} else if (err instanceof Exception) {
|
||||
res.status((err as Exception).code);
|
||||
res.send({ error: (err as Exception).message });
|
||||
|
||||
247
Common/Server/Utils/WhatsAppTemplateUtil.ts
Normal file
247
Common/Server/Utils/WhatsAppTemplateUtil.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import NotificationSettingEventType from "../../Types/NotificationSetting/NotificationSettingEventType";
|
||||
import WhatsAppTemplateMessages, {
|
||||
WhatsAppTemplateIds,
|
||||
WhatsAppTemplateId,
|
||||
WhatsAppTemplateLanguage,
|
||||
} from "../../Types/WhatsApp/WhatsAppTemplates";
|
||||
import WhatsAppMessage, {
|
||||
WhatsAppMessagePayload,
|
||||
} from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
|
||||
const DEFAULT_ACTION_LINK: string = "https://oneuptime.com/dashboard";
|
||||
|
||||
const templateDashboardLinkVariableMap: Partial<
|
||||
Record<WhatsAppTemplateId, string>
|
||||
> = {
|
||||
[WhatsAppTemplateIds.AlertCreated]: "alert_link",
|
||||
[WhatsAppTemplateIds.AlertCreatedOwnerNotification]: "alert_link",
|
||||
[WhatsAppTemplateIds.AlertNotePostedOwnerNotification]: "alert_link",
|
||||
[WhatsAppTemplateIds.AlertStateChangedOwnerNotification]: "alert_link",
|
||||
[WhatsAppTemplateIds.AlertOwnerAddedNotification]: "alert_link",
|
||||
[WhatsAppTemplateIds.IncidentCreated]: "incident_link",
|
||||
[WhatsAppTemplateIds.IncidentCreatedOwnerNotification]: "incident_link",
|
||||
[WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: "incident_link",
|
||||
[WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "incident_link",
|
||||
[WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "incident_link",
|
||||
[WhatsAppTemplateIds.MonitorOwnerAddedNotification]: "monitor_link",
|
||||
[WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: "monitor_link",
|
||||
[WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: "monitor_link",
|
||||
[WhatsAppTemplateIds.MonitorProbeStatusChangedNotification]: "monitor_link",
|
||||
[WhatsAppTemplateIds.MonitorNoProbesMonitoringNotification]: "monitor_link",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceCreatedOwnerNotification]:
|
||||
"maintenance_link",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceNotePostedOwnerNotification]:
|
||||
"maintenance_link",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification]:
|
||||
"maintenance_link",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification]:
|
||||
"maintenance_link",
|
||||
[WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification]:
|
||||
"status_page_link",
|
||||
[WhatsAppTemplateIds.StatusPageCreatedOwnerNotification]: "status_page_link",
|
||||
[WhatsAppTemplateIds.StatusPageOwnerAddedNotification]: "status_page_link",
|
||||
[WhatsAppTemplateIds.ProbeStatusChangedOwnerNotification]: "probe_link",
|
||||
[WhatsAppTemplateIds.ProbeOwnerAddedNotification]: "probe_link",
|
||||
[WhatsAppTemplateIds.OnCallUserIsOnRosterNotification]: "schedule_link",
|
||||
[WhatsAppTemplateIds.OnCallUserIsNextNotification]: "schedule_link",
|
||||
[WhatsAppTemplateIds.OnCallUserNoLongerActiveNotification]: "schedule_link",
|
||||
[WhatsAppTemplateIds.OnCallUserAddedToPolicyNotification]: "policy_link",
|
||||
[WhatsAppTemplateIds.OnCallUserRemovedFromPolicyNotification]: "policy_link",
|
||||
[WhatsAppTemplateIds.VerificationCode]: "dashboard_link",
|
||||
};
|
||||
|
||||
const templateIdByEventType: Record<
|
||||
NotificationSettingEventType,
|
||||
WhatsAppTemplateId
|
||||
> = {
|
||||
[NotificationSettingEventType.SEND_INCIDENT_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.IncidentCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_INCIDENT_NOTE_POSTED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.IncidentNotePostedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_INCIDENT_STATE_CHANGED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.IncidentStateChangedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_INCIDENT_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.IncidentOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_ALERT_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.AlertCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.AlertNotePostedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_ALERT_STATE_CHANGED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.AlertStateChangedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_ALERT_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.AlertOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_MONITOR_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.MonitorOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_MONITOR_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.MonitorCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_PORBE_STATUS_CHANGES]:
|
||||
WhatsAppTemplateIds.MonitorProbeStatusChangedNotification,
|
||||
[NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_NO_PROBES_ARE_MONITORING_THE_MONITOR]:
|
||||
WhatsAppTemplateIds.MonitorNoProbesMonitoringNotification,
|
||||
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ScheduledMaintenanceCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ScheduledMaintenanceNotePostedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_STATUS_PAGE_CREATED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.StatusPageCreatedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_STATUS_PAGE_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.StatusPageOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_PROBE_STATUS_CHANGED_OWNER_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ProbeStatusChangedOwnerNotification,
|
||||
[NotificationSettingEventType.SEND_PROBE_OWNER_ADDED_NOTIFICATION]:
|
||||
WhatsAppTemplateIds.ProbeOwnerAddedNotification,
|
||||
[NotificationSettingEventType.SEND_WHEN_USER_IS_ON_CALL_ROSTER]:
|
||||
WhatsAppTemplateIds.OnCallUserIsOnRosterNotification,
|
||||
[NotificationSettingEventType.SEND_WHEN_USER_IS_NEXT_ON_CALL_ROSTER]:
|
||||
WhatsAppTemplateIds.OnCallUserIsNextNotification,
|
||||
[NotificationSettingEventType.SEND_WHEN_USER_IS_ADDED_TO_ON_CALL_POLICY]:
|
||||
WhatsAppTemplateIds.OnCallUserAddedToPolicyNotification,
|
||||
[NotificationSettingEventType.SEND_WHEN_USER_IS_REMOVED_FROM_ON_CALL_POLICY]:
|
||||
WhatsAppTemplateIds.OnCallUserRemovedFromPolicyNotification,
|
||||
[NotificationSettingEventType.SEND_WHEN_USER_IS_NO_LONGER_ACTIVE_ON_ON_CALL_ROSTER]:
|
||||
WhatsAppTemplateIds.OnCallUserNoLongerActiveNotification,
|
||||
};
|
||||
|
||||
export function getWhatsAppTemplateIdForEventType(
|
||||
eventType: NotificationSettingEventType,
|
||||
): WhatsAppTemplateId {
|
||||
const templateId: WhatsAppTemplateId | undefined =
|
||||
templateIdByEventType[eventType];
|
||||
|
||||
if (!templateId) {
|
||||
throw new Error(
|
||||
`WhatsApp template is not defined for event type ${eventType}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return templateId;
|
||||
}
|
||||
|
||||
export function getWhatsAppTemplateStringForEventType(
|
||||
eventType: NotificationSettingEventType,
|
||||
): string {
|
||||
const templateId: WhatsAppTemplateId =
|
||||
getWhatsAppTemplateIdForEventType(eventType);
|
||||
|
||||
const templateContent: string | undefined =
|
||||
WhatsAppTemplateMessages[templateId];
|
||||
|
||||
if (!templateContent) {
|
||||
throw new Error(
|
||||
`WhatsApp template content is not defined for event type ${eventType}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return templateContent;
|
||||
}
|
||||
|
||||
function renderTemplateContent(
|
||||
templateContent: string,
|
||||
variables: Record<string, string>,
|
||||
context: string,
|
||||
): string {
|
||||
return templateContent.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
(_match: string, key: string) => {
|
||||
const value: string | undefined = variables[key];
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(
|
||||
`Missing variable "${key}" for WhatsApp template ${context}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function createWhatsAppMessageFromTemplate({
|
||||
templateString,
|
||||
actionLink,
|
||||
eventType,
|
||||
templateKey,
|
||||
templateVariables,
|
||||
}: {
|
||||
templateString?: string;
|
||||
actionLink?: string | undefined;
|
||||
eventType?: NotificationSettingEventType;
|
||||
templateKey?: WhatsAppTemplateId;
|
||||
templateVariables?: Record<string, string>;
|
||||
}): WhatsAppMessagePayload {
|
||||
const resolvedTemplateKey: WhatsAppTemplateId | undefined =
|
||||
templateKey ??
|
||||
(eventType ? getWhatsAppTemplateIdForEventType(eventType) : undefined);
|
||||
|
||||
if (!resolvedTemplateKey) {
|
||||
throw new Error(
|
||||
"WhatsApp template key or event type must be provided to create WhatsApp message.",
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedActionLink: string = (actionLink ?? DEFAULT_ACTION_LINK).trim();
|
||||
|
||||
const templateVariablesWithDefaults: Record<string, string> = {
|
||||
...(templateVariables ?? {}),
|
||||
};
|
||||
|
||||
const dashboardLinkVariableName: string | undefined =
|
||||
templateDashboardLinkVariableMap[resolvedTemplateKey];
|
||||
|
||||
if (dashboardLinkVariableName) {
|
||||
const providedLink: string | undefined =
|
||||
templateVariablesWithDefaults[dashboardLinkVariableName] ??
|
||||
templateVariables?.[dashboardLinkVariableName];
|
||||
|
||||
const finalLink: string = (providedLink || resolvedActionLink).trim();
|
||||
|
||||
templateVariablesWithDefaults[dashboardLinkVariableName] = finalLink;
|
||||
}
|
||||
|
||||
const resolvedTemplateContent: string | undefined =
|
||||
templateString ?? WhatsAppTemplateMessages[resolvedTemplateKey];
|
||||
|
||||
if (!resolvedTemplateContent) {
|
||||
throw new Error(
|
||||
`WhatsApp template content is not defined for template ${resolvedTemplateKey}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const body: string = renderTemplateContent(
|
||||
resolvedTemplateContent,
|
||||
templateVariablesWithDefaults,
|
||||
resolvedTemplateKey,
|
||||
);
|
||||
|
||||
return {
|
||||
body,
|
||||
templateKey: resolvedTemplateKey,
|
||||
templateVariables: templateVariablesWithDefaults,
|
||||
templateLanguageCode: WhatsAppTemplateLanguage[resolvedTemplateKey],
|
||||
};
|
||||
}
|
||||
|
||||
export function appendRecipientToWhatsAppMessage(
|
||||
payload: WhatsAppMessagePayload,
|
||||
to: WhatsAppMessage["to"],
|
||||
): WhatsAppMessage {
|
||||
return {
|
||||
...payload,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
createWhatsAppMessageFromTemplate,
|
||||
appendRecipientToWhatsAppMessage,
|
||||
getWhatsAppTemplateIdForEventType,
|
||||
getWhatsAppTemplateStringForEventType,
|
||||
};
|
||||
@@ -2378,29 +2378,23 @@ All monitoring checks are passing normally.`;
|
||||
logger.debug(`Channel data: ${JSON.stringify(channelData)}`);
|
||||
|
||||
// Check if the bot was added
|
||||
const recipientId: string | undefined =
|
||||
data.turnContext.activity.recipient?.id;
|
||||
|
||||
const botWasAdded: boolean = membersAdded.some((member: JSONObject) => {
|
||||
return member["id"] === MicrosoftTeamsAppClientId;
|
||||
return member["id"] === recipientId;
|
||||
});
|
||||
|
||||
if (botWasAdded) {
|
||||
logger.debug("OneUptime bot was added to a Teams conversation");
|
||||
|
||||
const welcomeText: string =
|
||||
"🎉 Welcome to OneUptime!\n\nI'm your monitoring and alerting assistant. I'll help you stay on top of your system's health and notify you about any incidents.\n\nType 'help' to see what I can do for you.";
|
||||
|
||||
try {
|
||||
// Send welcome message directly using TurnContext
|
||||
await data.turnContext.sendActivity(welcomeText);
|
||||
logger.debug("Welcome message sent successfully using TurnContext");
|
||||
} catch (error) {
|
||||
logger.error("Error sending welcome message via TurnContext: " + error);
|
||||
}
|
||||
await this.sendWelcomeAdaptiveCard(data.turnContext);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleInstallationUpdateActivity(data: {
|
||||
activity: JSONObject;
|
||||
turnContext: TurnContext;
|
||||
}): Promise<void> {
|
||||
// Handle bot installation/uninstallation
|
||||
const action: string = (data.activity["action"] as string) || "";
|
||||
@@ -2412,6 +2406,7 @@ All monitoring checks are passing normally.`;
|
||||
|
||||
if (action === "add") {
|
||||
logger.debug("OneUptime bot was installed");
|
||||
await this.sendWelcomeAdaptiveCard(data.turnContext);
|
||||
} else if (action === "remove") {
|
||||
logger.debug("OneUptime bot was uninstalled");
|
||||
}
|
||||
@@ -2482,6 +2477,20 @@ All monitoring checks are passing normally.`;
|
||||
await next();
|
||||
},
|
||||
);
|
||||
|
||||
this.onInstallationUpdateAdd(
|
||||
async (context: TurnContext, next: () => Promise<void>) => {
|
||||
logger.debug(
|
||||
"Handling installation update add activity: " +
|
||||
JSON.stringify(context.activity),
|
||||
);
|
||||
await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
|
||||
activity: context.activity as unknown as JSONObject,
|
||||
turnContext: context,
|
||||
});
|
||||
await next();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected override async onInvokeActivity(
|
||||
@@ -2528,6 +2537,132 @@ All monitoring checks are passing normally.`;
|
||||
}
|
||||
}
|
||||
|
||||
private static buildWelcomeAdaptiveCard(): JSONObject {
|
||||
return {
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
type: "AdaptiveCard",
|
||||
version: "1.4",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Welcome to OneUptime for Microsoft Teams",
|
||||
weight: "Bolder",
|
||||
size: "Large",
|
||||
wrap: true,
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "OneUptime keeps your team ahead of incidents by streaming alerts, maintenance updates, and on-call context directly into Microsoft Teams.",
|
||||
wrap: true,
|
||||
spacing: "Small",
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Getting started",
|
||||
weight: "Bolder",
|
||||
size: "Medium",
|
||||
spacing: "Large",
|
||||
wrap: true,
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "1. Connect this Teams workspace to your OneUptime project from **Settings → Integrations → Microsoft Teams**.\n2. Choose which incidents, alerts, and maintenance events should sync into Teams.\n3. Try the commands below or automate workflows from the OneUptime dashboard.",
|
||||
wrap: true,
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Bot commands",
|
||||
weight: "Bolder",
|
||||
size: "Medium",
|
||||
spacing: "Large",
|
||||
wrap: true,
|
||||
},
|
||||
{
|
||||
type: "FactSet",
|
||||
facts: [
|
||||
{
|
||||
title: "help",
|
||||
value: "Show quick help and useful links",
|
||||
},
|
||||
{
|
||||
title: "/incident",
|
||||
value: "Create a new incident without leaving Teams",
|
||||
},
|
||||
{
|
||||
title: "/maintenance",
|
||||
value: "Schedule or review maintenance windows",
|
||||
},
|
||||
{
|
||||
title: "show active incidents",
|
||||
value: "List all incidents that are currently open",
|
||||
},
|
||||
{
|
||||
title: "show scheduled maintenance",
|
||||
value: "Display upcoming maintenance events",
|
||||
},
|
||||
{
|
||||
title: "show active alerts",
|
||||
value: "Summarize active alerts for your project",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "To use this app, each user must have an active OneUptime account. Please contact our support team for more details.",
|
||||
wrap: true,
|
||||
spacing: "Large",
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Need more help?",
|
||||
weight: "Bolder",
|
||||
size: "Medium",
|
||||
spacing: "Large",
|
||||
wrap: true,
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Review our setup guide or reach out if you need assistance configuring notifications.",
|
||||
wrap: true,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Setup Guide",
|
||||
url: "https://oneuptime.com/docs/microsoft-teams",
|
||||
},
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "Contact Support",
|
||||
url: "mailto:support@oneuptime.com?subject=OneUptime%20Microsoft%20Teams%20Bot",
|
||||
},
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "Open OneUptime Dashboard",
|
||||
url: "https://oneuptime.com/dashboard",
|
||||
},
|
||||
],
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
private static async sendWelcomeAdaptiveCard(
|
||||
turnContext: TurnContext,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const welcomeCard: JSONObject = this.buildWelcomeAdaptiveCard();
|
||||
const message: Partial<Activity> = MessageFactory.attachment({
|
||||
contentType: "application/vnd.microsoft.card.adaptive",
|
||||
content: welcomeCard,
|
||||
});
|
||||
|
||||
await turnContext.sendActivity(message);
|
||||
logger.debug("Welcome adaptive card sent successfully");
|
||||
} catch (error) {
|
||||
logger.error("Error sending welcome adaptive card: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to refresh teams list for a user
|
||||
@CaptureSpan()
|
||||
public static async refreshTeams(data: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @jest-environment jsdom */
|
||||
// Ensure deterministic timezone for Date#getHours() etc.
|
||||
// This must be set before importing the component under test.
|
||||
/*
|
||||
* Ensure deterministic timezone for Date#getHours() etc.
|
||||
* This must be set before importing the component under test.
|
||||
*/
|
||||
// eslint-disable-next-line no-undef
|
||||
process.env.TZ = "UTC";
|
||||
import React from "react";
|
||||
@@ -8,65 +10,138 @@ import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom";
|
||||
import TimePicker from "../../../../UI/Components/TimePicker/TimePicker";
|
||||
import DateUtilities from "../../../../Types/Date";
|
||||
|
||||
type DateModule = typeof import("../../../../Types/Date");
|
||||
type DateLib = DateModule["default"];
|
||||
type MockedDateLib = jest.Mocked<DateLib>;
|
||||
type UserEventInstance = ReturnType<typeof userEvent.setup>;
|
||||
type ChangeHandler = jest.Mock<void, [string | undefined]>;
|
||||
type VoidHandler = jest.Mock<void, []>;
|
||||
type DialogElement = HTMLElement;
|
||||
type ButtonElement = HTMLButtonElement;
|
||||
type InputElement = HTMLInputElement;
|
||||
type HourMinuteMock = {
|
||||
getHours: () => number;
|
||||
getMinutes: () => number;
|
||||
};
|
||||
|
||||
// Mock OneUptimeDate utilities used by the component
|
||||
jest.mock("../../../../Types/Date", () => {
|
||||
const real = jest.requireActual("../../../../Types/Date");
|
||||
const real: DateModule = jest.requireActual("../../../../Types/Date");
|
||||
// Helper to create a minimal date-like object with getHours/getMinutes
|
||||
const makeHM = (h: number, m: number) => ({ getHours: () => h, getMinutes: () => m });
|
||||
const makeHM: (h: number, m: number) => HourMinuteMock = (
|
||||
h: number,
|
||||
m: number,
|
||||
): HourMinuteMock => {
|
||||
return {
|
||||
getHours: () => {
|
||||
return h;
|
||||
},
|
||||
getMinutes: () => {
|
||||
return m;
|
||||
},
|
||||
};
|
||||
};
|
||||
return {
|
||||
__esModule: true,
|
||||
default: {
|
||||
...real.default,
|
||||
getUserPrefers12HourFormat: jest.fn(() => false), // default to 24h; tests can override per test
|
||||
getCurrentDate: jest.fn(() => makeHM(13, 45) as any),
|
||||
getUserPrefers12HourFormat: jest.fn(() => {
|
||||
return false;
|
||||
}), // default to 24h; tests can override per test
|
||||
getCurrentDate: jest.fn(() => {
|
||||
return makeHM(13, 45) as unknown as Date;
|
||||
}),
|
||||
fromString: jest.fn((v: string | Date) => {
|
||||
if (!v) { return undefined as any; }
|
||||
if (!v) {
|
||||
return undefined as unknown as Date;
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const m = v.match(/T(\d{2}):(\d{2})/);
|
||||
const hh = m ? parseInt(m[1] as string, 10) : 0;
|
||||
const mm = m ? parseInt(m[2] as string, 10) : 0;
|
||||
return makeHM(hh, mm) as any;
|
||||
const match: RegExpMatchArray | null = v.match(/T(\d{2}):(\d{2})/);
|
||||
const hh: number = match ? parseInt(match[1] as string, 10) : 0;
|
||||
const mm: number = match ? parseInt(match[2] as string, 10) : 0;
|
||||
return makeHM(hh, mm) as unknown as Date;
|
||||
}
|
||||
// If a Date instance is provided, prefer UTC to avoid env timezone
|
||||
const d = v as Date;
|
||||
const hh = (d as any).getUTCHours ? (d as any).getUTCHours() : d.getHours();
|
||||
const mm = (d as any).getUTCMinutes ? (d as any).getUTCMinutes() : d.getMinutes();
|
||||
return makeHM(hh, mm) as any;
|
||||
const d: Date = v;
|
||||
const hasUtcHours: (() => number) | undefined = (
|
||||
d as { getUTCHours?: () => number }
|
||||
).getUTCHours;
|
||||
const hasUtcMinutes: (() => number) | undefined = (
|
||||
d as { getUTCMinutes?: () => number }
|
||||
).getUTCMinutes;
|
||||
const hh: number = hasUtcHours ? hasUtcHours.call(d) : d.getHours();
|
||||
const mm: number = hasUtcMinutes
|
||||
? hasUtcMinutes.call(d)
|
||||
: d.getMinutes();
|
||||
return makeHM(hh, mm) as unknown as Date;
|
||||
}),
|
||||
toString: jest.fn((d: Date) => d.toISOString()),
|
||||
getDateWithCustomTime: jest.fn(({ hours, minutes }: { hours: number; minutes: number; seconds?: number }) => {
|
||||
const base = new Date("2024-05-15T00:00:00.000Z");
|
||||
base.setUTCHours(hours, minutes, 0, 0);
|
||||
return base;
|
||||
toString: jest.fn((d: Date) => {
|
||||
return d.toISOString();
|
||||
}),
|
||||
getDateWithCustomTime: jest.fn(
|
||||
({
|
||||
hours,
|
||||
minutes,
|
||||
}: {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds?: number;
|
||||
}) => {
|
||||
const base: Date = new Date("2024-05-15T00:00:00.000Z");
|
||||
base.setUTCHours(hours, minutes, 0, 0);
|
||||
return base;
|
||||
},
|
||||
),
|
||||
getCurrentTimezoneString: jest.fn(() => {
|
||||
return "UTC";
|
||||
}),
|
||||
getCurrentTimezone: jest.fn(() => {
|
||||
return "Etc/UTC";
|
||||
}),
|
||||
getCurrentTimezoneString: jest.fn(() => "UTC"),
|
||||
getCurrentTimezone: jest.fn(() => "Etc/UTC"),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Icon to avoid SVG complexity
|
||||
jest.mock("../../../../UI/Components/Icon/Icon", () => ({
|
||||
__esModule: true,
|
||||
default: ({ className }: { className?: string }) => <i data-testid="icon" className={className} />,
|
||||
}));
|
||||
jest.mock("../../../../UI/Components/Icon/Icon", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ className }: { className?: string }) => {
|
||||
return <i data-testid="icon" className={className} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Modal to render children immediately and expose submit/close
|
||||
jest.mock("../../../../UI/Components/Modal/Modal", () => ({
|
||||
__esModule: true,
|
||||
default: ({ title, description, onClose, onSubmit, children, submitButtonText }: any) => (
|
||||
<div role="dialog" aria-label={title}>
|
||||
<div>{description}</div>
|
||||
<div>{children}</div>
|
||||
<button onClick={onSubmit}>{submitButtonText ?? "Apply"}</button>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
),
|
||||
ModalWidth: { Medium: "Medium" },
|
||||
}));
|
||||
jest.mock("../../../../UI/Components/Modal/Modal", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({
|
||||
title,
|
||||
description,
|
||||
onClose,
|
||||
onSubmit,
|
||||
children,
|
||||
submitButtonText,
|
||||
}: any) => {
|
||||
return (
|
||||
<div role="dialog" aria-label={title}>
|
||||
<div>{description}</div>
|
||||
<div>{children}</div>
|
||||
<button onClick={onSubmit}>{submitButtonText ?? "Apply"}</button>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
ModalWidth: { Medium: "Medium" },
|
||||
};
|
||||
});
|
||||
|
||||
const getDateLib = () => require("../../../../Types/Date").default;
|
||||
const getDateLib: () => MockedDateLib = () => {
|
||||
return DateUtilities as MockedDateLib;
|
||||
};
|
||||
|
||||
describe("TimePicker", () => {
|
||||
beforeEach(() => {
|
||||
@@ -75,7 +150,7 @@ describe("TimePicker", () => {
|
||||
});
|
||||
|
||||
it("renders in 24h by default and shows current time", () => {
|
||||
const onChange = jest.fn();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
||||
|
||||
// Should display HH:mm based on value prop
|
||||
@@ -83,162 +158,246 @@ describe("TimePicker", () => {
|
||||
expect(screen.getByLabelText("Minutes")).toHaveValue("05");
|
||||
|
||||
// AM/PM buttons are not shown in 24h
|
||||
expect(screen.queryByRole("button", { name: "AM" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "PM" })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "AM" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "PM" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens modal on click when enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
render(<TimePicker value="2024-05-15T10:20:00.000Z" />);
|
||||
|
||||
// Click the field container by clicking on hours input
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
|
||||
// Modal should appear
|
||||
expect(screen.getByRole("dialog", { name: "Select time" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("dialog", { name: "Select time" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/your UTC/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not open modal when readOnly or disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = render(<TimePicker value="2024-05-15T10:20:00.000Z" readOnly />);
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const { rerender } = render(
|
||||
<TimePicker value="2024-05-15T10:20:00.000Z" readOnly />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
expect(screen.queryByRole("dialog", { name: "Select time" })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Select time" }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
rerender(<TimePicker value="2024-05-15T10:20:00.000Z" disabled />);
|
||||
await user.click(screen.getByLabelText("Minutes"));
|
||||
expect(screen.queryByRole("dialog", { name: "Select time" })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("dialog", { name: "Select time" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies changes from modal and emits ISO via onChange (24h)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
||||
|
||||
// Open modal
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
// Increase hours and minutes using the chevrons
|
||||
const incHour = within(dialog).getByLabelText("Increase hours");
|
||||
const incMin = within(dialog).getByLabelText("Increase minutes");
|
||||
const incHour: ButtonElement = within(dialog).getByLabelText(
|
||||
"Increase hours",
|
||||
) as HTMLButtonElement;
|
||||
const incMin: ButtonElement = within(dialog).getByLabelText(
|
||||
"Increase minutes",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
await user.click(incHour); // 08 -> 09
|
||||
await user.click(incMin); // 05 -> 06
|
||||
|
||||
// Apply
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
// onChange should be called with ISO string
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
const emitted = onChange.mock.calls[0][0] as string;
|
||||
const emittedCall: [string | undefined] | undefined =
|
||||
onChange.mock.calls[0];
|
||||
expect(emittedCall).toBeDefined();
|
||||
const emitted: string = (emittedCall as [string])[0];
|
||||
expect(typeof emitted).toBe("string");
|
||||
|
||||
const lib = getDateLib();
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
// getDateWithCustomTime uses UTC hours in our mock; 9:06 maps to 09:06:00Z on the chosen date
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 9, minutes: 6, seconds: 0 });
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 9,
|
||||
minutes: 6,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("supports decrement wrapping for hours and minutes (24h)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T00:00:00.000Z" onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
const decHour = within(dialog).getByLabelText("Decrease hours");
|
||||
const decMin = within(dialog).getByLabelText("Decrease minutes");
|
||||
const decHour: ButtonElement = within(dialog).getByLabelText(
|
||||
"Decrease hours",
|
||||
) as HTMLButtonElement;
|
||||
const decMin: ButtonElement = within(dialog).getByLabelText(
|
||||
"Decrease minutes",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
// Minutes 00 -> 59 and hours 00 -> 23 when decreasing
|
||||
await user.click(decMin);
|
||||
await user.click(decHour);
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
const lib = getDateLib();
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
// dec minute first -> 00 -> 59, hours 0->23, then dec hour -> 22
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 22, minutes: 59, seconds: 0 });
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 22,
|
||||
minutes: 59,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders and operates in 12h mode with AM/PM toggles", async () => {
|
||||
const user = userEvent.setup();
|
||||
const lib = getDateLib();
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(true);
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
||||
|
||||
const onChange = jest.fn();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T13:45:00.000Z" onChange={onChange} />);
|
||||
|
||||
// Displays 01:45 PM
|
||||
expect(screen.getByLabelText("Hours")).toHaveValue("01");
|
||||
expect(screen.getByLabelText("Minutes")).toHaveValue("45");
|
||||
// Inline AM/PM buttons have aria-label overriding the name
|
||||
const apButtons = screen.getAllByRole("button", { name: "Open time selector for AM/PM" });
|
||||
expect(apButtons).toHaveLength(2);
|
||||
const apButtons: HTMLElement[] = screen.getAllByRole("button", {
|
||||
name: "Open time selector for AM/PM",
|
||||
});
|
||||
expect(apButtons).toHaveLength(2);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
// Modal description should reflect 12h mode
|
||||
expect(within(dialog).getByText(/choose hours, minutes, and AM\/PM/i)).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText(/choose hours, minutes, and AM\/PM/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Toggle to AM and change hour input to 12 to map to 00
|
||||
await user.click(within(dialog).getByRole("button", { name: /^AM$/ }));
|
||||
|
||||
const hourInput = within(dialog).getByLabelText("Hours");
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: /^AM$/ }) as HTMLButtonElement,
|
||||
);
|
||||
const hourInput: InputElement = within(dialog).getByLabelText(
|
||||
"Hours",
|
||||
) as InputElement;
|
||||
// Change to 12
|
||||
await user.clear(hourInput);
|
||||
await user.type(hourInput, "12");
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
// Should map to hours 0 in 24h
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 0, minutes: 45, seconds: 0 });
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 0,
|
||||
minutes: 45,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("AM/PM button mapping inside modal", async () => {
|
||||
const user = userEvent.setup();
|
||||
const lib = getDateLib();
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(true);
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
||||
|
||||
render(<TimePicker value="2024-05-15T01:10:00.000Z" />);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
// Click PM, should add 12 hours (1 -> 13)
|
||||
await user.click(within(dialog).getByRole("button", { name: /^PM$/ }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: /^PM$/ }) as HTMLButtonElement,
|
||||
);
|
||||
|
||||
// Increase minutes to 11 to ensure state changed
|
||||
await user.click(within(dialog).getByLabelText("Increase minutes"));
|
||||
await user.click(
|
||||
within(dialog).getByLabelText("Increase minutes") as HTMLButtonElement,
|
||||
);
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 13, minutes: 11, seconds: 0 });
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 13,
|
||||
minutes: 11,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("quick minutes buttons set minutes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "05" }));
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: "05" }) as HTMLButtonElement,
|
||||
);
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}) as HTMLButtonElement,
|
||||
);
|
||||
|
||||
const lib = getDateLib();
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 8, minutes: 5, seconds: 0 });
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 8,
|
||||
minutes: 5,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("respects placeholder in 24h and 12h modes", () => {
|
||||
const lib = getDateLib();
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(false);
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(false);
|
||||
const { unmount } = render(<TimePicker placeholder="HH" />);
|
||||
expect(screen.getByLabelText("Hours")).toHaveAttribute("placeholder", "HH");
|
||||
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(true);
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
||||
unmount();
|
||||
render(<TimePicker />);
|
||||
expect(screen.getByLabelText("Hours")).toHaveAttribute("placeholder", "hh");
|
||||
@@ -249,17 +408,21 @@ describe("TimePicker", () => {
|
||||
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("Required");
|
||||
// Error icon rendered
|
||||
expect(screen.getAllByTestId("icon").some(i => i.className?.includes("text-red-500"))).toBeTruthy();
|
||||
expect(
|
||||
screen.getAllByTestId("icon").some((iconEl: HTMLElement) => {
|
||||
return iconEl.className?.includes("text-red-500");
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls onFocus and onBlur from the hours input", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onFocus = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const onFocus: VoidHandler = jest.fn();
|
||||
const onBlur: VoidHandler = jest.fn();
|
||||
|
||||
render(<TimePicker onFocus={onFocus} onBlur={onBlur} />);
|
||||
|
||||
const hours = screen.getByLabelText("Hours");
|
||||
const hours: InputElement = screen.getByLabelText("Hours") as InputElement;
|
||||
await user.click(hours);
|
||||
expect(onFocus).toHaveBeenCalled();
|
||||
|
||||
@@ -269,10 +432,12 @@ describe("TimePicker", () => {
|
||||
|
||||
it("updates when value prop changes", () => {
|
||||
// Force 24h mode for this test to avoid bleed from prior tests
|
||||
const lib = getDateLib();
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(false);
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(false);
|
||||
|
||||
const { rerender } = render(<TimePicker value="2024-05-15T02:03:00.000Z" />);
|
||||
const { rerender } = render(
|
||||
<TimePicker value="2024-05-15T02:03:00.000Z" />,
|
||||
);
|
||||
expect(screen.getByLabelText("Hours")).toHaveValue("02");
|
||||
expect(screen.getByLabelText("Minutes")).toHaveValue("03");
|
||||
|
||||
@@ -282,52 +447,84 @@ describe("TimePicker", () => {
|
||||
});
|
||||
|
||||
it("clamps and maps hour text edits inside modal for 12h", async () => {
|
||||
const user = userEvent.setup();
|
||||
const lib = getDateLib();
|
||||
(lib.getUserPrefers12HourFormat as jest.Mock).mockReturnValue(true);
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
lib.getUserPrefers12HourFormat.mockReturnValue(true);
|
||||
|
||||
render(<TimePicker value="2024-05-15T12:00:00.000Z" />);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
const hourInput = within(dialog).getByLabelText("Hours");
|
||||
const hourInput: InputElement = within(dialog).getByLabelText(
|
||||
"Hours",
|
||||
) as InputElement;
|
||||
await user.clear(hourInput);
|
||||
await user.type(hourInput, "99"); // should clamp to 12 in 12h mode
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
// 12 PM stays 12 (i.e., 12 in 24h), with minutes from initial value 00
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 12, minutes: 0, seconds: 0 });
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 12,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("minute text edits clamp to 0-59", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
render(<TimePicker value="2024-05-15T10:10:00.000Z" />);
|
||||
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
|
||||
const minInput = within(dialog).getByLabelText("Minutes");
|
||||
const minInput: InputElement = within(dialog).getByLabelText(
|
||||
"Minutes",
|
||||
) as InputElement;
|
||||
await user.clear(minInput);
|
||||
await user.type(minInput, "99");
|
||||
|
||||
await user.click(within(dialog).getByRole("button", { name: "Apply" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Apply",
|
||||
}),
|
||||
);
|
||||
|
||||
const lib = getDateLib();
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({ hours: 10, minutes: 59, seconds: 0 });
|
||||
const lib: MockedDateLib = getDateLib();
|
||||
expect(lib.getDateWithCustomTime).toHaveBeenCalledWith({
|
||||
hours: 10,
|
||||
minutes: 59,
|
||||
seconds: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("modal Close does not emit change or update main display", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
const user: UserEventInstance = userEvent.setup();
|
||||
const onChange: ChangeHandler = jest.fn();
|
||||
render(<TimePicker value="2024-05-15T08:05:00.000Z" onChange={onChange} />);
|
||||
|
||||
// Open modal, change something, then close
|
||||
await user.click(screen.getByLabelText("Hours"));
|
||||
const dialog = screen.getByRole("dialog", { name: "Select time" });
|
||||
await user.click(within(dialog).getByLabelText("Increase hours"));
|
||||
await user.click(within(dialog).getByRole("button", { name: "Close" }));
|
||||
const dialog: DialogElement = screen.getByRole("dialog", {
|
||||
name: "Select time",
|
||||
});
|
||||
await user.click(
|
||||
within(dialog).getByLabelText("Increase hours") as HTMLButtonElement,
|
||||
);
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Close",
|
||||
}) as HTMLButtonElement,
|
||||
);
|
||||
|
||||
// No onChange called
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
@@ -137,6 +137,7 @@ enum IconProp {
|
||||
EyeSlash = "EyeSlash",
|
||||
SquareStack3D = "SquareStack3D",
|
||||
ExclaimationCircle = "ExclaimationCircle",
|
||||
WhatsApp = "WhatsApp",
|
||||
}
|
||||
|
||||
export default IconProp;
|
||||
|
||||
12
Common/Types/WhatsApp/WhatsAppMessage.ts
Normal file
12
Common/Types/WhatsApp/WhatsAppMessage.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import Phone from "../Phone";
|
||||
|
||||
export interface WhatsAppMessagePayload {
|
||||
body: string;
|
||||
templateKey?: string | undefined;
|
||||
templateVariables?: Record<string, string> | undefined;
|
||||
templateLanguageCode?: string | undefined;
|
||||
}
|
||||
|
||||
export default interface WhatsAppMessage extends WhatsAppMessagePayload {
|
||||
to: Phone;
|
||||
}
|
||||
197
Common/Types/WhatsApp/WhatsAppTemplates.ts
Normal file
197
Common/Types/WhatsApp/WhatsAppTemplates.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
type TemplateIdsMap = {
|
||||
readonly AlertCreated: "oneuptime_alert_created";
|
||||
readonly IncidentCreated: "oneuptime_incident_created";
|
||||
readonly VerificationCode: "oneuptime_verification_code";
|
||||
readonly TestNotification: "oneuptime_test_notification";
|
||||
readonly IncidentCreatedOwnerNotification: "oneuptime_incident_created_owner_notification";
|
||||
readonly IncidentNotePostedOwnerNotification: "oneuptime_incident_note_posted_owner_notification";
|
||||
readonly IncidentStateChangedOwnerNotification: "oneuptime_incident_state_change_owner_notification";
|
||||
readonly IncidentOwnerAddedNotification: "oneuptime_incident_owner_added_notification";
|
||||
readonly AlertCreatedOwnerNotification: "oneuptime_alert_created_owner_notification";
|
||||
readonly AlertNotePostedOwnerNotification: "oneuptime_alert_note_posted_owner_notification";
|
||||
readonly AlertStateChangedOwnerNotification: "oneuptime_alert_state_changed_owner_notification";
|
||||
readonly AlertOwnerAddedNotification: "oneuptime_alert_owner_added_notification";
|
||||
readonly MonitorOwnerAddedNotification: "oneuptime_monitor_owner_added_notification";
|
||||
readonly MonitorCreatedOwnerNotification: "oneuptime_monitor_created_owner_notification";
|
||||
readonly MonitorStatusChangedOwnerNotification: "oneuptime_monitor_status_changed_owner_notification";
|
||||
readonly MonitorProbeStatusChangedNotification: "oneuptime_monitor_probe_status_changed_notification";
|
||||
readonly MonitorNoProbesMonitoringNotification: "oneuptime_monitor_no_probes_monitoring_notification";
|
||||
readonly ScheduledMaintenanceCreatedOwnerNotification: "oneuptime_scheduled_maintenance_created_owner_notification";
|
||||
readonly ScheduledMaintenanceNotePostedOwnerNotification: "oneuptime_scheduled_maintenance_note_posted_owner_notification";
|
||||
readonly ScheduledMaintenanceOwnerAddedNotification: "oneuptime_scheduled_maintenance_owner_added_notification";
|
||||
readonly ScheduledMaintenanceStateChangedOwnerNotification: "oneuptime_scheduled_maintenance_state_changed_owner_notification";
|
||||
readonly StatusPageAnnouncementCreatedOwnerNotification: "oneuptime_status_page_announcement_created_owner_notification";
|
||||
readonly StatusPageCreatedOwnerNotification: "oneuptime_status_page_created_owner_notification";
|
||||
readonly StatusPageOwnerAddedNotification: "oneuptime_status_page_owner_added_notification";
|
||||
readonly ProbeStatusChangedOwnerNotification: "oneuptime_probe_status_changed_owner_notification";
|
||||
readonly ProbeOwnerAddedNotification: "oneuptime_probe_owner_added_notification";
|
||||
readonly OnCallUserIsOnRosterNotification: "oneuptime_oncall_user_is_on_roster_notification";
|
||||
readonly OnCallUserIsNextNotification: "oneuptime_oncall_user_is_next_notification";
|
||||
readonly OnCallUserAddedToPolicyNotification: "oneuptime_oncall_user_added_to_policy_notification";
|
||||
readonly OnCallUserRemovedFromPolicyNotification: "oneuptime_oncall_user_removed_from_policy_notification";
|
||||
readonly OnCallUserNoLongerActiveNotification: "oneuptime_oncall_user_no_longer_active_notification";
|
||||
};
|
||||
|
||||
const templateIds: TemplateIdsMap = {
|
||||
AlertCreated: "oneuptime_alert_created",
|
||||
IncidentCreated: "oneuptime_incident_created",
|
||||
VerificationCode: "oneuptime_verification_code",
|
||||
TestNotification: "oneuptime_test_notification",
|
||||
IncidentCreatedOwnerNotification:
|
||||
"oneuptime_incident_created_owner_notification",
|
||||
IncidentNotePostedOwnerNotification:
|
||||
"oneuptime_incident_note_posted_owner_notification",
|
||||
IncidentStateChangedOwnerNotification:
|
||||
"oneuptime_incident_state_change_owner_notification",
|
||||
IncidentOwnerAddedNotification: "oneuptime_incident_owner_added_notification",
|
||||
AlertCreatedOwnerNotification: "oneuptime_alert_created_owner_notification",
|
||||
AlertNotePostedOwnerNotification:
|
||||
"oneuptime_alert_note_posted_owner_notification",
|
||||
AlertStateChangedOwnerNotification:
|
||||
"oneuptime_alert_state_changed_owner_notification",
|
||||
AlertOwnerAddedNotification: "oneuptime_alert_owner_added_notification",
|
||||
MonitorOwnerAddedNotification: "oneuptime_monitor_owner_added_notification",
|
||||
MonitorCreatedOwnerNotification:
|
||||
"oneuptime_monitor_created_owner_notification",
|
||||
MonitorStatusChangedOwnerNotification:
|
||||
"oneuptime_monitor_status_changed_owner_notification",
|
||||
MonitorProbeStatusChangedNotification:
|
||||
"oneuptime_monitor_probe_status_changed_notification",
|
||||
MonitorNoProbesMonitoringNotification:
|
||||
"oneuptime_monitor_no_probes_monitoring_notification",
|
||||
ScheduledMaintenanceCreatedOwnerNotification:
|
||||
"oneuptime_scheduled_maintenance_created_owner_notification",
|
||||
ScheduledMaintenanceNotePostedOwnerNotification:
|
||||
"oneuptime_scheduled_maintenance_note_posted_owner_notification",
|
||||
ScheduledMaintenanceOwnerAddedNotification:
|
||||
"oneuptime_scheduled_maintenance_owner_added_notification",
|
||||
ScheduledMaintenanceStateChangedOwnerNotification:
|
||||
"oneuptime_scheduled_maintenance_state_changed_owner_notification",
|
||||
StatusPageAnnouncementCreatedOwnerNotification:
|
||||
"oneuptime_status_page_announcement_created_owner_notification",
|
||||
StatusPageCreatedOwnerNotification:
|
||||
"oneuptime_status_page_created_owner_notification",
|
||||
StatusPageOwnerAddedNotification:
|
||||
"oneuptime_status_page_owner_added_notification",
|
||||
ProbeStatusChangedOwnerNotification:
|
||||
"oneuptime_probe_status_changed_owner_notification",
|
||||
ProbeOwnerAddedNotification: "oneuptime_probe_owner_added_notification",
|
||||
OnCallUserIsOnRosterNotification:
|
||||
"oneuptime_oncall_user_is_on_roster_notification",
|
||||
OnCallUserIsNextNotification: "oneuptime_oncall_user_is_next_notification",
|
||||
OnCallUserAddedToPolicyNotification:
|
||||
"oneuptime_oncall_user_added_to_policy_notification",
|
||||
OnCallUserRemovedFromPolicyNotification:
|
||||
"oneuptime_oncall_user_removed_from_policy_notification",
|
||||
OnCallUserNoLongerActiveNotification:
|
||||
"oneuptime_oncall_user_no_longer_active_notification",
|
||||
} as const;
|
||||
|
||||
export const WhatsAppTemplateIds: TemplateIdsMap = templateIds;
|
||||
|
||||
export type WhatsAppTemplateIdsDefinition = typeof WhatsAppTemplateIds;
|
||||
|
||||
export type WhatsAppTemplateIdsMap = WhatsAppTemplateIdsDefinition;
|
||||
|
||||
export type WhatsAppTemplateId =
|
||||
WhatsAppTemplateIdsDefinition[keyof WhatsAppTemplateIdsDefinition];
|
||||
|
||||
type WhatsAppTemplateMessagesDefinition = Readonly<
|
||||
Record<WhatsAppTemplateId, string>
|
||||
>;
|
||||
|
||||
export const WhatsAppTemplateMessages: WhatsAppTemplateMessagesDefinition = {
|
||||
[WhatsAppTemplateIds.AlertCreated]: `A new alert #{{alert_number}} ({{alert_title}}) has been created for project {{project_name}}. To acknowledge this alert, open {{acknowledge_url}} to respond. For more information, please check out this alert {{alert_link}} on the OneUptime dashboard.`,
|
||||
[WhatsAppTemplateIds.IncidentCreated]: `A new incident #{{incident_number}} ({{incident_title}}) has been created for project {{project_name}}. To acknowledge this incident, open {{acknowledge_url}} to respond. For more information, please check out this incident {{incident_link}} on the OneUptime dashboard.`,
|
||||
[WhatsAppTemplateIds.VerificationCode]: `{{1}} is your verification code. For your security, do not share this code.`,
|
||||
[WhatsAppTemplateIds.TestNotification]: `This is a WhatsApp test message from OneUptime to verify your integration. No action is required.`,
|
||||
[WhatsAppTemplateIds.IncidentCreatedOwnerNotification]: `Incident #{{incident_number}} ({{incident_title}}) has been created for project {{project_name}}. View incident details using {{incident_link}} on the OneUptime dashboard for complete context.`,
|
||||
[WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: `A new note was posted on incident #{{incident_number}} ({{incident_title}}). Review the incident using {{incident_link}} on the OneUptime dashboard for more context.`,
|
||||
[WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: `Incident #{{incident_number}} ({{incident_title}}) state changed to {{incident_state}}. Track the incident status using {{incident_link}} on the OneUptime dashboard for more context.`,
|
||||
[WhatsAppTemplateIds.IncidentOwnerAddedNotification]: `You have been added as an owner of incident #{{incident_number}} ({{incident_title}}). Manage the incident using {{incident_link}} on the OneUptime dashboard.`,
|
||||
[WhatsAppTemplateIds.AlertCreatedOwnerNotification]: `Alert #{{alert_number}} ({{alert_title}}) has been created for project {{project_name}}. View alert details using {{alert_link}} on the OneUptime dashboard `,
|
||||
[WhatsAppTemplateIds.AlertNotePostedOwnerNotification]: `A new note was posted on alert #{{alert_number}} ({{alert_title}}). Review the alert using {{alert_link}} on the OneUptime dashboard for updates.`,
|
||||
[WhatsAppTemplateIds.AlertStateChangedOwnerNotification]: `Alert #{{alert_number}} ({{alert_title}}) state changed to {{alert_state}}. Track the alert status using {{alert_link}} on the OneUptime dashboard to stay informed.`,
|
||||
[WhatsAppTemplateIds.AlertOwnerAddedNotification]: `You have been added as an owner of alert #{{alert_number}} ({{alert_title}}). Manage the alert using {{alert_link}} on the OneUptime dashboard to take action.`,
|
||||
[WhatsAppTemplateIds.MonitorOwnerAddedNotification]: `You have been added as an owner of monitor {{monitor_name}}. Manage the monitor using {{monitor_link}} on the OneUptime dashboard to keep things running.`,
|
||||
[WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: `Monitor {{monitor_name}} has been created. Check monitor {{monitor_link}} on the OneUptime dashboard `,
|
||||
[WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: `Monitor {{monitor_name}} status changed to {{monitor_status}}. Check the monitor status using {{monitor_link}} on the OneUptime dashboard to stay informed.`,
|
||||
[WhatsAppTemplateIds.MonitorProbeStatusChangedNotification]: `Probes for monitor {{monitor_name}} are {{probe_status}}. Review probe details using {{monitor_link}} on the OneUptime dashboard for more insight.`,
|
||||
[WhatsAppTemplateIds.MonitorNoProbesMonitoringNotification]: `No probes are monitoring monitor {{monitor_name}}. Please check the monitor using {{monitor_link}} on the OneUptime dashboard to restore coverage.`,
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceCreatedOwnerNotification]: `Scheduled maintenance #{{event_number}} ({{event_title}}) has been created. View event details using {{maintenance_link}} on the OneUptime dashboard to prepare.`,
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceNotePostedOwnerNotification]: `A new note was posted on scheduled maintenance #{{event_number}} ({{event_title}}). Review the event using {{maintenance_link}} on the OneUptime dashboard for the latest updates.`,
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification]: `You have been added as an owner of scheduled maintenance #{{event_number}} ({{event_title}}). Please check the event using {{maintenance_link}} on the OneUptime dashboard.`,
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification]: `Scheduled maintenance #{{event_number}} ({{event_title}}) state changed to {{event_state}}. Track event status using {{maintenance_link}} on the OneUptime dashboard to stay aligned.`,
|
||||
[WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification]: `Announcement {{announcement_title}} was published on status page {{status_page_name}}. View the announcement using {{status_page_link}} on the OneUptime dashboard `,
|
||||
[WhatsAppTemplateIds.StatusPageCreatedOwnerNotification]: `Status page {{status_page_name}} has been created. View status page details using {{status_page_link}} on the OneUptime dashboard for full context.`,
|
||||
[WhatsAppTemplateIds.StatusPageOwnerAddedNotification]: `You have been added as an owner of status page {{status_page_name}}. Manage the status page using {{status_page_link}} on the OneUptime dashboard to stay engaged.`,
|
||||
[WhatsAppTemplateIds.ProbeStatusChangedOwnerNotification]: `Probe {{probe_name}} status is {{probe_status}}. Review the probe using {{probe_link}} on the OneUptime dashboard for specifics.`,
|
||||
[WhatsAppTemplateIds.ProbeOwnerAddedNotification]: `You have been added as an owner of probe {{probe_name}}. Manage the probe using {{probe_link}} on the OneUptime dashboard to take action.`,
|
||||
[WhatsAppTemplateIds.OnCallUserIsOnRosterNotification]: `You are now on-call for policy {{on_call_policy_name}} on schedule {{schedule_name}}. View the on-call schedule using {{schedule_link}} on the OneUptime dashboard to plan ahead.`,
|
||||
[WhatsAppTemplateIds.OnCallUserIsNextNotification]: `You are next on-call for policy {{on_call_policy_name}} on schedule {{schedule_name}}. Prepare for your shift using {{schedule_link}} on the OneUptime dashboard for the latest details.`,
|
||||
[WhatsAppTemplateIds.OnCallUserAddedToPolicyNotification]: `You have been added to on-call policy {{on_call_policy_name}} for {{on_call_context}}. Review the on-call policy using {{policy_link}} on the OneUptime dashboard for full guidelines.`,
|
||||
[WhatsAppTemplateIds.OnCallUserRemovedFromPolicyNotification]: `You have been removed from on-call policy {{on_call_policy_name}} for {{on_call_context}}. View on-call policies using {{policy_link}} on the OneUptime dashboard for updates.`,
|
||||
[WhatsAppTemplateIds.OnCallUserNoLongerActiveNotification]: `You are no longer on-call for policy {{on_call_policy_name}} on schedule {{schedule_name}}. Review your schedule using {{schedule_link}} on the OneUptime dashboard to stay informed.`,
|
||||
};
|
||||
|
||||
export const WhatsAppTemplateLanguage: Record<WhatsAppTemplateId, string> = {
|
||||
[WhatsAppTemplateIds.AlertCreated]: "en",
|
||||
[WhatsAppTemplateIds.IncidentCreated]: "en",
|
||||
[WhatsAppTemplateIds.VerificationCode]: "en",
|
||||
[WhatsAppTemplateIds.TestNotification]: "en",
|
||||
[WhatsAppTemplateIds.IncidentCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.IncidentNotePostedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.AlertCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.AlertNotePostedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.AlertStateChangedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.AlertOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.MonitorOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.MonitorProbeStatusChangedNotification]: "en",
|
||||
[WhatsAppTemplateIds.MonitorNoProbesMonitoringNotification]: "en",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceNotePostedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.StatusPageCreatedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.StatusPageOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.ProbeStatusChangedOwnerNotification]: "en",
|
||||
[WhatsAppTemplateIds.ProbeOwnerAddedNotification]: "en",
|
||||
[WhatsAppTemplateIds.OnCallUserIsOnRosterNotification]: "en",
|
||||
[WhatsAppTemplateIds.OnCallUserIsNextNotification]: "en",
|
||||
[WhatsAppTemplateIds.OnCallUserAddedToPolicyNotification]: "en",
|
||||
[WhatsAppTemplateIds.OnCallUserRemovedFromPolicyNotification]: "en",
|
||||
[WhatsAppTemplateIds.OnCallUserNoLongerActiveNotification]: "en",
|
||||
};
|
||||
|
||||
// Authentication templates that require OTP button components
|
||||
export const AuthenticationTemplates: Set<WhatsAppTemplateId> = new Set([
|
||||
WhatsAppTemplateIds.VerificationCode,
|
||||
]);
|
||||
|
||||
export function renderWhatsAppTemplate(
|
||||
templateId: WhatsAppTemplateId,
|
||||
variables: Record<string, string>,
|
||||
): string {
|
||||
const template: string | undefined = WhatsAppTemplateMessages[templateId];
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`WhatsApp template ${templateId} is not defined.`);
|
||||
}
|
||||
|
||||
return template.replace(/\{\{(.*?)\}\}/g, (_match: string, key: string) => {
|
||||
if (variables[key] === undefined) {
|
||||
throw new Error(
|
||||
`Missing variable "${key}" for WhatsApp template ${templateId}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return variables[key] as string;
|
||||
});
|
||||
}
|
||||
|
||||
export default WhatsAppTemplateMessages;
|
||||
8
Common/Types/WhatsAppStatus.ts
Normal file
8
Common/Types/WhatsAppStatus.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
enum WhatsAppStatus {
|
||||
Success = "Success",
|
||||
Error = "Error",
|
||||
LowBalance = "Low Balance",
|
||||
NotVerified = "Not Verified",
|
||||
}
|
||||
|
||||
export default WhatsAppStatus;
|
||||
@@ -17,7 +17,7 @@ import Select from "../../../Types/BaseDatabase/Select";
|
||||
export interface ComponentProps<TBaseModel extends BaseModel> {
|
||||
modelType: { new (): TBaseModel };
|
||||
modelId: ObjectID;
|
||||
onDuplicateSuccess?: (item: TBaseModel) => void | undefined;
|
||||
onDuplicateSuccess?: (item: TBaseModel) => Promise<void> | void;
|
||||
fieldsToDuplicate: Select<TBaseModel>;
|
||||
fieldsToChange: Array<ModelField<TBaseModel>>;
|
||||
navigateToOnSuccess?: Route | undefined;
|
||||
@@ -78,7 +78,9 @@ const DuplicateModel: <TBaseModel extends BaseModel>(
|
||||
throw new Error(`Could not create ${model.singularName}`);
|
||||
}
|
||||
|
||||
props.onDuplicateSuccess?.(newItem.data);
|
||||
if (props.onDuplicateSuccess) {
|
||||
await props.onDuplicateSuccess(newItem.data);
|
||||
}
|
||||
|
||||
if (props.navigateToOnSuccess) {
|
||||
Navigation.navigate(
|
||||
|
||||
@@ -1042,6 +1042,18 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>,
|
||||
);
|
||||
} else if (icon === IconProp.WhatsApp) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M26.576 5.363c-2.69-2.69-6.406-4.354-10.511-4.354-8.209 0-14.865 6.655-14.865 14.865 0 2.732 0.737 5.291 2.022 7.491l-0.038-0.070-2.109 7.702 7.879-2.067c2.051 1.139 4.498 1.809 7.102 1.809h0.006c8.209-0.003 14.862-6.659 14.862-14.868 0-4.103-1.662-7.817-4.349-10.507l0 0zM16.062 28.228h-0.005c-0 0-0.001 0-0.001 0-2.319 0-4.489-0.64-6.342-1.753l0.056 0.031-0.451-0.267-4.675 1.227 1.247-4.559-0.294-0.467c-1.185-1.862-1.889-4.131-1.889-6.565 0-6.822 5.531-12.353 12.353-12.353s12.353 5.531 12.353 12.353c0 6.822-5.53 12.353-12.353 12.353h-0zM22.838 18.977c-0.371-0.186-2.197-1.083-2.537-1.208-0.341-0.124-0.589-0.185-0.837 0.187-0.246 0.371-0.958 1.207-1.175 1.455-0.216 0.249-0.434 0.279-0.805 0.094-1.15-0.466-2.138-1.087-2.997-1.852l0.010 0.009c-0.799-0.74-1.484-1.587-2.037-2.521l-0.028-0.052c-0.216-0.371-0.023-0.572 0.162-0.757 0.167-0.166 0.372-0.434 0.557-0.65 0.146-0.179 0.271-0.384 0.366-0.604l0.006-0.017c0.043-0.087 0.068-0.188 0.068-0.296 0-0.131-0.037-0.253-0.101-0.357l0.002 0.003c-0.094-0.186-0.836-2.014-1.145-2.758-0.302-0.724-0.609-0.625-0.836-0.637-0.216-0.010-0.464-0.012-0.712-0.012-0.395 0.010-0.746 0.188-0.988 0.463l-0.001 0.002c-0.802 0.761-1.3 1.834-1.3 3.023 0 0.026 0 0.053 0.001 0.079l-0-0.004c0.131 1.467 0.681 2.784 1.527 3.857l-0.012-0.015c1.604 2.379 3.742 4.282 6.251 5.564l0.094 0.043c0.548 0.248 1.25 0.513 1.968 0.74l0.149 0.041c0.442 0.14 0.951 0.221 1.479 0.221 0.303 0 0.601-0.027 0.889-0.078l-0.031 0.004c1.069-0.223 1.956-0.868 2.497-1.749l0.009-0.017c0.165-0.366 0.261-0.793 0.261-1.242 0-0.185-0.016-0.366-0.047-0.542l0.003 0.019c-0.092-0.155-0.34-0.247-0.712-0.434z"
|
||||
/>,
|
||||
{
|
||||
viewBox: "0 0 32 32",
|
||||
strokeWidth: "1.2",
|
||||
},
|
||||
);
|
||||
} else if (icon === IconProp.Hide) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
@@ -65,11 +65,14 @@ const TimePicker: FunctionComponent<ComponentProps> = (
|
||||
}, []);
|
||||
|
||||
// Timezone label derived from OneUptimeDate utilities (e.g., "PDT (America/Los_Angeles)" or "GMT+5:30 (Asia/Kolkata)")
|
||||
const [timezoneLabel, setTimezoneLabel] = useState<string>("your local time zone");
|
||||
const [timezoneLabel, setTimezoneLabel] = useState<string>(
|
||||
"your local time zone",
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
const abbr: string = OneUptimeDate.getCurrentTimezoneString();
|
||||
const iana: string = OneUptimeDate.getCurrentTimezone() as unknown as string;
|
||||
const iana: string =
|
||||
OneUptimeDate.getCurrentTimezone() as unknown as string;
|
||||
setTimezoneLabel(`${abbr}${iana ? ` (${iana})` : ""}`);
|
||||
}, []);
|
||||
|
||||
@@ -290,7 +293,6 @@ const TimePicker: FunctionComponent<ComponentProps> = (
|
||||
submitButtonText="Apply"
|
||||
>
|
||||
<div className="p-2">
|
||||
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
{/* Hours selector */}
|
||||
<div className="flex flex-col items-center">
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"babelConfig": false
|
||||
}
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/build/dist"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"Common/(.*)": "<rootDir>/$1"
|
||||
},
|
||||
|
||||
8
Common/package-lock.json
generated
8
Common/package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
@@ -4007,9 +4007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@simplewebauthn/server": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.1.tgz",
|
||||
"integrity": "sha512-Inmfye5opZXe3HI0GaksqBnQiM7glcNySoG6DH1GgkO1Lh9dvuV4XSV9DK02DReUVX39HpcDob9nxHELjECoQw==",
|
||||
"version": "13.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz",
|
||||
"integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hexagon/base64": "^1.1.27",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
|
||||
@@ -16,6 +16,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -590,7 +590,7 @@ The zip file contains the app manifest and required icons for Teams installation
|
||||
text={`
|
||||
##### ⏳ App Approval Status
|
||||
|
||||
**Please Note:** We're currently waiting for the OneUptime app to be approved by Microsoft. Once it's approved, you will be able to add it to your teams directly from the Microsoft Teams Store.
|
||||
**Please Note:** We're currently waiting for the OneUptime app to be approved by Microsoft. Once it's approved, you will be able to add it to your teams directly from the Microsoft Teams Store. In the meatime, you can follow the Manual Sideloading installation steps below to get started.
|
||||
|
||||
##### Installation Steps (Once Approved):
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import SmsLogsTable from "./SmsLogsTable";
|
||||
import CallLogsTable from "./CallLogsTable";
|
||||
import PushLogsTable from "./PushLogsTable";
|
||||
import WorkspaceLogsTable from "./WorkspaceLogsTable";
|
||||
import WhatsAppLogsTable from "./WhatsAppLogsTable";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import BaseModel from "Common/Types/Workflow/Components/BaseModel";
|
||||
|
||||
@@ -36,6 +37,10 @@ const NotificationLogsTabs: FunctionComponent<NotificationLogsTabsProps> = (
|
||||
name: "SMS",
|
||||
children: <SmsLogsTable {...commonProps} />,
|
||||
},
|
||||
{
|
||||
name: "WhatsApp",
|
||||
children: <WhatsAppLogsTable {...commonProps} />,
|
||||
},
|
||||
{
|
||||
name: "Call",
|
||||
children: <CallLogsTable {...commonProps} />,
|
||||
|
||||
176
Dashboard/src/Components/NotificationLogs/WhatsAppLogsTable.tsx
Normal file
176
Dashboard/src/Components/NotificationLogs/WhatsAppLogsTable.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Columns from "Common/UI/Components/ModelTable/Columns";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import { Green, Red } from "Common/Types/BrandColors";
|
||||
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import Filter from "Common/UI/Components/ModelFilter/Filter";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import BaseModel from "Common/Types/Workflow/Components/BaseModel";
|
||||
import UserElement from "../User/User";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
|
||||
export interface WhatsAppLogsTableProps {
|
||||
query?: Query<BaseModel>;
|
||||
singularName?: string;
|
||||
}
|
||||
|
||||
const WhatsAppLogsTable: FunctionComponent<WhatsAppLogsTableProps> = (
|
||||
props: WhatsAppLogsTableProps,
|
||||
): ReactElement => {
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const [modalText, setModalText] = useState<string>("");
|
||||
const [modalTitle, setModalTitle] = useState<string>("");
|
||||
|
||||
const defaultColumns: Columns<WhatsAppLog> = [
|
||||
{
|
||||
field: { toNumber: true },
|
||||
title: "To",
|
||||
type: FieldType.Phone,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
user: {
|
||||
name: true,
|
||||
email: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
},
|
||||
title: "User",
|
||||
type: FieldType.Text,
|
||||
hideOnMobile: true,
|
||||
noValueMessage: "-",
|
||||
getElement: (item: WhatsAppLog): ReactElement => {
|
||||
if (!item["user"]) {
|
||||
return <p>-</p>;
|
||||
}
|
||||
|
||||
return <UserElement user={item["user"] as User} />;
|
||||
},
|
||||
},
|
||||
{ field: { createdAt: true }, title: "Sent at", type: FieldType.DateTime },
|
||||
{
|
||||
field: { status: true },
|
||||
title: "Status",
|
||||
type: FieldType.Text,
|
||||
getElement: (item: WhatsAppLog): ReactElement => {
|
||||
if (item["status"]) {
|
||||
return (
|
||||
<Pill
|
||||
isMinimal={false}
|
||||
color={item["status"] === WhatsAppStatus.Success ? Green : Red}
|
||||
text={item["status"] as string}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFilters: Array<Filter<WhatsAppLog>> = [
|
||||
{ field: { createdAt: true }, title: "Sent at", type: FieldType.Date },
|
||||
{ field: { status: true }, title: "Status", type: FieldType.Dropdown },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelTable<WhatsAppLog>
|
||||
modelType={WhatsAppLog}
|
||||
id={
|
||||
props.singularName
|
||||
? `${props.singularName.replace(/\s+/g, "-").toLowerCase()}-whatsapp-logs-table`
|
||||
: "whatsapp-logs-table"
|
||||
}
|
||||
name="WhatsApp Logs"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
showViewIdButton={true}
|
||||
isViewable={false}
|
||||
userPreferencesKey={
|
||||
props.singularName
|
||||
? `${props.singularName.replace(/\s+/g, "-").toLowerCase()}-whatsapp-logs-table`
|
||||
: "whatsapp-logs-table"
|
||||
}
|
||||
query={{
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
...(props.query || {}),
|
||||
}}
|
||||
selectMoreFields={{
|
||||
messageText: true,
|
||||
statusMessage: true,
|
||||
user: {
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
cardProps={{
|
||||
title: "WhatsApp Logs",
|
||||
description: props.singularName
|
||||
? `WhatsApp messages sent for this ${props.singularName}.`
|
||||
: "WhatsApp messages sent for this project.",
|
||||
}}
|
||||
noItemsMessage={
|
||||
props.singularName
|
||||
? `No WhatsApp logs for this ${props.singularName}.`
|
||||
: "No WhatsApp logs."
|
||||
}
|
||||
showRefreshButton={true}
|
||||
columns={defaultColumns}
|
||||
filters={defaultFilters}
|
||||
actionButtons={[
|
||||
{
|
||||
title: "View Message",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.List,
|
||||
onClick: async (
|
||||
item: WhatsAppLog,
|
||||
onCompleteAction: VoidFunction,
|
||||
) => {
|
||||
setModalText(item["messageText"] as string);
|
||||
setModalTitle("WhatsApp Message");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "View Status Message",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.Error,
|
||||
onClick: async (
|
||||
item: WhatsAppLog,
|
||||
onCompleteAction: VoidFunction,
|
||||
) => {
|
||||
setModalText(item["statusMessage"] as string);
|
||||
setModalTitle("Status Message");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showModal && (
|
||||
<ConfirmModal
|
||||
title={modalTitle}
|
||||
description={modalText || "-"}
|
||||
onSubmit={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
submitButtonText="Close"
|
||||
submitButtonType={ButtonStyleType.NORMAL}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatsAppLogsTable;
|
||||
@@ -43,6 +43,15 @@ const NotificationMethodView: FunctionComponent<ComponentProps> = (
|
||||
]?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item.getColumnValue("userWhatsApp") &&
|
||||
(item.getColumnValue("userWhatsApp") as JSONObject)["phone"] && (
|
||||
<p>
|
||||
WhatsApp:{" "}
|
||||
{(item.getColumnValue("userWhatsApp") as JSONObject)[
|
||||
"phone"
|
||||
]?.toString()}
|
||||
</p>
|
||||
)}
|
||||
{item.getColumnValue("userPush") &&
|
||||
(item.getColumnValue("userPush") as JSONObject)["deviceName"] && (
|
||||
<p>
|
||||
|
||||
298
Dashboard/src/Components/NotificationMethods/WhatsApp.tsx
Normal file
298
Dashboard/src/Components/NotificationMethods/WhatsApp.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { ErrorFunction, VoidFunction } from "Common/Types/FunctionTypes";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import BasicFormModal from "Common/UI/Components/FormModal/BasicFormModal";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import User from "Common/UI/Utils/User";
|
||||
import UserWhatsApp from "Common/Models/DatabaseModels/UserWhatsApp";
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
|
||||
const WhatsApp: () => JSX.Element = (): ReactElement => {
|
||||
const [showVerificationCodeModal, setShowVerificationCodeModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [showResendCodeModal, setShowResendCodeModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [verificationError, setVerificationError] = useState<string>("");
|
||||
const [resendError, setResendError] = useState<string>("");
|
||||
const [currentItem, setCurrentItem] = useState<UserWhatsApp | null>(null);
|
||||
const [refreshToggle, setRefreshToggle] = useState<string>(
|
||||
OneUptimeDate.getCurrentDate().toString(),
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [showVerificationCodeResentModal, setShowVerificationCodeResentModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setVerificationError("");
|
||||
}, [showVerificationCodeModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showResendCodeModal) {
|
||||
setResendError("");
|
||||
}
|
||||
}, [showResendCodeModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelTable<UserWhatsApp>
|
||||
modelType={UserWhatsApp}
|
||||
userPreferencesKey={"user-whatsapp-table"}
|
||||
query={{
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
userId: User.getUserId().toString(),
|
||||
}}
|
||||
refreshToggle={refreshToggle}
|
||||
onBeforeCreate={(model: UserWhatsApp): Promise<UserWhatsApp> => {
|
||||
model.projectId = ProjectUtil.getCurrentProjectId()!;
|
||||
model.userId = User.getUserId();
|
||||
return Promise.resolve(model);
|
||||
}}
|
||||
createVerb={"Add"}
|
||||
actionButtons={[
|
||||
{
|
||||
title: "Verify",
|
||||
buttonStyleType: ButtonStyleType.SUCCESS_OUTLINE,
|
||||
icon: IconProp.Check,
|
||||
isVisible: (item: UserWhatsApp): boolean => {
|
||||
if (item["isVerified"]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onClick: async (
|
||||
item: UserWhatsApp,
|
||||
onCompleteAction: VoidFunction,
|
||||
onError: ErrorFunction,
|
||||
) => {
|
||||
try {
|
||||
setCurrentItem(item);
|
||||
setShowVerificationCodeModal(true);
|
||||
setVerificationError("");
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Resend Code",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.SMS,
|
||||
isVisible: (item: UserWhatsApp): boolean => {
|
||||
if (item["isVerified"]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onClick: async (
|
||||
item: UserWhatsApp,
|
||||
onCompleteAction: VoidFunction,
|
||||
onError: ErrorFunction,
|
||||
) => {
|
||||
try {
|
||||
setCurrentItem(item);
|
||||
setResendError("");
|
||||
setShowResendCodeModal(true);
|
||||
|
||||
onCompleteAction();
|
||||
} catch (err) {
|
||||
onCompleteAction();
|
||||
onError(err as Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
id="user-whatsapp"
|
||||
name="User Settings > Notification Methods > WhatsApp"
|
||||
isDeleteable={true}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
cardProps={{
|
||||
title: "WhatsApp Numbers for Notifications",
|
||||
description:
|
||||
"Manage WhatsApp numbers that will receive notifications for this project.",
|
||||
}}
|
||||
noItemsMessage={
|
||||
"No WhatsApp numbers found. Please add one to receive notifications."
|
||||
}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
phone: true,
|
||||
},
|
||||
title: "WhatsApp Number",
|
||||
fieldType: FormFieldSchemaType.Phone,
|
||||
required: true,
|
||||
placeholder: "+11234567890",
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
filters={[]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
phone: true,
|
||||
},
|
||||
title: "WhatsApp Number",
|
||||
type: FieldType.Phone,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
isVerified: true,
|
||||
},
|
||||
title: "Verified",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showVerificationCodeModal && currentItem ? (
|
||||
<BasicFormModal
|
||||
title={"Verify WhatsApp Number"}
|
||||
onClose={() => {
|
||||
setShowVerificationCodeModal(false);
|
||||
}}
|
||||
error={verificationError}
|
||||
isLoading={isLoading}
|
||||
submitButtonText={"Verify"}
|
||||
onSubmit={async (item: JSONObject) => {
|
||||
setIsLoading(true);
|
||||
setVerificationError("");
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/user-whatsapp/verify",
|
||||
),
|
||||
data: {
|
||||
code: item["code"],
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
itemId: currentItem["_id"],
|
||||
},
|
||||
});
|
||||
|
||||
if (response.isFailure()) {
|
||||
setVerificationError(API.getFriendlyMessage(response));
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setShowVerificationCodeModal(false);
|
||||
setRefreshToggle(OneUptimeDate.getCurrentDate().toString());
|
||||
}
|
||||
} catch (e) {
|
||||
setVerificationError(API.getFriendlyMessage(e));
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
formProps={{
|
||||
name: "Verify WhatsApp Number",
|
||||
|
||||
fields: [
|
||||
{
|
||||
title: "Verification Code",
|
||||
description:
|
||||
"We have sent a WhatsApp message with your verification code.",
|
||||
field: {
|
||||
code: true,
|
||||
},
|
||||
placeholder: "123456",
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 6,
|
||||
maxLength: 6,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Number,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{showResendCodeModal && currentItem ? (
|
||||
<ConfirmModal
|
||||
title={`Resend Code`}
|
||||
error={resendError}
|
||||
description={
|
||||
"Are you sure you want to resend the WhatsApp verification code?"
|
||||
}
|
||||
submitButtonText={"Resend Code"}
|
||||
onClose={() => {
|
||||
setShowResendCodeModal(false);
|
||||
setResendError("");
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
onSubmit={async () => {
|
||||
setIsLoading(true);
|
||||
setResendError("");
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/user-whatsapp/resend-verification-code",
|
||||
),
|
||||
data: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
itemId: currentItem["_id"],
|
||||
},
|
||||
});
|
||||
|
||||
if (response.isFailure()) {
|
||||
setResendError(API.getFriendlyMessage(response));
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setShowResendCodeModal(false);
|
||||
setShowVerificationCodeResentModal(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setResendError(API.getFriendlyMessage(err));
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{showVerificationCodeResentModal ? (
|
||||
<ConfirmModal
|
||||
title={`Code sent successfully`}
|
||||
error={resendError}
|
||||
description={`We have sent a verification code via WhatsApp.`}
|
||||
submitButtonText={"Close"}
|
||||
onSubmit={async () => {
|
||||
setShowVerificationCodeResentModal(false);
|
||||
setResendError("");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatsApp;
|
||||
@@ -257,7 +257,6 @@ const OnCallDutyScheduleView: FunctionComponent<
|
||||
onCallDutyPolicyScheduleId={modelId}
|
||||
projectId={ProjectUtil.getCurrentProjectId() as ObjectID}
|
||||
/>
|
||||
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,178 @@
|
||||
import DuplicateModel from "Common/UI/Components/DuplicateModel/DuplicateModel";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import OnCallDutySchedule from "Common/Models/DatabaseModels/OnCallDutyPolicySchedule";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import OnCallDutyPolicyScheduleLayer from "Common/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer";
|
||||
import OnCallDutyPolicyScheduleLayerUser from "Common/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import ModelAPI, { type ListResult } from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const OnCallDutyScheduleSettings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
const OnCallDutyScheduleSettings: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const duplicateScheduleLayers: (
|
||||
newSchedule: OnCallDutySchedule,
|
||||
) => Promise<void> = async (
|
||||
newSchedule: OnCallDutySchedule,
|
||||
): Promise<void> => {
|
||||
const projectId: ObjectID | null =
|
||||
newSchedule.projectId || ProjectUtil.getCurrentProjectId();
|
||||
|
||||
if (!newSchedule.id) {
|
||||
throw new Error(
|
||||
"Failed to duplicate schedule layers: new schedule ID is missing.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error(
|
||||
"Failed to duplicate schedule layers: project ID is missing.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const existingLayers: ListResult<OnCallDutyPolicyScheduleLayer> =
|
||||
await ModelAPI.getList<OnCallDutyPolicyScheduleLayer>({
|
||||
modelType: OnCallDutyPolicyScheduleLayer,
|
||||
query: {
|
||||
onCallDutyPolicyScheduleId: modelId,
|
||||
projectId: projectId,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
order: true,
|
||||
name: true,
|
||||
description: true,
|
||||
startsAt: true,
|
||||
restrictionTimes: true,
|
||||
rotation: true,
|
||||
handOffTime: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingLayers.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const layer of existingLayers.data) {
|
||||
if (!layer.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newLayer: OnCallDutyPolicyScheduleLayer =
|
||||
new OnCallDutyPolicyScheduleLayer();
|
||||
newLayer.projectId = projectId;
|
||||
newLayer.onCallDutyPolicyScheduleId = newSchedule.id;
|
||||
if (!layer.name) {
|
||||
throw new Error(
|
||||
"Failed to duplicate schedule layers: layer name is missing.",
|
||||
);
|
||||
}
|
||||
newLayer.name = layer.name;
|
||||
|
||||
if (layer.description !== undefined) {
|
||||
newLayer.description = layer.description;
|
||||
}
|
||||
|
||||
if (typeof layer.order === "number") {
|
||||
newLayer.order = layer.order;
|
||||
}
|
||||
|
||||
if (!layer.startsAt) {
|
||||
throw new Error(
|
||||
"Failed to duplicate schedule layers: layer start time is missing.",
|
||||
);
|
||||
}
|
||||
newLayer.startsAt = new Date(layer.startsAt);
|
||||
|
||||
if (!layer.handOffTime) {
|
||||
throw new Error(
|
||||
"Failed to duplicate schedule layers: layer hand off time is missing.",
|
||||
);
|
||||
}
|
||||
newLayer.handOffTime = new Date(layer.handOffTime);
|
||||
if (layer.rotation) {
|
||||
newLayer.rotation = layer.rotation;
|
||||
}
|
||||
|
||||
if (layer.restrictionTimes) {
|
||||
newLayer.restrictionTimes = layer.restrictionTimes;
|
||||
}
|
||||
|
||||
const createdLayer: OnCallDutyPolicyScheduleLayer = (
|
||||
await ModelAPI.create<OnCallDutyPolicyScheduleLayer>({
|
||||
model: newLayer,
|
||||
modelType: OnCallDutyPolicyScheduleLayer,
|
||||
})
|
||||
).data as OnCallDutyPolicyScheduleLayer;
|
||||
|
||||
if (!createdLayer.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingLayerUsers: ListResult<OnCallDutyPolicyScheduleLayerUser> =
|
||||
await ModelAPI.getList<OnCallDutyPolicyScheduleLayerUser>({
|
||||
modelType: OnCallDutyPolicyScheduleLayerUser,
|
||||
query: {
|
||||
onCallDutyPolicyScheduleId: modelId,
|
||||
onCallDutyPolicyScheduleLayerId: layer.id,
|
||||
projectId: projectId,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
order: true,
|
||||
userId: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
for (const existingLayerUser of existingLayerUsers.data) {
|
||||
if (!existingLayerUser.userId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newLayerUser: OnCallDutyPolicyScheduleLayerUser =
|
||||
new OnCallDutyPolicyScheduleLayerUser();
|
||||
newLayerUser.projectId = projectId;
|
||||
newLayerUser.onCallDutyPolicyScheduleId = newSchedule.id;
|
||||
newLayerUser.onCallDutyPolicyScheduleLayerId = createdLayer.id;
|
||||
newLayerUser.userId = existingLayerUser.userId;
|
||||
if (typeof existingLayerUser.order === "number") {
|
||||
newLayerUser.order = existingLayerUser.order;
|
||||
}
|
||||
|
||||
await ModelAPI.create<OnCallDutyPolicyScheduleLayerUser>({
|
||||
model: newLayerUser,
|
||||
modelType: OnCallDutyPolicyScheduleLayerUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to duplicate schedule layers: ${API.getFriendlyMessage(err)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -21,7 +183,9 @@ const OnCallDutyScheduleSettings: FunctionComponent<PageComponentProps> = (): Re
|
||||
fieldsToDuplicate={{
|
||||
description: true,
|
||||
labels: true,
|
||||
projectId: true,
|
||||
}}
|
||||
onDuplicateSuccess={duplicateScheduleLayers}
|
||||
navigateToOnSuccess={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ON_CALL_DUTY_SCHEDULES] as Route,
|
||||
)}
|
||||
|
||||
@@ -80,7 +80,8 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
name="Enable Notifications"
|
||||
cardProps={{
|
||||
title: "Enable Notifications",
|
||||
description: "Enable Call and SMS notifications for this project.",
|
||||
description:
|
||||
"Enable Call, SMS, and WhatsApp notifications for this project.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Notification Settings"
|
||||
@@ -95,6 +96,16 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
enableWhatsAppNotifications: true,
|
||||
},
|
||||
title: "Enable WhatsApp Notifications",
|
||||
description:
|
||||
"Enable WhatsApp notifications for this project. This will be used for alerting users via WhatsApp.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
enableSmsNotifications: true,
|
||||
@@ -120,6 +131,16 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
description:
|
||||
"Enable Call notifications for this project. This will be used for alerting users by phone call.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
enableWhatsAppNotifications: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Enable WhatsApp Notifications",
|
||||
placeholder: "Not Enabled",
|
||||
description:
|
||||
"Enable WhatsApp notifications for this project. This will be used for alerting users via WhatsApp.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
enableSmsNotifications: true,
|
||||
|
||||
@@ -23,6 +23,7 @@ import UserEmail from "Common/Models/DatabaseModels/UserEmail";
|
||||
import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationRule";
|
||||
import UserPush from "Common/Models/DatabaseModels/UserPush";
|
||||
import UserSMS from "Common/Models/DatabaseModels/UserSMS";
|
||||
import UserWhatsApp from "Common/Models/DatabaseModels/UserWhatsApp";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -39,6 +40,7 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
);
|
||||
const [userEmails, setUserEmails] = useState<Array<UserEmail>>([]);
|
||||
const [userSMSs, setUserSMSs] = useState<Array<UserSMS>>([]);
|
||||
const [userWhatsApps, setUserWhatsApps] = useState<Array<UserWhatsApp>>([]);
|
||||
const [userCalls, setUserCalls] = useState<Array<UserCall>>([]);
|
||||
const [userPush, setUserPush] = useState<Array<UserPush>>([]);
|
||||
const [
|
||||
@@ -131,6 +133,19 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
if (userPushDevice) {
|
||||
model.userPushId = userPushDevice.id!;
|
||||
}
|
||||
|
||||
const userWhatsApp: UserWhatsApp | undefined = userWhatsApps.find(
|
||||
(userWhatsApp: UserWhatsApp) => {
|
||||
return (
|
||||
userWhatsApp.id!.toString() ===
|
||||
miscDataProps["notificationMethod"]?.toString()
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (userWhatsApp) {
|
||||
model.userWhatsAppId = userWhatsApp.id!;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(model);
|
||||
@@ -189,6 +204,9 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
}}
|
||||
filters={[]}
|
||||
columns={[
|
||||
@@ -197,9 +215,18 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
userCall: {
|
||||
phone: true,
|
||||
},
|
||||
userEmail: {
|
||||
email: true,
|
||||
},
|
||||
userSms: {
|
||||
phone: true,
|
||||
},
|
||||
userPush: {
|
||||
deviceName: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
title: "Notification Method",
|
||||
type: FieldType.Text,
|
||||
@@ -324,6 +351,25 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
|
||||
setUserPush(userPushDevices.data);
|
||||
|
||||
const userWhatsAppList: ListResult<UserWhatsApp> = await ModelAPI.getList(
|
||||
{
|
||||
modelType: UserWhatsApp,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
userId: User.getUserId(),
|
||||
isVerified: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
phone: true,
|
||||
},
|
||||
sort: {},
|
||||
},
|
||||
);
|
||||
|
||||
setUserWhatsApps(userWhatsAppList.data);
|
||||
|
||||
setAlertSeverities(alertSeverities.data);
|
||||
|
||||
const dropdownOptions: Array<DropdownOption> = [
|
||||
@@ -331,10 +377,12 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
...userEmails.data,
|
||||
...userSMSes.data,
|
||||
...userPushDevices.data,
|
||||
...userWhatsAppList.data,
|
||||
].map((model: BaseModel) => {
|
||||
const isUserCall: boolean = model instanceof UserCall;
|
||||
const isUserSms: boolean = model instanceof UserSMS;
|
||||
const isUserPush: boolean = model instanceof UserPush;
|
||||
const isUserWhatsApp: boolean = model instanceof UserWhatsApp;
|
||||
|
||||
let option: DropdownOption;
|
||||
|
||||
@@ -358,6 +406,8 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
option.label = "Call: " + option.label;
|
||||
} else if (isUserSms) {
|
||||
option.label = "SMS: " + option.label;
|
||||
} else if (isUserWhatsApp) {
|
||||
option.label = "WhatsApp: " + option.label;
|
||||
} else {
|
||||
option.label = "Email: " + option.label;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import UserEmail from "Common/Models/DatabaseModels/UserEmail";
|
||||
import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationRule";
|
||||
import UserPush from "Common/Models/DatabaseModels/UserPush";
|
||||
import UserSMS from "Common/Models/DatabaseModels/UserSMS";
|
||||
import UserWhatsApp from "Common/Models/DatabaseModels/UserWhatsApp";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
@@ -39,6 +40,7 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
>([]);
|
||||
const [userEmails, setUserEmails] = useState<Array<UserEmail>>([]);
|
||||
const [userSMSs, setUserSMSs] = useState<Array<UserSMS>>([]);
|
||||
const [userWhatsApps, setUserWhatsApps] = useState<Array<UserWhatsApp>>([]);
|
||||
const [userCalls, setUserCalls] = useState<Array<UserCall>>([]);
|
||||
const [userPush, setUserPush] = useState<Array<UserPush>>([]);
|
||||
const [
|
||||
@@ -131,6 +133,19 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
if (userPushDevice) {
|
||||
model.userPushId = userPushDevice.id!;
|
||||
}
|
||||
|
||||
const userWhatsApp: UserWhatsApp | undefined = userWhatsApps.find(
|
||||
(userWhatsApp: UserWhatsApp) => {
|
||||
return (
|
||||
userWhatsApp.id!.toString() ===
|
||||
miscDataProps["notificationMethod"]?.toString()
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (userWhatsApp) {
|
||||
model.userWhatsAppId = userWhatsApp.id!;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(model);
|
||||
@@ -189,6 +204,9 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
}}
|
||||
filters={[]}
|
||||
columns={[
|
||||
@@ -207,6 +225,9 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
title: "Notification Method",
|
||||
type: FieldType.Text,
|
||||
@@ -330,6 +351,25 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
|
||||
setUserPush(userPushDevices.data);
|
||||
|
||||
const userWhatsAppList: ListResult<UserWhatsApp> = await ModelAPI.getList(
|
||||
{
|
||||
modelType: UserWhatsApp,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
userId: User.getUserId(),
|
||||
isVerified: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
phone: true,
|
||||
},
|
||||
sort: {},
|
||||
},
|
||||
);
|
||||
|
||||
setUserWhatsApps(userWhatsAppList.data);
|
||||
|
||||
setIncidentSeverities(incidentSeverities.data);
|
||||
|
||||
const dropdownOptions: Array<DropdownOption> = [
|
||||
@@ -337,10 +377,12 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
...userEmails.data,
|
||||
...userSMSes.data,
|
||||
...userPushDevices.data,
|
||||
...userWhatsAppList.data,
|
||||
].map((model: BaseModel) => {
|
||||
const isUserCall: boolean = model instanceof UserCall;
|
||||
const isUserSms: boolean = model instanceof UserSMS;
|
||||
const isUserPush: boolean = model instanceof UserPush;
|
||||
const isUserWhatsApp: boolean = model instanceof UserWhatsApp;
|
||||
|
||||
let option: DropdownOption;
|
||||
|
||||
@@ -364,6 +406,8 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
option.label = "Call: " + option.label;
|
||||
} else if (isUserSms) {
|
||||
option.label = "SMS: " + option.label;
|
||||
} else if (isUserWhatsApp) {
|
||||
option.label = "WhatsApp: " + option.label;
|
||||
} else {
|
||||
option.label = "Email: " + option.label;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import UserCall from "../../Components/NotificationMethods/Call";
|
||||
import UserEmail from "../../Components/NotificationMethods/Email";
|
||||
import UserSMS from "../../Components/NotificationMethods/SMS";
|
||||
import UserPush from "../../Components/NotificationMethods/Push";
|
||||
import UserSMS from "../../Components/NotificationMethods/SMS";
|
||||
import UserWhatsApp from "../../Components/NotificationMethods/WhatsApp";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
@@ -10,6 +11,7 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
<Fragment>
|
||||
<UserEmail />
|
||||
<UserSMS />
|
||||
<UserWhatsApp />
|
||||
<UserCall />
|
||||
<UserPush />
|
||||
</Fragment>
|
||||
|
||||
@@ -10,6 +10,17 @@ import UserNotificationSetting from "Common/Models/DatabaseModels/UserNotificati
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import Includes from "Common/Types/BaseDatabase/Includes";
|
||||
import { ShowAs } from "Common/UI/Components/ModelTable/BaseModelTable";
|
||||
import Icon, { SizeProp } from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Color from "Common/Types/Color";
|
||||
import {
|
||||
Blue500,
|
||||
Gray500,
|
||||
Green500,
|
||||
Orange500,
|
||||
Purple500,
|
||||
Sky500,
|
||||
} from "Common/Types/BrandColors";
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
type GetModelTableFunctionProps = {
|
||||
@@ -87,6 +98,15 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertByWhatsApp: true,
|
||||
},
|
||||
title: "Alert By WhatsApp",
|
||||
description: "Select if you want to be alerted by WhatsApp.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertByCall: true,
|
||||
@@ -109,6 +129,12 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
filters={[]}
|
||||
selectMoreFields={{
|
||||
alertBySMS: true,
|
||||
alertByWhatsApp: true,
|
||||
alertByCall: true,
|
||||
alertByPush: true,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
@@ -121,29 +147,86 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
field: {
|
||||
alertByEmail: true,
|
||||
},
|
||||
title: "Email Alerts",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertBySMS: true,
|
||||
title: "Delivery Channels",
|
||||
type: FieldType.Element,
|
||||
getElement: (item: UserNotificationSetting): ReactElement => {
|
||||
type ChannelDescriptor = {
|
||||
key: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
icon: IconProp;
|
||||
color: Color;
|
||||
};
|
||||
|
||||
const channels: Array<ChannelDescriptor> = [
|
||||
{
|
||||
key: "email",
|
||||
label: "Email",
|
||||
enabled: Boolean(item.alertByEmail),
|
||||
icon: IconProp.Email,
|
||||
color: Blue500,
|
||||
},
|
||||
{
|
||||
key: "sms",
|
||||
label: "SMS",
|
||||
enabled: Boolean(item.alertBySMS),
|
||||
icon: IconProp.SMS,
|
||||
color: Purple500,
|
||||
},
|
||||
{
|
||||
key: "call",
|
||||
label: "Call",
|
||||
enabled: Boolean(item.alertByCall),
|
||||
icon: IconProp.Call,
|
||||
color: Orange500,
|
||||
},
|
||||
{
|
||||
key: "push",
|
||||
label: "Push",
|
||||
enabled: Boolean(item.alertByPush),
|
||||
icon: IconProp.Notification,
|
||||
color: Sky500,
|
||||
},
|
||||
{
|
||||
key: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
enabled: Boolean(item.alertByWhatsApp),
|
||||
icon: IconProp.WhatsApp,
|
||||
color: Green500,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{channels.map((channel: ChannelDescriptor) => {
|
||||
const stateClasses: string = channel.enabled
|
||||
? "border-emerald-500/60 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
|
||||
: "border-gray-200 bg-gray-100 text-gray-500 dark:border-gray-600 dark:bg-gray-700/60 dark:text-gray-300";
|
||||
|
||||
return (
|
||||
<span
|
||||
key={channel.key}
|
||||
aria-label={`${channel.label} alerts ${channel.enabled ? "enabled" : "disabled"}`}
|
||||
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium transition-colors ${stateClasses}`}
|
||||
>
|
||||
<Icon
|
||||
icon={channel.icon}
|
||||
className="h-3 w-3"
|
||||
size={SizeProp.Small}
|
||||
color={channel.enabled ? channel.color : Gray500}
|
||||
/>
|
||||
<span>{channel.label}</span>
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wide ${channel.enabled ? "text-emerald-600 dark:text-emerald-200" : "text-gray-400 dark:text-gray-400"}`}
|
||||
>
|
||||
{channel.enabled ? "On" : "Off"}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
title: "SMS Alerts",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertByCall: true,
|
||||
},
|
||||
title: "Call Alerts",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
alertByPush: true,
|
||||
},
|
||||
title: "Push Alerts",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -60,6 +60,9 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
userPush: {
|
||||
deviceName: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
}}
|
||||
noItemsMessage={"No notifications sent out so far."}
|
||||
showRefreshButton={true}
|
||||
@@ -112,6 +115,18 @@ const Settings: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
userCall: {
|
||||
phone: true,
|
||||
},
|
||||
userEmail: {
|
||||
email: true,
|
||||
},
|
||||
userSms: {
|
||||
phone: true,
|
||||
},
|
||||
userPush: {
|
||||
deviceName: true,
|
||||
},
|
||||
userWhatsApp: {
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
title: "Notification Method",
|
||||
type: FieldType.Element,
|
||||
|
||||
6
Docs/DevPromps/MobileAppPrompt.md
Normal file
6
Docs/DevPromps/MobileAppPrompt.md
Normal file
@@ -0,0 +1,6 @@
|
||||
We're building the OneUptime mobile app in react native and expo. This mobile app is in MobileApp diretctory.
|
||||
|
||||
<PROMPT>
|
||||
|
||||
Please make sure the code you write is clean, refactored well and not duplicated. If you like to refactor code into seperate files, please use as many files as you like.
|
||||
Please make sure to use typescript and type all the variables and functions well. Please do not use "any" type. Please make sure the files you areate are PascalCase and not camelCase.
|
||||
@@ -17,6 +17,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
|
||||
# IF APP_VERSION is not set, set it to 1.0.0
|
||||
|
||||
@@ -7,6 +7,7 @@ import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressStatic,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import LocalFile from "Common/Server/Utils/LocalFile";
|
||||
@@ -25,7 +26,7 @@ const DocsFeatureSet: FeatureSet = {
|
||||
// Handle requests to specific documentation pages
|
||||
app.get(
|
||||
"/docs/as-markdown/:categorypath/:pagepath",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const fullPath: string =
|
||||
`${req.params["categorypath"]}/${req.params["pagepath"]}`.toLowerCase();
|
||||
@@ -38,15 +39,18 @@ const DocsFeatureSet: FeatureSet = {
|
||||
return Response.sendMarkdownResponse(req, res, contentInMarkdown);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
res.status(500);
|
||||
return res.send("Internal Server Error");
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/docs/:categorypath/:pagepath",
|
||||
async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (
|
||||
_req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const fullPath: string =
|
||||
`${_req.params["categorypath"]}/${_req.params["pagepath"]}`.toLowerCase();
|
||||
@@ -114,12 +118,7 @@ const DocsFeatureSet: FeatureSet = {
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
res.status(500);
|
||||
return res.render(`${ViewsPath}/ServerError`, {
|
||||
nav: DocsNav,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
link: "",
|
||||
});
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
ENV NODE_OPTIONS="--use-openssl-ca"
|
||||
|
||||
## Add Intermediate Certs
|
||||
@@ -59,7 +60,7 @@ ENV PRODUCTION=true
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN npx playwright install --with-deps
|
||||
RUN PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0 npx playwright install --with-deps
|
||||
|
||||
# Install app dependencies
|
||||
COPY ./FluentIngest/package*.json /usr/src/app/
|
||||
|
||||
21
HelmChart/Docs/GitHub.md
Normal file
21
HelmChart/Docs/GitHub.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# GitHub Ops
|
||||
|
||||
### Cancel all GitHub actions jobs at once.
|
||||
|
||||
Please install GitHub CLI and run the below command in your terminal.
|
||||
|
||||
```
|
||||
brew install gh
|
||||
```
|
||||
|
||||
Authenticate with your GitHub account
|
||||
|
||||
```
|
||||
gh auth login
|
||||
````
|
||||
|
||||
Then run the below command in your terminal
|
||||
|
||||
```
|
||||
for id in $(gh run list --limit 5000 --jq ".[] | select (.status == \"queued\" ) | .databaseId" --json databaseId,status); do gh run cancel $id; done
|
||||
```
|
||||
@@ -110,6 +110,8 @@ spec:
|
||||
value: {{ $.Values.billing.callHighRiskValueInCentsPerMinute | quote }}
|
||||
- name: SMS_DEFAULT_COST_IN_CENTS
|
||||
value: {{ $.Values.billing.smsDefaultValueInCents | quote }}
|
||||
- name: WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS
|
||||
value: {{ $.Values.billing.whatsAppTextDefaultValueInCents | quote }}
|
||||
- name: CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE
|
||||
value: {{ $.Values.billing.callDefaultValueInCentsPerMinute | quote }}
|
||||
- name: DISABLE_TELEMETRY
|
||||
|
||||
@@ -582,6 +582,9 @@
|
||||
"smsDefaultValueInCents": {
|
||||
"type": ["integer", "null"]
|
||||
},
|
||||
"whatsAppTextDefaultValueInCents": {
|
||||
"type": ["integer", "null"]
|
||||
},
|
||||
"callDefaultValueInCentsPerMinute": {
|
||||
"type": ["integer", "null"]
|
||||
},
|
||||
|
||||
@@ -225,6 +225,7 @@ billing:
|
||||
publicKey:
|
||||
privateKey:
|
||||
smsDefaultValueInCents:
|
||||
whatsAppTextDefaultValueInCents:
|
||||
callDefaultValueInCentsPerMinute:
|
||||
smsHighRiskValueInCents:
|
||||
callHighRiskValueInCentsPerMinute:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import BlogPostUtil, { BlogPost, BlogPostHeader } from "../Utils/BlogPost";
|
||||
import { BlogRootPath, ViewsPath } from "../Utils/Config";
|
||||
import NotFoundUtil from "../Utils/NotFound";
|
||||
import ServerErrorUtil from "../Utils/ServerError";
|
||||
import Text from "Common/Types/Text";
|
||||
import Express, {
|
||||
ExpressApplication,
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -19,7 +19,7 @@ const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
app.get(
|
||||
"/blog/post/:file",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const fileName: string = req.params["file"] as string;
|
||||
|
||||
@@ -30,14 +30,14 @@ app.get(
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ServerErrorUtil.renderServerError(res);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/blog/post/:file/view",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const fileName: string = req.params["file"] as string;
|
||||
|
||||
@@ -59,14 +59,14 @@ app.get(
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ServerErrorUtil.renderServerError(res);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/blog/post/:postName/:fileName",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
/*
|
||||
* return static files for blog post images
|
||||
* the static files are stored in the /usr/src/blog/post/:file/:imageName
|
||||
@@ -83,7 +83,7 @@ app.get(
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ServerErrorUtil.renderServerError(res);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -92,7 +92,7 @@ app.get(
|
||||
|
||||
app.get(
|
||||
"/blog/tag/:tagName",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const tagName: string = req.params["tagName"] as string;
|
||||
const tagSlug: string = tagName; // original slug
|
||||
@@ -149,64 +149,67 @@ app.get(
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ServerErrorUtil.renderServerError(res);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// main blog page
|
||||
app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const req: ExpressRequest = _req; // alias for clarity
|
||||
const pageParam: string | undefined = req.query["page"] as
|
||||
| string
|
||||
| undefined;
|
||||
const pageSizeParam: string | undefined = req.query["pageSize"] as
|
||||
| string
|
||||
| undefined;
|
||||
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
|
||||
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
|
||||
if (isNaN(page) || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (isNaN(pageSize) || pageSize < 1) {
|
||||
pageSize = 24;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
app.get(
|
||||
"/blog",
|
||||
async (_req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const req: ExpressRequest = _req; // alias for clarity
|
||||
const pageParam: string | undefined = req.query["page"] as
|
||||
| string
|
||||
| undefined;
|
||||
const pageSizeParam: string | undefined = req.query["pageSize"] as
|
||||
| string
|
||||
| undefined;
|
||||
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
|
||||
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
|
||||
if (isNaN(page) || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (isNaN(pageSize) || pageSize < 1) {
|
||||
pageSize = 24;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
const allPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList();
|
||||
const totalPosts: number = allPosts.length;
|
||||
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
|
||||
if (page > totalPages) {
|
||||
page = totalPages;
|
||||
}
|
||||
const start: number = (page - 1) * pageSize;
|
||||
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
|
||||
start,
|
||||
start + pageSize,
|
||||
);
|
||||
const allTags: Array<string> = await BlogPostUtil.getTags();
|
||||
const allPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList();
|
||||
const totalPosts: number = allPosts.length;
|
||||
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
|
||||
if (page > totalPages) {
|
||||
page = totalPages;
|
||||
}
|
||||
const start: number = (page - 1) * pageSize;
|
||||
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
|
||||
start,
|
||||
start + pageSize,
|
||||
);
|
||||
const allTags: Array<string> = await BlogPostUtil.getTags();
|
||||
|
||||
res.render(`${ViewsPath}/Blog/List`, {
|
||||
support: false,
|
||||
footerCards: true,
|
||||
cta: true,
|
||||
blackLogo: false,
|
||||
requestDemoCta: false,
|
||||
blogPosts: paginatedPosts,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
totalPages: totalPages,
|
||||
totalPosts: totalPosts,
|
||||
basePath: `/blog`,
|
||||
allTags: allTags,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ServerErrorUtil.renderServerError(res);
|
||||
}
|
||||
});
|
||||
res.render(`${ViewsPath}/Blog/List`, {
|
||||
support: false,
|
||||
footerCards: true,
|
||||
cta: true,
|
||||
blackLogo: false,
|
||||
requestDemoCta: false,
|
||||
blogPosts: paginatedPosts,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
totalPages: totalPages,
|
||||
totalPosts: totalPosts,
|
||||
basePath: `/blog`,
|
||||
allTags: allTags,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user