mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
376 Commits
dropdown-l
...
postmortem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4868e285b0 | ||
|
|
f572eb6f93 | ||
|
|
a0868e2f75 | ||
|
|
3dfd7a9206 | ||
|
|
d7582337bf | ||
|
|
23043462d7 | ||
|
|
76d53c53c8 | ||
|
|
4437e912a3 | ||
|
|
937d4675a8 | ||
|
|
3cc984f149 | ||
|
|
991928a5a5 | ||
|
|
6f46812418 | ||
|
|
6e20e7f08f | ||
|
|
ae406d8ee1 | ||
|
|
05920d5b99 | ||
|
|
3a309aabcf | ||
|
|
22a3004a3f | ||
|
|
b8f69fbea3 | ||
|
|
888aff6392 | ||
|
|
234de977c4 | ||
|
|
fa5f606709 | ||
|
|
b889611d16 | ||
|
|
43f0eeb0f8 | ||
|
|
be311dd8b5 | ||
|
|
29428bf660 | ||
|
|
9eebbe9dfb | ||
|
|
0dc3bb4f33 | ||
|
|
adf5a9c1f3 | ||
|
|
faaded049a | ||
|
|
d02e3882be | ||
|
|
e1af84fafa | ||
|
|
c371f0a25f | ||
|
|
c86d2c2a4a | ||
|
|
a807cc10ab | ||
|
|
6cc480744d | ||
|
|
7cb6104795 | ||
|
|
49dd315501 | ||
|
|
9ec2b458ed | ||
|
|
702b5811a9 | ||
|
|
7dc7255790 | ||
|
|
1f620e7092 | ||
|
|
87466246fa | ||
|
|
12eaa17859 | ||
|
|
e782ae6b3c | ||
|
|
9ad87328c2 | ||
|
|
8279294d15 | ||
|
|
8c6da51d58 | ||
|
|
6d114e3ac4 | ||
|
|
44427d3ee7 | ||
|
|
09b0c3b1ef | ||
|
|
ad597fe5dd | ||
|
|
74f17fa45c | ||
|
|
b19a5fa58a | ||
|
|
57abffa113 | ||
|
|
e8e493ee5a | ||
|
|
e065ebdddc | ||
|
|
39da442892 | ||
|
|
45b02b30e3 | ||
|
|
30414327f9 | ||
|
|
b99a20a588 | ||
|
|
22178c282d | ||
|
|
30389a8d49 | ||
|
|
7b73cc2ea7 | ||
|
|
6d2c331216 | ||
|
|
624e4c2296 | ||
|
|
5e901ee973 | ||
|
|
a103abc7a9 | ||
|
|
a7dda0bd53 | ||
|
|
6948754c86 | ||
|
|
cc5731bb6d | ||
|
|
6761a8a686 | ||
|
|
6e487199aa | ||
|
|
cda5de92ec | ||
|
|
33349341a9 | ||
|
|
db81fdd3e7 | ||
|
|
d71eba91dd | ||
|
|
682bb805f3 | ||
|
|
7f38e3d417 | ||
|
|
559985e93b | ||
|
|
43588cbe5a | ||
|
|
0772fce477 | ||
|
|
78107d8b1c | ||
|
|
078af43b0c | ||
|
|
9b9aeb2f40 | ||
|
|
67577f5a2b | ||
|
|
4e808cf382 | ||
|
|
c993b33dab | ||
|
|
3c5a64024b | ||
|
|
86efe54a29 | ||
|
|
17bf568428 | ||
|
|
26ac698cc7 | ||
|
|
72bb25e036 | ||
|
|
1f23742c1f | ||
|
|
ac66cee4aa | ||
|
|
66efe2d2fa | ||
|
|
0ad5c14882 | ||
|
|
2468b39dd2 | ||
|
|
4fec2caef6 | ||
|
|
dc041d924a | ||
|
|
37acc617a0 | ||
|
|
cd28370ce3 | ||
|
|
e847f430f2 | ||
|
|
d1e94daaca | ||
|
|
df264d6766 | ||
|
|
49c2312c47 | ||
|
|
0fd3121b29 | ||
|
|
ea43c43991 | ||
|
|
51a128efd3 | ||
|
|
847bac5c6a | ||
|
|
29b137afbd | ||
|
|
1be0b475a6 | ||
|
|
2467d2c02d | ||
|
|
b9597250ac | ||
|
|
203e9b8c39 | ||
|
|
16078ffe3b | ||
|
|
898c4de78f | ||
|
|
da53b7c51c | ||
|
|
8a330e7914 | ||
|
|
8bf7b8dfa2 | ||
|
|
9d36920477 | ||
|
|
264cdc7c6b | ||
|
|
3d8daa46aa | ||
|
|
673ab6845f | ||
|
|
bb3df528cf | ||
|
|
f52e73afb2 | ||
|
|
3e04d38eb1 | ||
|
|
27c2ffdfbd | ||
|
|
78ee52fb4d | ||
|
|
adc15561b9 | ||
|
|
e19a14e906 | ||
|
|
035f3412b8 | ||
|
|
deb902463c | ||
|
|
a03a2bf9b0 | ||
|
|
5f396d36a4 | ||
|
|
99cf626d7d | ||
|
|
ae72437591 | ||
|
|
86301213f0 | ||
|
|
c6e889b2a8 | ||
|
|
0a053c51e3 | ||
|
|
296ecbd9e3 | ||
|
|
aa4797cc54 | ||
|
|
fd4759f16e | ||
|
|
a7b7dc61cf | ||
|
|
3b0bdca980 | ||
|
|
07bc6d4edd | ||
|
|
8642a54fec | ||
|
|
9ed0c3cf2b | ||
|
|
396c73f601 | ||
|
|
ceb54ae12d | ||
|
|
8df9a14b13 | ||
|
|
7d32627917 | ||
|
|
a9ea19507e | ||
|
|
8c2c002382 | ||
|
|
2a2aca032e | ||
|
|
911fe180ab | ||
|
|
11cbe5f34a | ||
|
|
883f51e2d2 | ||
|
|
8ebe034f0c | ||
|
|
96ea2f6ff7 | ||
|
|
d91a8cae67 | ||
|
|
959035fd14 | ||
|
|
e1730e4d3a | ||
|
|
4aa009f46c | ||
|
|
1551401fc3 | ||
|
|
3768c95aa1 | ||
|
|
57e933ee9c | ||
|
|
add0efa6db | ||
|
|
7d12c8997e | ||
|
|
f836369c01 | ||
|
|
f48c3c608c | ||
|
|
58a955baf7 | ||
|
|
b5243cec1a | ||
|
|
d6336ee8f3 | ||
|
|
1c5506f4d1 | ||
|
|
31106c66d5 | ||
|
|
bf33a5ce5d | ||
|
|
a9f53ec416 | ||
|
|
64f819e0db | ||
|
|
e8816d61b0 | ||
|
|
42713843f3 | ||
|
|
db8e23c8dc | ||
|
|
c94430aabe | ||
|
|
e8f74d0147 | ||
|
|
cda3be805b | ||
|
|
372ce67ce6 | ||
|
|
62cae0d32c | ||
|
|
6be92aa41d | ||
|
|
d166fe49ec | ||
|
|
d5f2b32fe9 | ||
|
|
be71858b4a | ||
|
|
8c121869ee | ||
|
|
bfc761aac5 | ||
|
|
17f6507d0c | ||
|
|
4fa4dd7b6c | ||
|
|
69be00067a | ||
|
|
7af0091de2 | ||
|
|
cd6bac1111 | ||
|
|
62578b2389 | ||
|
|
0a791bba01 | ||
|
|
accd86edf1 | ||
|
|
8f3c06bc86 | ||
|
|
7baeaaee02 | ||
|
|
8f05de6860 | ||
|
|
05e0c5528e | ||
|
|
91ef08595c | ||
|
|
04459b51da | ||
|
|
5903764395 | ||
|
|
51ac869650 | ||
|
|
ce4a49fbd2 | ||
|
|
ca252d8e64 | ||
|
|
850d125c82 | ||
|
|
4b619eadc0 | ||
|
|
0fc63385a5 | ||
|
|
b5c0953c8b | ||
|
|
9deaf19d4c | ||
|
|
ac239ffe4d | ||
|
|
84ac063445 | ||
|
|
904311b124 | ||
|
|
c1562872a0 | ||
|
|
503d5ff946 | ||
|
|
271fa4c9ad | ||
|
|
a57902dd07 | ||
|
|
9336a33e47 | ||
|
|
fe034807d7 | ||
|
|
f08adc7b78 | ||
|
|
e767b3f4b7 | ||
|
|
c874f4f3e2 | ||
|
|
cb3fb984ec | ||
|
|
51882a595a | ||
|
|
8d5f8454c4 | ||
|
|
a036830009 | ||
|
|
76c6ffeb51 | ||
|
|
92c9de7ca9 | ||
|
|
e752340a16 | ||
|
|
3f7d7d4347 | ||
|
|
30651f1ca7 | ||
|
|
a04fa51c37 | ||
|
|
5c5a4ca787 | ||
|
|
62fe797b93 | ||
|
|
1ee2f17b28 | ||
|
|
9e714af5c2 | ||
|
|
74e18a2861 | ||
|
|
cd9b3c1386 | ||
|
|
40a7fc5d02 | ||
|
|
49ccb8fd75 | ||
|
|
5c3923f534 | ||
|
|
7a94684cec | ||
|
|
893ccf3331 | ||
|
|
b5f354da75 | ||
|
|
b128be0c13 | ||
|
|
a0fae9b514 | ||
|
|
04fc51b873 | ||
|
|
2cb45d18c9 | ||
|
|
495f59f36b | ||
|
|
21da644555 | ||
|
|
02e7bd6f3e | ||
|
|
254795261d | ||
|
|
53f0cd144c | ||
|
|
679864dbaa | ||
|
|
c6acc85d7d | ||
|
|
5ea2426ea4 | ||
|
|
1144783f4d | ||
|
|
d68bc56d1b | ||
|
|
5170254473 | ||
|
|
84353e1a05 | ||
|
|
62d74c1d84 | ||
|
|
654f64aaf7 | ||
|
|
150af5b65d | ||
|
|
3740636136 | ||
|
|
d40deae7ef | ||
|
|
669ed24249 | ||
|
|
ae341eae08 | ||
|
|
241ff7671d | ||
|
|
b7492f0706 | ||
|
|
fcd076d057 | ||
|
|
b924d68c51 | ||
|
|
4713a42829 | ||
|
|
9399907bfc | ||
|
|
2a84fd6751 | ||
|
|
4efd3d0428 | ||
|
|
535ae01dee | ||
|
|
b04b59b0a9 | ||
|
|
92025ce415 | ||
|
|
4893b01f38 | ||
|
|
0f8436b92f | ||
|
|
5e4aa44f2a | ||
|
|
0290355cfc | ||
|
|
64360fc3fe | ||
|
|
dd07cb6312 | ||
|
|
46e5aae8e4 | ||
|
|
626b6d93a8 | ||
|
|
e570030319 | ||
|
|
97d3a34abc | ||
|
|
848c441419 | ||
|
|
5b7e52a94e | ||
|
|
77e3394638 | ||
|
|
93721350c6 | ||
|
|
1bc6eca55c | ||
|
|
29560e3a4a | ||
|
|
03cc76ab07 | ||
|
|
dade1d0403 | ||
|
|
8a3feab3d0 | ||
|
|
7864bbb87b | ||
|
|
d112d87b80 | ||
|
|
2f8fcabce4 | ||
|
|
0023560588 | ||
|
|
0bc14acde9 | ||
|
|
3f3956edd6 | ||
|
|
93755da2e8 | ||
|
|
0657222ea7 | ||
|
|
ca352826ca | ||
|
|
3cbd99042b | ||
|
|
2f102acdc2 | ||
|
|
b76811d152 | ||
|
|
2335935a3e | ||
|
|
c324fe03d3 | ||
|
|
d5bc83a5a1 | ||
|
|
e2baa449f5 | ||
|
|
51b88eb065 | ||
|
|
b0d95bb7df | ||
|
|
8bf8c891ab | ||
|
|
fcf919c70b | ||
|
|
f0f3d32d31 | ||
|
|
444e8f17b6 | ||
|
|
3aabf44b4e | ||
|
|
c11fcc3c8e | ||
|
|
52519c9af8 | ||
|
|
2483cf9499 | ||
|
|
634e21b13c | ||
|
|
aad933b9eb | ||
|
|
9356f2964e | ||
|
|
aae70ead3b | ||
|
|
8a482dce10 | ||
|
|
9fdf46889c | ||
|
|
40ca9dc04c | ||
|
|
74937f2208 | ||
|
|
c02ab56477 | ||
|
|
3f99b9680f | ||
|
|
b08c39037d | ||
|
|
f7cc3c00da | ||
|
|
ac4286935a | ||
|
|
90a0b2e4a8 | ||
|
|
9b22c48d27 | ||
|
|
9c9dad5da0 | ||
|
|
e986f74025 | ||
|
|
deb2e81b21 | ||
|
|
0f8b322892 | ||
|
|
23c7de3ecd | ||
|
|
ad144a6240 | ||
|
|
debfef0388 | ||
|
|
bb85c9f8c8 | ||
|
|
25ab1cdbf9 | ||
|
|
44b8a9ddc9 | ||
|
|
c388ff9550 | ||
|
|
321d1680e6 | ||
|
|
6c0e9f0fed | ||
|
|
99349ecb30 | ||
|
|
258bbbd9cf | ||
|
|
1094a07fc6 | ||
|
|
14a5671645 | ||
|
|
5a41c66953 | ||
|
|
af605fce4c | ||
|
|
f8ef6c69fe | ||
|
|
e1848f44f7 | ||
|
|
825bd39dda | ||
|
|
b99905dfe8 | ||
|
|
a4bf40a2c1 | ||
|
|
711998b048 | ||
|
|
132e044c07 | ||
|
|
8ecc307451 | ||
|
|
c85c29989f | ||
|
|
95726e0f21 | ||
|
|
adc15992e9 | ||
|
|
58d83a2a80 | ||
|
|
5461cd4502 | ||
|
|
478465a65b |
1094
.github/workflows/release.yml
vendored
1094
.github/workflows/release.yml
vendored
File diff suppressed because it is too large
Load Diff
1232
.github/workflows/test-release.yaml
vendored
1232
.github/workflows/test-release.yaml
vendored
File diff suppressed because it is too large
Load Diff
1
.github/workflows/test.telemetry.yaml
vendored
1
.github/workflows/test.telemetry.yaml
vendored
@@ -17,5 +17,6 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Telemetry && npm install && npm run test
|
||||
|
||||
|
||||
37
APIReference/package-lock.json
generated
37
APIReference/package-lock.json
generated
@@ -29,25 +29,25 @@
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@hcaptcha/react-hcaptcha": "^1.14.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@@ -86,21 +86,21 @@
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.16.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
@@ -115,7 +115,7 @@
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
@@ -3107,10 +3107,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
|
||||
30
Accounts/package-lock.json
generated
30
Accounts/package-lock.json
generated
@@ -33,25 +33,25 @@
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@hcaptcha/react-hcaptcha": "^1.14.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@@ -90,21 +90,21 @@
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.16.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
@@ -119,7 +119,7 @@
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
|
||||
@@ -7,10 +7,20 @@ import {
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import ModelForm, { FormType } from "Common/UI/Components/Forms/ModelForm";
|
||||
import ModelForm, {
|
||||
FormType,
|
||||
ModelField,
|
||||
} from "Common/UI/Components/Forms/ModelForm";
|
||||
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import { DASHBOARD_URL } from "Common/UI/Config";
|
||||
import Captcha from "Common/UI/Components/Captcha/Captcha";
|
||||
import {
|
||||
DASHBOARD_URL,
|
||||
CAPTCHA_ENABLED,
|
||||
CAPTCHA_SITE_KEY,
|
||||
} from "Common/UI/Config";
|
||||
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
|
||||
import EditionLabel from "Common/UI/Components/EditionLabel/EditionLabel";
|
||||
import UiAnalytics from "Common/UI/Utils/Analytics";
|
||||
@@ -73,6 +83,84 @@ const LoginPage: () => JSX.Element = () => {
|
||||
const [twofactorAuthError, setTwoFactorAuthError] =
|
||||
React.useState<string>("");
|
||||
|
||||
const isCaptchaEnabled: boolean =
|
||||
CAPTCHA_ENABLED && Boolean(CAPTCHA_SITE_KEY);
|
||||
|
||||
const [shouldResetCaptcha, setShouldResetCaptcha] =
|
||||
React.useState<boolean>(false);
|
||||
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
|
||||
|
||||
const handleCaptchaReset: () => void = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => {
|
||||
return current + 1;
|
||||
});
|
||||
}, []);
|
||||
let loginFields: Array<ModelField<User>> = [
|
||||
{
|
||||
field: {
|
||||
email: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Email,
|
||||
placeholder: "jeff@example.com",
|
||||
required: true,
|
||||
disabled: Boolean(initialValues && initialValues["email"]),
|
||||
title: "Email",
|
||||
dataTestId: "email",
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
password: true,
|
||||
},
|
||||
title: "Password",
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 6,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
sideLink: {
|
||||
text: "Forgot password?",
|
||||
url: new Route("/accounts/forgot-password"),
|
||||
openLinkInNewTab: false,
|
||||
},
|
||||
dataTestId: "password",
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (isCaptchaEnabled) {
|
||||
loginFields = loginFields.concat([
|
||||
{
|
||||
overrideField: {
|
||||
captchaToken: true,
|
||||
},
|
||||
overrideFieldKey: "captchaToken",
|
||||
fieldType: FormFieldSchemaType.CustomComponent,
|
||||
title: "Human Verification",
|
||||
description:
|
||||
"Complete the captcha challenge so we know you're not a bot.",
|
||||
required: true,
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
getCustomElement: (
|
||||
_values: FormValues<User>,
|
||||
customProps: CustomElementProps,
|
||||
) => {
|
||||
return (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (Navigation.getQueryStringByName("email")) {
|
||||
setInitialValues({
|
||||
@@ -228,45 +316,41 @@ const LoginPage: () => JSX.Element = () => {
|
||||
modelType={User}
|
||||
id="login-form"
|
||||
name="Login"
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
email: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Email,
|
||||
placeholder: "jeff@example.com",
|
||||
required: true,
|
||||
disabled: Boolean(initialValues && initialValues["email"]),
|
||||
title: "Email",
|
||||
dataTestId: "email",
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
password: true,
|
||||
},
|
||||
title: "Password",
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 6,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
sideLink: {
|
||||
text: "Forgot password?",
|
||||
url: new Route("/accounts/forgot-password"),
|
||||
openLinkInNewTab: false,
|
||||
},
|
||||
dataTestId: "password",
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
fields={loginFields}
|
||||
createOrUpdateApiUrl={apiUrl}
|
||||
formType={FormType.Create}
|
||||
submitButtonText={"Login"}
|
||||
onBeforeCreate={(data: User) => {
|
||||
onBeforeCreate={(data: User, miscDataProps: JSONObject) => {
|
||||
if (isCaptchaEnabled) {
|
||||
const captchaToken: string | undefined = (
|
||||
miscDataProps["captchaToken"] as string | undefined
|
||||
)
|
||||
?.toString()
|
||||
.trim();
|
||||
|
||||
if (!captchaToken) {
|
||||
throw new Error(
|
||||
"Please complete the captcha challenge before signing in.",
|
||||
);
|
||||
}
|
||||
|
||||
miscDataProps["captchaToken"] = captchaToken;
|
||||
setShouldResetCaptcha(true);
|
||||
}
|
||||
|
||||
setInitialValues(User.toJSON(data, User));
|
||||
return Promise.resolve(data);
|
||||
}}
|
||||
onLoadingChange={(loading: boolean) => {
|
||||
if (!isCaptchaEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loading && shouldResetCaptcha) {
|
||||
setShouldResetCaptcha(false);
|
||||
handleCaptchaReset();
|
||||
}
|
||||
}}
|
||||
onSuccess={(
|
||||
value: User | JSONObject,
|
||||
miscData: JSONObject | undefined,
|
||||
|
||||
@@ -4,12 +4,22 @@ import URL from "Common/Types/API/URL";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ModelForm, { FormType } from "Common/UI/Components/Forms/ModelForm";
|
||||
import Fields from "Common/UI/Components/Forms/Types/Fields";
|
||||
import ModelForm, {
|
||||
FormType,
|
||||
ModelField,
|
||||
} from "Common/UI/Components/Forms/ModelForm";
|
||||
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import { BILLING_ENABLED, DASHBOARD_URL } from "Common/UI/Config";
|
||||
import Captcha from "Common/UI/Components/Captcha/Captcha";
|
||||
import {
|
||||
BILLING_ENABLED,
|
||||
DASHBOARD_URL,
|
||||
CAPTCHA_ENABLED,
|
||||
CAPTCHA_SITE_KEY,
|
||||
} from "Common/UI/Config";
|
||||
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
|
||||
import BaseAPI from "Common/UI/Utils/API/API";
|
||||
import UiAnalytics from "Common/UI/Utils/Analytics";
|
||||
@@ -36,6 +46,19 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
undefined,
|
||||
);
|
||||
|
||||
const isCaptchaEnabled: boolean =
|
||||
CAPTCHA_ENABLED && Boolean(CAPTCHA_SITE_KEY);
|
||||
|
||||
const [shouldResetCaptcha, setShouldResetCaptcha] =
|
||||
React.useState<boolean>(false);
|
||||
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
|
||||
|
||||
const handleCaptchaReset: () => void = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => {
|
||||
return current + 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (UserUtil.isLoggedIn()) {
|
||||
Navigation.navigate(DASHBOARD_URL);
|
||||
}
|
||||
@@ -93,7 +116,7 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
let formFields: Fields<User> = [
|
||||
let formFields: Array<ModelField<User>> = [
|
||||
{
|
||||
field: {
|
||||
email: true,
|
||||
@@ -183,6 +206,39 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
},
|
||||
]);
|
||||
|
||||
if (isCaptchaEnabled) {
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
overrideField: {
|
||||
captchaToken: true,
|
||||
},
|
||||
overrideFieldKey: "captchaToken",
|
||||
fieldType: FormFieldSchemaType.CustomComponent,
|
||||
title: "Human Verification",
|
||||
description:
|
||||
"Complete the captcha challenge so we know you're not a bot.",
|
||||
required: true,
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
getCustomElement: (
|
||||
_values: FormValues<User>,
|
||||
customProps: CustomElementProps,
|
||||
) => {
|
||||
return (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
@@ -222,7 +278,27 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
maxPrimaryButtonWidth={true}
|
||||
fields={formFields}
|
||||
createOrUpdateApiUrl={apiUrl}
|
||||
onBeforeCreate={(item: User): Promise<User> => {
|
||||
onBeforeCreate={(
|
||||
item: User,
|
||||
miscDataProps: JSONObject,
|
||||
): Promise<User> => {
|
||||
if (isCaptchaEnabled) {
|
||||
const captchaToken: string | undefined = (
|
||||
miscDataProps["captchaToken"] as string | undefined
|
||||
)
|
||||
?.toString()
|
||||
.trim();
|
||||
|
||||
if (!captchaToken) {
|
||||
throw new Error(
|
||||
"Please complete the captcha challenge before signing up.",
|
||||
);
|
||||
}
|
||||
|
||||
miscDataProps["captchaToken"] = captchaToken;
|
||||
setShouldResetCaptcha(true);
|
||||
}
|
||||
|
||||
const utmParams: Dictionary<string> = UserUtil.getUtmParams();
|
||||
|
||||
if (utmParams && Object.keys(utmParams).length > 0) {
|
||||
@@ -240,6 +316,16 @@ const RegisterPage: () => JSX.Element = () => {
|
||||
}}
|
||||
formType={FormType.Create}
|
||||
submitButtonText={"Sign Up"}
|
||||
onLoadingChange={(loading: boolean) => {
|
||||
if (!isCaptchaEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loading && shouldResetCaptcha) {
|
||||
setShouldResetCaptcha(false);
|
||||
handleCaptchaReset();
|
||||
}
|
||||
}}
|
||||
onSuccess={(value: User, miscData: JSONObject | undefined) => {
|
||||
if (value && value.email) {
|
||||
UiAnalytics.userAuth(value.email);
|
||||
|
||||
30
AdminDashboard/package-lock.json
generated
30
AdminDashboard/package-lock.json
generated
@@ -32,25 +32,25 @@
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@hcaptcha/react-hcaptcha": "^1.14.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@@ -89,21 +89,21 @@
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.16.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
@@ -118,7 +118,7 @@
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<link rel="icon" type="image/png" sizes="194x194" href="/favicon-194x194.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#121212">
|
||||
<meta name="theme-color" content="#121212">
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "OneUptime Account",
|
||||
"short_name": "OneUptime",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/accounts/assets/img/favicons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"theme_color": "#121212",
|
||||
"background_color": "#121212",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./assets/img/favicons/favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import UserView from "./Pages/Users/View/Index";
|
||||
import UserDelete from "./Pages/Users/View/Delete";
|
||||
import UserSettings from "./Pages/Users/View/Settings";
|
||||
import ProjectView from "./Pages/Projects/View/Index";
|
||||
import ProjectDelete from "./Pages/Projects/View/Delete";
|
||||
|
||||
@@ -71,6 +72,11 @@ const App: () => JSX.Element = () => {
|
||||
element={<UserView />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_SETTINGS]?.toString() || ""}
|
||||
element={<UserSettings />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_DELETE]?.toString() || ""}
|
||||
element={<UserDelete />}
|
||||
|
||||
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import SideMenuComponent from "./SideMenu";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import ModelPage from "Common/UI/Components/Page/ModelPage";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
|
||||
const UserSettings: FunctionComponent = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<ModelPage<User>
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_VIEW] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenuComponent modelId={modelId} />}
|
||||
>
|
||||
<CardModelDetail<User>
|
||||
name="user-master-admin-settings"
|
||||
cardProps={{
|
||||
title: "Master Admin Access",
|
||||
description:
|
||||
"Grant or revoke master admin access for this user. Master admins can manage every project and workspace.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Update Access"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
description:
|
||||
"Enable to give this user full access to the entire platform.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: User,
|
||||
id: "user-master-admin-settings-detail",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
fieldType: FieldType.Boolean,
|
||||
placeholder: "No",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
@@ -30,6 +30,18 @@ const SideMenuComponent: FunctionComponent<SideMenuProps> = (
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: props.modelId,
|
||||
},
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
|
||||
@@ -6,6 +6,7 @@ enum PageMap {
|
||||
|
||||
USERS = "USERS",
|
||||
USER_VIEW = "USER_VIEW",
|
||||
USER_SETTINGS = "USER_SETTINGS",
|
||||
USER_DELETE = "USER_DELETE",
|
||||
|
||||
PROJECTS = "PROJECTS",
|
||||
|
||||
@@ -18,6 +18,9 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.USERS]: new Route(`/admin/users`),
|
||||
[PageMap.USER_VIEW]: new Route(`/admin/users/${RouteParams.ModelID}`),
|
||||
[PageMap.USER_SETTINGS]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/settings`,
|
||||
),
|
||||
[PageMap.USER_DELETE]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/delete`,
|
||||
),
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<!-- End Google Tag Manager -->
|
||||
<% } %>
|
||||
|
||||
<link rel="manifest" href="/admin/assets/img/favicons/ma">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/admin/assets/img/favicons/apple-touch-icon.png">
|
||||
<link rel="shortcut icon" href="/admin/assets/img/favicons/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/admin/assets/img/favicons/favicon-32x32.png">
|
||||
@@ -75,20 +74,6 @@
|
||||
|
||||
<title>OneUptime Admin Dashboard</title>
|
||||
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
</head>
|
||||
<body class="h-full bg-gray-50">
|
||||
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
|
||||
|
||||
@@ -29,12 +29,18 @@ import UserCallAPI from "Common/Server/API/UserCallAPI";
|
||||
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
|
||||
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
|
||||
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
import IncidentInternalNoteAPI from "Common/Server/API/IncidentInternalNoteAPI";
|
||||
import IncidentPublicNoteAPI from "Common/Server/API/IncidentPublicNoteAPI";
|
||||
import ScheduledMaintenanceInternalNoteAPI from "Common/Server/API/ScheduledMaintenanceInternalNoteAPI";
|
||||
import ScheduledMaintenancePublicNoteAPI from "Common/Server/API/ScheduledMaintenancePublicNoteAPI";
|
||||
import IncidentAPI from "Common/Server/API/IncidentAPI";
|
||||
// User Notification methods.
|
||||
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
|
||||
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
|
||||
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
|
||||
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
|
||||
import UserPushAPI from "Common/Server/API/UserPushAPI";
|
||||
import UserAPI from "Common/Server/API/UserAPI";
|
||||
import ApiKeyPermissionService, {
|
||||
Service as ApiKeyPermissionServiceType,
|
||||
} from "Common/Server/Services/ApiKeyPermissionService";
|
||||
@@ -60,9 +66,7 @@ import EmailVerificationTokenService, {
|
||||
import AlertCustomFieldService, {
|
||||
Service as AlertCustomFieldServiceType,
|
||||
} from "Common/Server/Services/AlertCustomFieldService";
|
||||
import AlertInternalNoteService, {
|
||||
Service as AlertInternalNoteServiceType,
|
||||
} from "Common/Server/Services/AlertInternalNoteService";
|
||||
import AlertInternalNoteAPI from "Common/Server/API/AlertInternalNoteAPI";
|
||||
import AlertNoteTemplateService, {
|
||||
Service as AlertNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/AlertNoteTemplateService";
|
||||
@@ -93,9 +97,6 @@ import AlertStateTimelineService, {
|
||||
import IncidentCustomFieldService, {
|
||||
Service as IncidentCustomFieldServiceType,
|
||||
} from "Common/Server/Services/IncidentCustomFieldService";
|
||||
import IncidentInternalNoteService, {
|
||||
Service as IncidentInternalNoteServiceType,
|
||||
} from "Common/Server/Services/IncidentInternalNoteService";
|
||||
import IncidentNoteTemplateService, {
|
||||
Service as IncidentNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/IncidentNoteTemplateService";
|
||||
@@ -111,12 +112,6 @@ import IncidentOwnerTeamService, {
|
||||
import IncidentOwnerUserService, {
|
||||
Service as IncidentOwnerUserServiceType,
|
||||
} from "Common/Server/Services/IncidentOwnerUserService";
|
||||
import IncidentPublicNoteService, {
|
||||
Service as IncidentPublicNoteServiceType,
|
||||
} from "Common/Server/Services/IncidentPublicNoteService";
|
||||
import IncidentService, {
|
||||
Service as IncidentServiceType,
|
||||
} from "Common/Server/Services/IncidentService";
|
||||
import IncidentSeverityService, {
|
||||
Service as IncidentSeverityServiceType,
|
||||
} from "Common/Server/Services/IncidentSeverityService";
|
||||
@@ -232,9 +227,6 @@ import ResellerService, {
|
||||
import ScheduledMaintenanceCustomFieldService, {
|
||||
Service as ScheduledMaintenanceCustomFieldServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceCustomFieldService";
|
||||
import ScheduledMaintenanceInternalNoteService, {
|
||||
Service as ScheduledMaintenanceInternalNoteServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceInternalNoteService";
|
||||
import ScheduledMaintenanceNoteTemplateService, {
|
||||
Service as ScheduledMaintenanceNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceNoteTemplateService";
|
||||
@@ -244,9 +236,6 @@ import ScheduledMaintenanceOwnerTeamService, {
|
||||
import ScheduledMaintenanceOwnerUserService, {
|
||||
Service as ScheduledMaintenanceOwnerUserServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceOwnerUserService";
|
||||
import ScheduledMaintenancePublicNoteService, {
|
||||
Service as ScheduledMaintenancePublicNoteServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenancePublicNoteService";
|
||||
import ScheduledMaintenanceService, {
|
||||
Service as ScheduledMaintenanceServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceService";
|
||||
@@ -293,9 +282,7 @@ import PushNotificationLogService, {
|
||||
import SpanService, {
|
||||
SpanService as SpanServiceType,
|
||||
} from "Common/Server/Services/SpanService";
|
||||
import StatusPageAnnouncementService, {
|
||||
Service as StatusPageAnnouncementServiceType,
|
||||
} from "Common/Server/Services/StatusPageAnnouncementService";
|
||||
import StatusPageAnnouncementAPI from "Common/Server/API/StatusPageAnnouncementAPI";
|
||||
import StatusPageCustomFieldService, {
|
||||
Service as StatusPageCustomFieldServiceType,
|
||||
} from "Common/Server/Services/StatusPageCustomFieldService";
|
||||
@@ -353,9 +340,6 @@ import UserNotificationSettingService, {
|
||||
import UserOnCallLogService, {
|
||||
Service as UserNotificationLogServiceType,
|
||||
} from "Common/Server/Services/UserOnCallLogService";
|
||||
import UserService, {
|
||||
Service as UserServiceType,
|
||||
} from "Common/Server/Services/UserService";
|
||||
import WorkflowLogService, {
|
||||
Service as WorkflowLogServiceType,
|
||||
} from "Common/Server/Services/WorkflowLogService";
|
||||
@@ -400,7 +384,6 @@ import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
|
||||
import Alert from "Common/Models/DatabaseModels/Alert";
|
||||
import AlertCustomField from "Common/Models/DatabaseModels/AlertCustomField";
|
||||
import AlertInternalNote from "Common/Models/DatabaseModels/AlertInternalNote";
|
||||
import AlertNoteTemplate from "Common/Models/DatabaseModels/AlertNoteTemplate";
|
||||
import AlertOwnerTeam from "Common/Models/DatabaseModels/AlertOwnerTeam";
|
||||
import AlertOwnerUser from "Common/Models/DatabaseModels/AlertOwnerUser";
|
||||
@@ -408,14 +391,11 @@ import AlertSeverity from "Common/Models/DatabaseModels/AlertSeverity";
|
||||
import AlertState from "Common/Models/DatabaseModels/AlertState";
|
||||
import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline";
|
||||
|
||||
import Incident from "Common/Models/DatabaseModels/Incident";
|
||||
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
|
||||
import IncidentInternalNote from "Common/Models/DatabaseModels/IncidentInternalNote";
|
||||
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
|
||||
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
|
||||
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
|
||||
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
|
||||
import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote";
|
||||
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
|
||||
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
||||
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
|
||||
@@ -450,11 +430,9 @@ import PromoCode from "Common/Models/DatabaseModels/PromoCode";
|
||||
import Reseller from "Common/Models/DatabaseModels/Reseller";
|
||||
import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance";
|
||||
import ScheduledMaintenanceCustomField from "Common/Models/DatabaseModels/ScheduledMaintenanceCustomField";
|
||||
import ScheduledMaintenanceInternalNote from "Common/Models/DatabaseModels/ScheduledMaintenanceInternalNote";
|
||||
import ScheduledMaintenanceNoteTemplate from "Common/Models/DatabaseModels/ScheduledMaintenanceNoteTemplate";
|
||||
import ScheduledMaintenanceOwnerTeam from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam";
|
||||
import ScheduledMaintenanceOwnerUser from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerUser";
|
||||
import ScheduledMaintenancePublicNote from "Common/Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
||||
import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMaintenanceState";
|
||||
import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
||||
import ServiceCatalog from "Common/Models/DatabaseModels/ServiceCatalog";
|
||||
@@ -463,7 +441,6 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
|
||||
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
|
||||
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
|
||||
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
|
||||
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
|
||||
// Custom Fields API
|
||||
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
|
||||
import StatusPageFooterLink from "Common/Models/DatabaseModels/StatusPageFooterLink";
|
||||
@@ -482,7 +459,6 @@ import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
|
||||
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
|
||||
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
|
||||
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationRule";
|
||||
import UserNotificationSetting from "Common/Models/DatabaseModels/UserNotificationSetting";
|
||||
import UserOnCallLog from "Common/Models/DatabaseModels/UserOnCallLog";
|
||||
@@ -848,10 +824,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertInternalNote, AlertInternalNoteServiceType>(
|
||||
AlertInternalNote,
|
||||
AlertInternalNoteService,
|
||||
).getRouter(),
|
||||
new AlertInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
@@ -1048,10 +1021,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<StatusPageAnnouncement, StatusPageAnnouncementServiceType>(
|
||||
StatusPageAnnouncement,
|
||||
StatusPageAnnouncementService,
|
||||
).getRouter(),
|
||||
new StatusPageAnnouncementAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
@@ -1295,13 +1265,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<Incident, IncidentServiceType>(
|
||||
Incident,
|
||||
IncidentService,
|
||||
).getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new IncidentAPI().getRouter());
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
@@ -1724,40 +1688,22 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteServiceType
|
||||
>(
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteService,
|
||||
).getRouter(),
|
||||
new ScheduledMaintenancePublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteServiceType
|
||||
>(
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteService,
|
||||
).getRouter(),
|
||||
new ScheduledMaintenanceInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentPublicNote, IncidentPublicNoteServiceType>(
|
||||
IncidentPublicNote,
|
||||
IncidentPublicNoteService,
|
||||
).getRouter(),
|
||||
new IncidentPublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentInternalNote, IncidentInternalNoteServiceType>(
|
||||
IncidentInternalNote,
|
||||
IncidentInternalNoteService,
|
||||
).getRouter(),
|
||||
new IncidentInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
@@ -1874,10 +1820,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, TelemetryAPI);
|
||||
|
||||
//attach api's
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<User, UserServiceType>(User, UserService).getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserAPI().getRouter());
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -25,24 +25,71 @@ import EmailVerificationTokenService from "Common/Server/Services/EmailVerificat
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
|
||||
import UserSessionService, {
|
||||
SessionMetadata,
|
||||
} from "Common/Server/Services/UserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import CaptchaUtil from "Common/Server/Utils/Captcha";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import TotpAuth from "Common/Server/Utils/TotpAuth";
|
||||
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import UserSession from "Common/Models/DatabaseModels/UserSession";
|
||||
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
|
||||
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
|
||||
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
|
||||
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
type FinalizeUserLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
user: User;
|
||||
isGlobalLogin: boolean;
|
||||
};
|
||||
|
||||
const finalizeUserLogin: (
|
||||
data: FinalizeUserLoginInput,
|
||||
) => Promise<SessionMetadata> = async (
|
||||
data: FinalizeUserLoginInput,
|
||||
): Promise<SessionMetadata> => {
|
||||
const { req, res, user, isGlobalLogin } = data;
|
||||
|
||||
const sessionMetadata: SessionMetadata =
|
||||
await UserSessionService.createSession({
|
||||
userId: user.id!,
|
||||
isGlobalLogin,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
isGlobalLogin,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return sessionMetadata;
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/signup",
|
||||
async (
|
||||
@@ -61,6 +108,16 @@ router.post(
|
||||
);
|
||||
}
|
||||
|
||||
const miscDataProps: JSONObject =
|
||||
(req.body["miscDataProps"] as JSONObject) || {};
|
||||
|
||||
await CaptchaUtil.verifyCaptcha({
|
||||
token:
|
||||
(miscDataProps["captchaToken"] as string | undefined) ||
|
||||
(req.body["captchaToken"] as string | undefined),
|
||||
remoteIp: getClientIp(req) || null,
|
||||
});
|
||||
|
||||
const data: JSONObject = req.body["data"];
|
||||
|
||||
/* Creating a type that is a partial of the TBaseModel type. */
|
||||
@@ -185,9 +242,9 @@ router.post(
|
||||
if (savedUser) {
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(savedUser.id!);
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
await finalizeUserLogin({
|
||||
req,
|
||||
res,
|
||||
user: savedUser,
|
||||
isGlobalLogin: true,
|
||||
});
|
||||
@@ -487,6 +544,127 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/refresh-token",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
|
||||
if (!refreshToken) {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException(
|
||||
"Refresh token missing. Please login again.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const session: UserSession | null =
|
||||
await UserSessionService.findActiveSessionByRefreshToken(refreshToken);
|
||||
|
||||
if (!session || !session.id) {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.refreshTokenExpiresAt &&
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "Refresh token expired",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.userId) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "Session missing user",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
const user: User | null = await UserService.findOneById({
|
||||
id: session.userId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
isMasterAdmin: true,
|
||||
profilePictureId: true,
|
||||
timezone: true,
|
||||
enableTwoFactorAuth: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await UserSessionService.revokeSessionById(session.id, {
|
||||
reason: "User not found",
|
||||
});
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Account no longer exists."),
|
||||
);
|
||||
}
|
||||
|
||||
const additionalInfo: JSONObject = (session.additionalInfo ||
|
||||
{}) as JSONObject;
|
||||
const isGlobalLogin: boolean =
|
||||
typeof additionalInfo["isGlobalLogin"] === "boolean"
|
||||
? (additionalInfo["isGlobalLogin"] as boolean)
|
||||
: true;
|
||||
|
||||
const renewedSession: SessionMetadata =
|
||||
await UserSessionService.renewSessionWithNewRefreshToken({
|
||||
session,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
isGlobalLogin,
|
||||
sessionId: renewedSession.session.id!,
|
||||
refreshToken: renewedSession.refreshToken,
|
||||
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/logout",
|
||||
async (
|
||||
@@ -495,6 +673,15 @@ router.post(
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req);
|
||||
|
||||
if (refreshToken) {
|
||||
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
|
||||
reason: "User logout",
|
||||
});
|
||||
}
|
||||
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
@@ -628,6 +815,18 @@ const login: LoginFunction = async (options: {
|
||||
const verifyWebAuthn: boolean = options.verifyWebAuthn;
|
||||
|
||||
try {
|
||||
const miscDataProps: JSONObject =
|
||||
(req.body["miscDataProps"] as JSONObject) || {};
|
||||
|
||||
if (!verifyTotpAuth && !verifyWebAuthn) {
|
||||
await CaptchaUtil.verifyCaptcha({
|
||||
token:
|
||||
(miscDataProps["captchaToken"] as string | undefined) ||
|
||||
(req.body["captchaToken"] as string | undefined),
|
||||
remoteIp: getClientIp(req) || null,
|
||||
});
|
||||
}
|
||||
|
||||
const data: JSONObject = req.body["data"];
|
||||
|
||||
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
|
||||
@@ -788,8 +987,9 @@ const login: LoginFunction = async (options: {
|
||||
if (alreadySavedUser.password.toString() === user.password!.toString()) {
|
||||
logger.info("User logged in: " + alreadySavedUser.email?.toString());
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
await finalizeUserLogin({
|
||||
req,
|
||||
res,
|
||||
user: alreadySavedUser,
|
||||
isGlobalLogin: true,
|
||||
});
|
||||
|
||||
@@ -470,174 +470,181 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
// Update User - PUT /scim/v2/Users/{id}
|
||||
router.put(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
const handleUserUpdate: (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => Promise<void> = async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
|
||||
);
|
||||
logger.debug(
|
||||
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
logger.debug(
|
||||
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Check if user exists and is part of the project
|
||||
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: new ObjectID(userId),
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!projectUser || !projectUser.user) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException("User not found or not part of this project");
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const name: string = parseNameFromSCIM(scimUser);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
|
||||
);
|
||||
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
|
||||
// Handle user deactivation by removing from teams
|
||||
if (active === false && !scimConfig.enablePushGroups) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user marked as inactive, removing from teams`,
|
||||
);
|
||||
await handleUserTeamOperations(
|
||||
"remove",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
`SCIM Update user - user successfully removed from teams due to deactivation`,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle user activation by adding to teams
|
||||
if (active === true && !scimConfig.enablePushGroups) {
|
||||
logger.debug(`SCIM Update user - user marked as active, adding to teams`);
|
||||
await handleUserTeamOperations(
|
||||
"add",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
`SCIM Update user - user successfully added to teams due to activation`,
|
||||
);
|
||||
}
|
||||
|
||||
if (email || name) {
|
||||
const updateData: any = {};
|
||||
if (email) {
|
||||
updateData.email = new Email(email);
|
||||
}
|
||||
if (name) {
|
||||
updateData.name = new Name(name);
|
||||
}
|
||||
|
||||
// Check if user exists and is part of the project
|
||||
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: new ObjectID(userId),
|
||||
},
|
||||
logger.debug(
|
||||
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
|
||||
await UserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
data: updateData,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(`SCIM Update user - user updated successfully`);
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: User | null = await UserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!projectUser || !projectUser.user) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this project",
|
||||
if (updatedUser) {
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const name: string = parseNameFromSCIM(scimUser);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
|
||||
);
|
||||
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
|
||||
// Handle user deactivation by removing from teams
|
||||
if (active === false && !scimConfig.enablePushGroups) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user marked as inactive, removing from teams`,
|
||||
);
|
||||
await handleUserTeamOperations(
|
||||
"remove",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
`SCIM Update user - user successfully removed from teams due to deactivation`,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle user activation by adding to teams
|
||||
if (active === true && !scimConfig.enablePushGroups) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user marked as active, adding to teams`,
|
||||
);
|
||||
await handleUserTeamOperations(
|
||||
"add",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
`SCIM Update user - user successfully added to teams due to activation`,
|
||||
);
|
||||
}
|
||||
|
||||
if (email || name) {
|
||||
const updateData: any = {};
|
||||
if (email) {
|
||||
updateData.email = new Email(email);
|
||||
}
|
||||
if (name) {
|
||||
updateData.name = new Name(name);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
|
||||
await UserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
data: updateData,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(`SCIM Update user - user updated successfully`);
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: User | null = await UserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (updatedUser) {
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - no updates made, returning existing user`,
|
||||
);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
projectUser.user,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
|
||||
logger.debug(`SCIM Update user - no updates made, returning existing user`);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
projectUser.user,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
|
||||
// Update User - PUT /scim/v2/Users/{id}
|
||||
router.put(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
handleUserUpdate,
|
||||
);
|
||||
|
||||
// Update User - PATCH /scim/v2/Users/{id}
|
||||
router.patch(
|
||||
"/scim/v2/:projectScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
handleUserUpdate,
|
||||
);
|
||||
|
||||
// Groups endpoint - GET /scim/v2/Groups
|
||||
|
||||
@@ -20,6 +20,9 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
|
||||
import ProjectSSOService from "Common/Server/Services/ProjectSsoService";
|
||||
import TeamMemberService from "Common/Server/Services/TeamMemberService";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import UserSessionService, {
|
||||
SessionMetadata,
|
||||
} from "Common/Server/Services/UserSessionService";
|
||||
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
|
||||
import Select from "Common/Server/Types/Database/Select";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
@@ -28,6 +31,9 @@ import Express, {
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
@@ -40,6 +46,8 @@ import Name from "Common/Types/Name";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
/*
|
||||
* This route is used to get the SSO config for the user.
|
||||
* when the user logs in from OneUptime and not from the IDP.
|
||||
@@ -539,15 +547,31 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
expressResponse: res,
|
||||
});
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
const sessionMetadata: SessionMetadata =
|
||||
await UserSessionService.createSession({
|
||||
userId: alreadySavedUser.id!,
|
||||
isGlobalLogin: false,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
additionalInfo: {
|
||||
projectId: projectId.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
CookieUtil.setUserCookie({
|
||||
expressResponse: res,
|
||||
user: alreadySavedUser,
|
||||
isGlobalLogin: false,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
|
||||
@@ -1,35 +1,145 @@
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import { FileRoute } from "Common/ServiceRoute";
|
||||
import { StatusPageApiRoute } from "Common/ServiceRoute";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import NotAuthenticatedException from "Common/Types/Exception/NotAuthenticatedException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import { EncryptionSecret } from "Common/Server/EnvironmentConfig";
|
||||
import MailService from "Common/Server/Services/MailService";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPageService from "Common/Server/Services/StatusPageService";
|
||||
import StatusPagePrivateUserSessionService, {
|
||||
SessionMetadata as StatusPageSessionMetadata,
|
||||
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "Common/Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
import { MASTER_PASSWORD_COOKIE_IDENTIFIER } from "Common/Types/StatusPage/MasterPassword";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
type MasterPasswordAuthInput = {
|
||||
req: ExpressRequest;
|
||||
res?: ExpressResponse;
|
||||
statusPageId: ObjectID;
|
||||
};
|
||||
|
||||
const hasValidMasterPasswordSession: (
|
||||
data: MasterPasswordAuthInput,
|
||||
) => boolean = (data: MasterPasswordAuthInput): boolean => {
|
||||
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
|
||||
data.req,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
return (
|
||||
payload["statusPageId"] === data.statusPageId.toString() &&
|
||||
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const respondWithMasterPasswordAccess: (
|
||||
data: MasterPasswordAuthInput & { res: ExpressResponse },
|
||||
) => boolean = (
|
||||
data: MasterPasswordAuthInput & { res: ExpressResponse },
|
||||
): boolean => {
|
||||
if (!hasValidMasterPasswordSession(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
return true;
|
||||
};
|
||||
|
||||
type FinalizeStatusPageLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
user: StatusPagePrivateUser;
|
||||
};
|
||||
|
||||
const finalizeStatusPageLogin: (data: FinalizeStatusPageLoginInput) => Promise<{
|
||||
sessionMetadata: StatusPageSessionMetadata;
|
||||
accessToken: string;
|
||||
}> = async (
|
||||
data: FinalizeStatusPageLoginInput,
|
||||
): Promise<{
|
||||
sessionMetadata: StatusPageSessionMetadata;
|
||||
accessToken: string;
|
||||
}> => {
|
||||
const { req, res, user } = data;
|
||||
|
||||
if (!user.projectId) {
|
||||
throw new BadDataException(
|
||||
"Status page user is missing associated projectId.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.statusPageId) {
|
||||
throw new BadDataException(
|
||||
"Status page user is missing associated statusPageId.",
|
||||
);
|
||||
}
|
||||
|
||||
const sessionMetadata: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.createSession({
|
||||
projectId: user.projectId,
|
||||
statusPageId: user.statusPageId,
|
||||
statusPagePrivateUserId: user.id!,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId: user.statusPageId,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return {
|
||||
sessionMetadata,
|
||||
accessToken,
|
||||
};
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/logout/:statuspageid",
|
||||
async (
|
||||
@@ -46,7 +156,21 @@ router.post(
|
||||
req.params["statuspageid"].toString(),
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId)); // remove the cookie.
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
|
||||
|
||||
if (refreshToken) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionByRefreshToken(
|
||||
refreshToken,
|
||||
{
|
||||
reason: "User logged out",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(res, CookieUtil.getRefreshTokenKey(statusPageId));
|
||||
CookieUtil.removeStatusPageMasterPasswordCookie(res, statusPageId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
@@ -55,6 +179,258 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/refresh-token/:statuspageid",
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const statusPageIdParam: string | undefined = req.params["statuspageid"];
|
||||
|
||||
if (!statusPageIdParam) {
|
||||
throw new BadDataException("Status Page ID is required.");
|
||||
}
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(statusPageIdParam.toString());
|
||||
|
||||
const refreshToken: string | undefined =
|
||||
CookieUtil.getRefreshTokenFromExpressRequest(req, statusPageId);
|
||||
|
||||
if (!refreshToken) {
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException(
|
||||
"Refresh token missing. Please login again.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const session: StatusPagePrivateUserSession | null =
|
||||
await StatusPagePrivateUserSessionService.findActiveSessionByRefreshToken(
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
if (!session || !session.id || !session.statusPageId) {
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (session.statusPageId.toString() !== statusPageId.toString()) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Status page mismatch",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.refreshTokenExpiresAt &&
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Refresh token expired",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.statusPagePrivateUserId) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "Session missing user",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Session expired. Please login again."),
|
||||
);
|
||||
}
|
||||
|
||||
const user: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: session.statusPagePrivateUserId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await StatusPagePrivateUserSessionService.revokeSessionById(
|
||||
session.id,
|
||||
{
|
||||
reason: "User not found",
|
||||
},
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotAuthenticatedException("Account no longer exists."),
|
||||
);
|
||||
}
|
||||
|
||||
const renewedSession: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.renewSessionWithNewRefreshToken(
|
||||
{
|
||||
session,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
},
|
||||
);
|
||||
|
||||
const accessToken: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user,
|
||||
statusPageId: user.statusPageId!,
|
||||
sessionId: renewedSession.session.id!,
|
||||
refreshToken: renewedSession.refreshToken,
|
||||
refreshTokenExpiresAt: renewedSession.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
user,
|
||||
StatusPagePrivateUser,
|
||||
{
|
||||
miscData: {
|
||||
token: accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/forgot-password",
|
||||
async (
|
||||
@@ -146,6 +522,8 @@ router.post(
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
const statusPageIdString: string | null =
|
||||
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
@@ -154,12 +532,13 @@ router.post(
|
||||
templateType: EmailTemplateType.StatusPageForgotPassword,
|
||||
vars: {
|
||||
statusPageName: statusPageName!,
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statusPage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statusPage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
homeURL: statusPageURL,
|
||||
tokenVerifyUrl: URL.fromString(statusPageURL)
|
||||
.addRoute("/reset-password/" + token)
|
||||
@@ -288,6 +667,8 @@ router.post(
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
const statusPageIdString: string | null =
|
||||
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
@@ -297,12 +678,13 @@ router.post(
|
||||
vars: {
|
||||
homeURL: statusPageURL,
|
||||
statusPageName: statusPageName || "",
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statusPage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statusPage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -376,6 +758,7 @@ router.post(
|
||||
password: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -383,31 +766,38 @@ router.post(
|
||||
});
|
||||
|
||||
if (alreadySavedUser) {
|
||||
const token: string = JSONWebToken.sign({
|
||||
data: alreadySavedUser,
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(
|
||||
new PositiveNumber(30),
|
||||
),
|
||||
const { accessToken } = await finalizeStatusPageLogin({
|
||||
req,
|
||||
res,
|
||||
user: alreadySavedUser,
|
||||
});
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
|
||||
token,
|
||||
{
|
||||
httpOnly: true,
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
},
|
||||
);
|
||||
const sanitizedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: alreadySavedUser.id!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sanitizedUser && (alreadySavedUser as any).password) {
|
||||
delete (alreadySavedUser as any).password;
|
||||
}
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
alreadySavedUser,
|
||||
sanitizedUser || alreadySavedUser,
|
||||
StatusPagePrivateUser,
|
||||
{
|
||||
miscData: {
|
||||
token: token,
|
||||
token: accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -355,45 +355,124 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
// Update Status Page User - PUT /status-page-scim/v2/Users/{id}
|
||||
router.put(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
const handleStatusPageUserUpdate: (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
) => Promise<void> = async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to this status page
|
||||
const statusPageUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: new ObjectID(userId),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!statusPageUser) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
|
||||
`Status Page SCIM Update user - user not found for userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
);
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
|
||||
);
|
||||
|
||||
// Handle user deactivation by deleting from status page
|
||||
if (active === false) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
const scimConfig: StatusPageSCIM = bearerData[
|
||||
"scimConfig"
|
||||
] as StatusPageSCIM;
|
||||
if (scimConfig.autoDeprovisionUsers) {
|
||||
await StatusPagePrivateUserService.deleteOneById({
|
||||
id: new ObjectID(userId),
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user removed from status page`,
|
||||
);
|
||||
|
||||
// Return empty response for deleted user
|
||||
return Response.sendJsonObjectResponse(req, res, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to this status page
|
||||
const statusPageUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: new ObjectID(userId),
|
||||
},
|
||||
// Prepare update data
|
||||
const updateData: {
|
||||
email?: Email;
|
||||
} = {};
|
||||
|
||||
if (email && email !== statusPageUser.email?.toString()) {
|
||||
updateData.email = new Email(email);
|
||||
}
|
||||
|
||||
// Only update if there are changes
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
|
||||
await StatusPagePrivateUserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
data: updateData,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(`Status Page SCIM Update user - user updated successfully`);
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
@@ -403,116 +482,48 @@ router.put(
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!statusPageUser) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user not found for userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
if (updatedUser) {
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
|
||||
);
|
||||
|
||||
// Handle user deactivation by deleting from status page
|
||||
if (active === false) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
|
||||
);
|
||||
|
||||
const scimConfig: StatusPageSCIM = bearerData[
|
||||
"scimConfig"
|
||||
] as StatusPageSCIM;
|
||||
if (scimConfig.autoDeprovisionUsers) {
|
||||
await StatusPagePrivateUserService.deleteOneById({
|
||||
id: new ObjectID(userId),
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user removed from status page`,
|
||||
);
|
||||
|
||||
// Return empty response for deleted user
|
||||
return Response.sendJsonObjectResponse(req, res, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: {
|
||||
email?: Email;
|
||||
} = {};
|
||||
|
||||
if (email && email !== statusPageUser.email?.toString()) {
|
||||
updateData.email = new Email(email);
|
||||
}
|
||||
|
||||
// Only update if there are changes
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
|
||||
await StatusPagePrivateUserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
data: updateData,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user updated successfully`,
|
||||
);
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (updatedUser) {
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - no updates made, returning existing user`,
|
||||
);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
statusPageUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - no updates made, returning existing user`,
|
||||
);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
statusPageUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
|
||||
// Update Status Page User - PUT /status-page-scim/v2/Users/{id}
|
||||
router.put(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
handleStatusPageUserUpdate,
|
||||
);
|
||||
|
||||
// Update Status Page User - PATCH /status-page-scim/v2/Users/{id}
|
||||
router.patch(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
handleStatusPageUserUpdate,
|
||||
);
|
||||
|
||||
// Delete Status Page User - DELETE /status-page-scim/v2/Users/{id}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import SSOUtil from "../Utils/SSO";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Email from "Common/Types/Email";
|
||||
import BadRequestException from "Common/Types/Exception/BadRequestException";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
@@ -8,9 +7,11 @@ import ServerException from "Common/Types/Exception/ServerException";
|
||||
import HashedString from "Common/Types/HashedString";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import PositiveNumber from "Common/Types/PositiveNumber";
|
||||
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPagePrivateUserSessionService, {
|
||||
SessionMetadata as StatusPageSessionMetadata,
|
||||
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
|
||||
import StatusPageService from "Common/Server/Services/StatusPageService";
|
||||
import StatusPageSsoService from "Common/Server/Services/StatusPageSsoService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
@@ -19,8 +20,10 @@ import Express, {
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
extractDeviceInfo,
|
||||
getClientIp,
|
||||
headerValueToString,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
@@ -30,6 +33,8 @@ import xml2js from "xml2js";
|
||||
// Initialize Express router.
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
// Define a GET route for SSO in a status page context.
|
||||
router.get(
|
||||
"/status-page-sso/:statusPageId/:statusPageSsoId",
|
||||
@@ -285,24 +290,30 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const token: string = JSONWebToken.sign({
|
||||
data: alreadySavedUser,
|
||||
expiresInSeconds: OneUptimeDate.getSecondsInDays(
|
||||
new PositiveNumber(30),
|
||||
),
|
||||
if (!alreadySavedUser.projectId) {
|
||||
alreadySavedUser.projectId = projectId;
|
||||
}
|
||||
|
||||
const sessionMetadata: StatusPageSessionMetadata =
|
||||
await StatusPagePrivateUserSessionService.createSession({
|
||||
projectId: alreadySavedUser.projectId!,
|
||||
statusPageId: statusPageId,
|
||||
statusPagePrivateUserId: alreadySavedUser.id!,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: headerValueToString(req.headers["user-agent"]),
|
||||
...extractDeviceInfo(req),
|
||||
});
|
||||
|
||||
const token: string = CookieUtil.setStatusPagePrivateUserCookie({
|
||||
expressResponse: res,
|
||||
user: alreadySavedUser,
|
||||
statusPageId: statusPageId,
|
||||
sessionId: sessionMetadata.session.id!,
|
||||
refreshToken: sessionMetadata.refreshToken,
|
||||
refreshTokenExpiresAt: sessionMetadata.refreshTokenExpiresAt,
|
||||
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
|
||||
token,
|
||||
|
||||
{
|
||||
httpOnly: true,
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
},
|
||||
);
|
||||
|
||||
// get status page URL.
|
||||
const statusPageURL: string =
|
||||
await StatusPageService.getStatusPageFirstURL(statusPageId);
|
||||
|
||||
@@ -42,8 +42,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
|
||||
{{> CustomLogo this}}
|
||||
{{> EmailTitle title=(concat "Postmortem Published: " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="A postmortem has been published for an incident. Here are the details: "}}
|
||||
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=incidentTitle text="" }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Postmortem: " text="" }}
|
||||
{{> DetailBoxField title="" text=postmortemNote }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -14,8 +14,11 @@
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
@@ -25,8 +25,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{> InfoBlock info="You can visit the status page here:"}}
|
||||
{{> InfoBlock info=statusPageUrl}}
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
37
App/package-lock.json
generated
37
App/package-lock.json
generated
@@ -39,25 +39,25 @@
|
||||
"@bull-board/express": "^5.21.4",
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@elastic/elasticsearch": "^8.12.1",
|
||||
"@hcaptcha/react-hcaptcha": "^1.14.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.206.0",
|
||||
"@opentelemetry/context-zone": "^1.25.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.52.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.207.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^1.2.2",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.52.1",
|
||||
"@opentelemetry/instrumentation": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.207.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.207.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-logs": "^0.52.1",
|
||||
"@opentelemetry/sdk-logs": "^0.207.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.207.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.26.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@@ -96,21 +96,21 @@
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.7",
|
||||
"otpauth": "^9.3.1",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.16.3",
|
||||
"playwright": "^1.56.0",
|
||||
"posthog-js": "^1.275.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-big-calendar": "^1.13.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^16.0.0",
|
||||
@@ -125,7 +125,7 @@
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twilio": "^4.22.0",
|
||||
"typeorm": "^0.3.26",
|
||||
@@ -3501,10 +3501,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
|
||||
@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -418,6 +419,7 @@ export default class AlertFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Alert from "./Alert";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("alert")
|
||||
@@ -340,6 +349,54 @@ export default class AlertInternalNote extends BaseModel {
|
||||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateAlertInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadAlertInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditAlertInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "AlertInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "alertInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class AlertSeverity extends BaseModel {
|
||||
Permission.EditAlertSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -356,6 +357,7 @@ export default class AlertState extends BaseModel {
|
||||
Permission.EditAlertState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -20,7 +20,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectUser,
|
||||
@@ -28,7 +32,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadBillingPaymentMethod,
|
||||
],
|
||||
delete: [Permission.ProjectOwner, Permission.DeleteBillingPaymentMethod],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.DeleteBillingPaymentMethod,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/billing-payment-methods"))
|
||||
@@ -45,7 +53,11 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
})
|
||||
export default class BillingPaymentMethod extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectUser,
|
||||
@@ -77,7 +89,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
@@ -103,7 +119,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
@@ -136,7 +156,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
@@ -218,7 +242,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
@@ -278,7 +306,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public paymentProviderCustomerId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
@@ -298,7 +330,11 @@ export default class BillingPaymentMethod extends BaseModel {
|
||||
public last4Digits?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.CreateBillingPaymentMethod],
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
|
||||
@@ -15,6 +15,7 @@ import TableColumn, {
|
||||
getTableColumns,
|
||||
} from "../../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../../Types/Database/TableColumnType";
|
||||
import { getFirstColorFieldColumn } from "../../../Types/Database/ColorField";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import Email from "../../../Types/Email";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
@@ -57,6 +58,7 @@ export default class DatabaseBaseModel extends BaseEntity {
|
||||
type: TableColumnType.ObjectID,
|
||||
description: "ID of this object",
|
||||
computed: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
public _id?: string = undefined;
|
||||
@@ -203,6 +205,10 @@ export default class DatabaseBaseModel extends BaseEntity {
|
||||
return new Columns(Object.keys(getTableColumns(this)));
|
||||
}
|
||||
|
||||
public getFirstColorColumn(): string | null {
|
||||
return getFirstColorFieldColumn(this);
|
||||
}
|
||||
|
||||
public canQueryMultiTenant(): boolean {
|
||||
return Boolean(this.isMultiTenantRequestAllowed);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Entity } from "typeorm";
|
||||
@CrudApiEndpoint(new Route("/file"))
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser, Permission.AuthenticatedRequest],
|
||||
read: [Permission.CurrentUser, Permission.AuthenticatedRequest],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import OnCallDutyPolicy from "./OnCallDutyPolicy";
|
||||
import Probe from "./Probe";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -228,6 +229,43 @@ export default class Incident extends BaseModel {
|
||||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Date,
|
||||
title: "Declared At",
|
||||
description: "Date and time when this incident was declared.",
|
||||
isDefaultValueColumn: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
default: () => {
|
||||
return "now()";
|
||||
},
|
||||
})
|
||||
public declaredAt?: Date = undefined;
|
||||
|
||||
@Index()
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
@@ -803,6 +841,77 @@ export default class Incident extends BaseModel {
|
||||
})
|
||||
public subscriberNotificationStatusMessage?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
computed: true,
|
||||
hideColumnInDocumentation: true,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Subscriber Notification Status on Postmortem Published",
|
||||
description:
|
||||
"Status of notification sent to subscribers about this incident postmortem",
|
||||
defaultValue: StatusPageSubscriberNotificationStatus.Pending,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
default: StatusPageSubscriberNotificationStatus.Pending,
|
||||
})
|
||||
public subscriberNotificationStatusOnPostmortemPublished?: StatusPageSubscriberNotificationStatus =
|
||||
undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Notification Status Message on Postmortem Published",
|
||||
description:
|
||||
"Status message for subscriber notifications on postmortem published - includes success messages, failure reasons, or skip reasons",
|
||||
required: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public subscriberNotificationStatusMessageOnPostmortemPublished?: string =
|
||||
undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
@@ -959,6 +1068,151 @@ export default class Incident extends BaseModel {
|
||||
})
|
||||
public postmortemNote?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Show postmortem on status page?",
|
||||
description:
|
||||
"Should the postmortem note and attachments be visible on the status page once published?",
|
||||
defaultValue: false,
|
||||
isDefaultValueColumn: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public showPostmortemOnStatusPage?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Notify Subscribers on Postmortem Published",
|
||||
description:
|
||||
"Should subscribers be notified when the postmortem is published?",
|
||||
defaultValue: true,
|
||||
isDefaultValueColumn: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: true,
|
||||
})
|
||||
public notifySubscribersOnPostmortemPublished?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Postmortem Posted At",
|
||||
description:
|
||||
"Timestamp that will be shown alongside the published postmortem on the status page.",
|
||||
required: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public postmortemPostedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectIncident,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectIncident,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectIncident,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Postmortem Attachments",
|
||||
description:
|
||||
"Files that accompany the postmortem note and can be shared publicly when enabled.",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(() => {
|
||||
return File;
|
||||
})
|
||||
@JoinTable({
|
||||
name: "IncidentPostmortemAttachmentFile",
|
||||
joinColumn: {
|
||||
name: "incidentId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public postmortemAttachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Incident from "./Incident";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("incident")
|
||||
@@ -340,6 +349,54 @@ export default class IncidentInternalNote extends BaseModel {
|
||||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateIncidentInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditIncidentInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "IncidentInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "incidentInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Incident from "./Incident";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("incident")
|
||||
@@ -341,6 +350,54 @@ export default class IncidentPublicNote extends BaseModel {
|
||||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateIncidentPublicNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentPublicNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditIncidentPublicNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "IncidentPublicNoteFile",
|
||||
joinColumn: {
|
||||
name: "incidentPublicNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class IncidentSeverity extends BaseModel {
|
||||
Permission.EditIncidentSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class IncidentState extends BaseModel {
|
||||
Permission.EditIncidentState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -118,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
|
||||
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
|
||||
import StatusPageOwnerUser from "./StatusPageOwnerUser";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
|
||||
import StatusPageResource from "./StatusPageResource";
|
||||
import StatusPageSCIM from "./StatusPageSCIM";
|
||||
import StatusPageSSO from "./StatusPageSso";
|
||||
@@ -130,6 +131,7 @@ import TeamComplianceSetting from "./TeamComplianceSetting";
|
||||
import TelemetryService from "./TelemetryService";
|
||||
import UsageBilling from "./TelemetryUsageBilling";
|
||||
import User from "./User";
|
||||
import UserSession from "./UserSession";
|
||||
import UserCall from "./UserCall";
|
||||
// Notification Methods
|
||||
import UserEmail from "./UserEmail";
|
||||
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageFooterLink,
|
||||
StatusPageHeaderLink,
|
||||
StatusPagePrivateUser,
|
||||
StatusPagePrivateUserSession,
|
||||
StatusPageHistoryChartBarColorRule,
|
||||
|
||||
ScheduledMaintenanceState,
|
||||
@@ -375,6 +378,7 @@ const AllModelTypes: Array<{
|
||||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
UserSession,
|
||||
UserTotpAuth,
|
||||
UserWebAuthn,
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -365,6 +366,7 @@ export default class Label extends AccessControlModel {
|
||||
Permission.EditProjectLabel,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -379,6 +380,7 @@ export default class MonitorStatus extends BaseModel {
|
||||
Permission.EditProjectMonitorStatus,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -422,6 +423,7 @@ export default class ScheduledMaintenanceFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Project from "./Project";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -16,7 +17,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@CanAccessIfCanReadOn("scheduledMaintenance")
|
||||
@TenantColumn("projectId")
|
||||
@@ -340,6 +349,54 @@ export default class ScheduledMaintenanceInternalNote extends BaseModel {
|
||||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateScheduledMaintenanceInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadScheduledMaintenanceInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditScheduledMaintenanceInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ScheduledMaintenanceInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "scheduledMaintenanceInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Project from "./Project";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
@@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("scheduledMaintenance")
|
||||
@@ -342,6 +351,54 @@ export default class ScheduledMaintenancePublicNote extends BaseModel {
|
||||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateScheduledMaintenancePublicNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadScheduledMaintenancePublicNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditScheduledMaintenancePublicNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ScheduledMaintenancePublicNoteFile",
|
||||
joinColumn: {
|
||||
name: "scheduledMaintenancePublicNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class ScheduledMaintenanceState extends BaseModel {
|
||||
Permission.EditScheduledMaintenanceState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -448,6 +449,7 @@ export default class ServiceCatalog extends BaseModel {
|
||||
Permission.EditServiceCatalog,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
@@ -30,6 +30,7 @@ import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import Timezone from "../../Types/Timezone";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
@@ -883,6 +884,78 @@ export default class StatusPage extends BaseModel {
|
||||
})
|
||||
public isPublicStatusPage?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectStatusPage,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable Master Password",
|
||||
description:
|
||||
"Require visitors to enter a master password before viewing a private status page.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public enableMasterPassword?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectStatusPage,
|
||||
],
|
||||
|
||||
// This is a hashed column. So, reading the value is does not affect anything.
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Master Password",
|
||||
description:
|
||||
"Password required to unlock a private status page. This value is stored as a secure hash.",
|
||||
hashed: true,
|
||||
type: TableColumnType.HashedString,
|
||||
placeholder: "Enter a new master password",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public masterPassword?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -2,6 +2,7 @@ import Monitor from "./Monitor";
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
@@ -375,6 +376,54 @@ export default class StatusPageAnnouncement extends BaseModel {
|
||||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateStatusPageAnnouncement,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPageAnnouncement,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditStatusPageAnnouncement,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this announcement",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "StatusPageAnnouncementFile",
|
||||
joinColumn: {
|
||||
name: "statusPageAnnouncementId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -282,8 +282,9 @@ export default class StatusPageDomain extends BaseModel {
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Sumdomain",
|
||||
description: "Subdomain of your status page - like (status)",
|
||||
title: "Subdomain",
|
||||
description:
|
||||
"Subdomain label for your status page such as 'status'. Leave blank or enter @ to use the root domain.",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
|
||||
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Growth,
|
||||
read: PlanType.Growth,
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@CanAccessIfCanReadOn("statusPage")
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/status-page-private-user-session"))
|
||||
@Entity({
|
||||
name: "StatusPagePrivateUserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "StatusPagePrivateUserSession",
|
||||
singularName: "Status Page Private User Session",
|
||||
pluralName: "Status Page Private User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Stores status page private user sessions, refresh tokens, and device metadata for secure access control.",
|
||||
})
|
||||
export default class StatusPagePrivateUserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Project that owns this private status page session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Project ID",
|
||||
description: "Project identifier for this session.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Status page associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page ID",
|
||||
description: "Identifier for the status page.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPagePrivateUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPagePrivateUser,
|
||||
title: "Status Page Private User",
|
||||
description: "Private user record associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPagePrivateUser;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPagePrivateUserId" })
|
||||
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page Private User ID",
|
||||
description: "Identifier for the status page private user.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPagePrivateUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for the private user session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was active.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to access the status page.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (desktop, mobile, etc).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for the session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address recorded for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "User agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Indicates if the session has been revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Reason provided for revoking this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description: "Flexible JSON payload for storing structured metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -505,6 +506,7 @@ export default class TelemetryService extends BaseModel {
|
||||
Permission.EditTelemetryService,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.Public],
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
|
||||
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import User from "./User";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-session"))
|
||||
@Entity({
|
||||
name: "UserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserSession",
|
||||
singularName: "User Session",
|
||||
pluralName: "User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Active user sessions with refresh tokens and device metadata for enhanced authentication security.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User account this session belongs to.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "User ID",
|
||||
description: "Identifier for the user that owns this session.",
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for this session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was used.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to sign in.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (e.g., desktop, mobile).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address observed for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "Complete user agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Marks whether the session has been explicitly revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Optional reason describing why the session was revoked.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description:
|
||||
"Flexible JSON payload for storing structured session metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
|
||||
export default UserSession;
|
||||
96
Common/Server/API/AlertInternalNoteAPI.ts
Normal file
96
Common/Server/API/AlertInternalNoteAPI.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import AlertInternalNote from "../../Models/DatabaseModels/AlertInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import AlertInternalNoteService, {
|
||||
Service as AlertInternalNoteServiceType,
|
||||
} from "../Services/AlertInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class AlertInternalNoteAPI extends BaseAPI<
|
||||
AlertInternalNote,
|
||||
AlertInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(AlertInternalNote, AlertInternalNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: AlertInternalNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,16 @@ export default class UserAPI extends BaseAPI<
|
||||
const userPermissions: Array<UserPermission> = (
|
||||
await this.getPermissionsForTenant(req)
|
||||
).filter((permission: UserPermission) => {
|
||||
return (
|
||||
permission.permission.toString() ===
|
||||
Permission.ProjectOwner.toString() ||
|
||||
permission.permission.toString() ===
|
||||
Permission.CreateBillingPaymentMethod.toString()
|
||||
);
|
||||
return [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ManageProjectBilling,
|
||||
Permission.CreateBillingPaymentMethod,
|
||||
].includes(permission.permission);
|
||||
});
|
||||
|
||||
if (userPermissions.length === 0) {
|
||||
throw new BadDataException(
|
||||
"Only Project owner can add payment methods.",
|
||||
"Only project owners or members with Manage Billing access can add payment methods.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
106
Common/Server/API/IncidentAPI.ts
Normal file
106
Common/Server/API/IncidentAPI.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentService, {
|
||||
Service as IncidentServiceType,
|
||||
} from "../Services/IncidentService";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class IncidentAPI extends BaseAPI<
|
||||
Incident,
|
||||
IncidentServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(Incident, IncidentService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/postmortem/attachment/:projectId/:incidentId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getPostmortemAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getPostmortemAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const projectIdParam: string | undefined = req.params["projectId"];
|
||||
const incidentIdParam: string | undefined = req.params["incidentId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!projectIdParam || !incidentIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let incidentId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
let projectId: ObjectID;
|
||||
|
||||
try {
|
||||
incidentId = new ObjectID(incidentIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
projectId = new ObjectID(projectIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const incident: Incident | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: incidentId,
|
||||
projectId,
|
||||
},
|
||||
select: {
|
||||
postmortemAttachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
if (!incident) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const attachment: File | undefined = incident.postmortemAttachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
96
Common/Server/API/IncidentInternalNoteAPI.ts
Normal file
96
Common/Server/API/IncidentInternalNoteAPI.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import IncidentInternalNote from "../../Models/DatabaseModels/IncidentInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentInternalNoteService, {
|
||||
Service as IncidentInternalNoteServiceType,
|
||||
} from "../Services/IncidentInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class IncidentInternalNoteAPI extends BaseAPI<
|
||||
IncidentInternalNote,
|
||||
IncidentInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(IncidentInternalNote, IncidentInternalNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: IncidentInternalNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
96
Common/Server/API/IncidentPublicNoteAPI.ts
Normal file
96
Common/Server/API/IncidentPublicNoteAPI.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentPublicNoteService, {
|
||||
Service as IncidentPublicNoteServiceType,
|
||||
} from "../Services/IncidentPublicNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class IncidentPublicNoteAPI extends BaseAPI<
|
||||
IncidentPublicNote,
|
||||
IncidentPublicNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(IncidentPublicNote, IncidentPublicNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: IncidentPublicNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,48 @@ export default class MicrosoftTeamsAPI {
|
||||
supportsCalling: false,
|
||||
supportsVideo: false,
|
||||
// Provide basic command lists to improve client compatibility (esp. mobile)
|
||||
commandLists: [],
|
||||
commandLists: [
|
||||
{
|
||||
scopes: ["team", "groupChat", "personal"],
|
||||
commands: [
|
||||
{
|
||||
title: "help",
|
||||
description:
|
||||
"Show instructions for interacting with the OneUptime bot.",
|
||||
},
|
||||
{
|
||||
title: "create incident",
|
||||
description:
|
||||
"Launch the adaptive card to declare a new incident in OneUptime.",
|
||||
},
|
||||
{
|
||||
title: "create maintenance",
|
||||
description:
|
||||
"Open the workflow to schedule maintenance directly from Teams.",
|
||||
},
|
||||
{
|
||||
title: "show active incidents",
|
||||
description:
|
||||
"List all ongoing incidents with severity and state context.",
|
||||
},
|
||||
{
|
||||
title: "show scheduled maintenance",
|
||||
description:
|
||||
"Display upcoming scheduled maintenance events for the workspace.",
|
||||
},
|
||||
{
|
||||
title: "show ongoing maintenance",
|
||||
description:
|
||||
"Surface maintenance windows that are currently in progress.",
|
||||
},
|
||||
{
|
||||
title: "show active alerts",
|
||||
description:
|
||||
"Provide a summary of alerts that still require attention.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: ["identity", "messageTeamMembers"],
|
||||
|
||||
100
Common/Server/API/ScheduledMaintenanceInternalNoteAPI.ts
Normal file
100
Common/Server/API/ScheduledMaintenanceInternalNoteAPI.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import ScheduledMaintenanceInternalNote from "../../Models/DatabaseModels/ScheduledMaintenanceInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import ScheduledMaintenanceInternalNoteService, {
|
||||
Service as ScheduledMaintenanceInternalNoteServiceType,
|
||||
} from "../Services/ScheduledMaintenanceInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class ScheduledMaintenanceInternalNoteAPI extends BaseAPI<
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteService,
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: ScheduledMaintenanceInternalNote | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
100
Common/Server/API/ScheduledMaintenancePublicNoteAPI.ts
Normal file
100
Common/Server/API/ScheduledMaintenancePublicNoteAPI.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import ScheduledMaintenancePublicNote from "../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import ScheduledMaintenancePublicNoteService, {
|
||||
Service as ScheduledMaintenancePublicNoteServiceType,
|
||||
} from "../Services/ScheduledMaintenancePublicNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class ScheduledMaintenancePublicNoteAPI extends BaseAPI<
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteService,
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: ScheduledMaintenancePublicNote | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
98
Common/Server/API/StatusPageAnnouncementAPI.ts
Normal file
98
Common/Server/API/StatusPageAnnouncementAPI.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import StatusPageAnnouncement from "../../Models/DatabaseModels/StatusPageAnnouncement";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import StatusPageAnnouncementService, {
|
||||
Service as StatusPageAnnouncementServiceType,
|
||||
} from "../Services/StatusPageAnnouncementService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class StatusPageAnnouncementAPI extends BaseAPI<
|
||||
StatusPageAnnouncement,
|
||||
StatusPageAnnouncementServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(StatusPageAnnouncement, StatusPageAnnouncementService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:projectId/:announcementId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const announcementIdParam: string | undefined =
|
||||
req.params["announcementId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!announcementIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let announcementId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
announcementId = new ObjectID(announcementIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const announcement: StatusPageAnnouncement | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: announcementId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = announcement?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
95
Common/Server/API/UserAPI.ts
Normal file
95
Common/Server/API/UserAPI.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserService, {
|
||||
Service as UserServiceType,
|
||||
} from "../Services/UserService";
|
||||
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
|
||||
import logger from "../Utils/Logger";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
|
||||
const BLANK_PROFILE_PICTURE_PATH: string =
|
||||
"/usr/src/Common/UI/Images/users/blank-profile.svg";
|
||||
|
||||
export default class UserAPI extends BaseAPI<User, UserServiceType> {
|
||||
public constructor() {
|
||||
super(User, UserService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/profile-picture/:userId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const userIdParam: string | undefined = req.params["userId"];
|
||||
|
||||
if (!userIdParam) {
|
||||
return this.sendBlankProfile(req, res);
|
||||
}
|
||||
|
||||
let userId: ObjectID;
|
||||
|
||||
try {
|
||||
userId = new ObjectID(userIdParam);
|
||||
} catch {
|
||||
return this.sendBlankProfile(req, res);
|
||||
}
|
||||
|
||||
try {
|
||||
const profilePictureSelect: {
|
||||
profilePictureFile: {
|
||||
_id: boolean;
|
||||
file: boolean;
|
||||
fileType: boolean;
|
||||
name: boolean;
|
||||
};
|
||||
} = {
|
||||
profilePictureFile: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
|
||||
const userById: User | null = await UserService.findOneBy({
|
||||
query: {
|
||||
_id: userId,
|
||||
},
|
||||
select: profilePictureSelect,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userById && userById.profilePictureFile) {
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(
|
||||
req,
|
||||
res,
|
||||
userById.profilePictureFile,
|
||||
);
|
||||
}
|
||||
|
||||
return this.sendBlankProfile(req, res);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return this.sendBlankProfile(req, res);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private sendBlankProfile(req: ExpressRequest, res: ExpressResponse): void {
|
||||
Response.setNoCacheHeaders(res);
|
||||
|
||||
try {
|
||||
Response.sendFileByPath(req, res, BLANK_PROFILE_PICTURE_PATH);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new NotFoundException("User profile picture not found"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
|
||||
"DISABLE_TELEMETRY",
|
||||
"SLACK_APP_CLIENT_ID",
|
||||
"MICROSOFT_TEAMS_APP_CLIENT_ID",
|
||||
"CAPTCHA_ENABLED",
|
||||
"CAPTCHA_SITE_KEY",
|
||||
];
|
||||
|
||||
const FRONTEND_ENV_ALLOW_PREFIXES: Array<string> = [
|
||||
@@ -324,6 +326,13 @@ export const Host: string = process.env["HOST"] || "";
|
||||
|
||||
export const ProvisionSsl: boolean = process.env["PROVISION_SSL"] === "true";
|
||||
|
||||
export const CaptchaEnabled: boolean =
|
||||
process.env["CAPTCHA_ENABLED"] === "true";
|
||||
|
||||
export const CaptchaSecretKey: string = process.env["CAPTCHA_SECRET_KEY"] || "";
|
||||
|
||||
export const CaptchaSiteKey: string = process.env["CAPTCHA_SITE_KEY"] || "";
|
||||
|
||||
export const WorkflowScriptTimeoutInMS: number = process.env[
|
||||
"WORKFLOW_SCRIPT_TIMEOUT_IN_MS"
|
||||
]
|
||||
@@ -446,6 +455,8 @@ export const MicrosoftTeamsAppClientId: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_ID"] || null;
|
||||
export const MicrosoftTeamsAppClientSecret: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_SECRET"] || null;
|
||||
export const MicrosoftTeamsAppTenantId: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_TENANT_ID"] || null;
|
||||
|
||||
// VAPID Configuration for Web Push Notifications
|
||||
export const VapidPublicKey: string | undefined =
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1762890441920 implements MigrationInterface {
|
||||
public name = "MigrationName1762890441920";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "StatusPagePrivateUserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ac5f4c13d6bc9696cbfb8e5a79" ON "StatusPagePrivateUserSession" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "userId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_d66bd8342b0005c7192bdb17efc" UNIQUE ("refreshToken"), CONSTRAINT "PK_9dcd180f25755bab5fcebcbeb14" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7353eaf92987aeaf38c2590e94" ON "UserSession" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_d66bd8342b0005c7192bdb17ef"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7353eaf92987aeaf38c2590e94"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserSession"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ac5f4c13d6bc9696cbfb8e5a79"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "StatusPagePrivateUserSession"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763471659817 implements MigrationInterface {
|
||||
public name = "MigrationName1763471659817";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "IncidentInternalNoteFile" ("incidentInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1e97a749db84f9dc65ee162dd6b" PRIMARY KEY ("incidentInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0edb0291ff3e97197269d77dc4" ON "IncidentInternalNoteFile" ("incidentInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b30b49d21a553c06bd0ff3acf5" ON "IncidentInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "IncidentPublicNoteFile" ("incidentPublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_42d2fe75b663f8fa20421f31e78" PRIMARY KEY ("incidentPublicNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e5c4a5671b2bb51a9918f1f203" ON "IncidentPublicNoteFile" ("incidentPublicNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_81a5bc92f59cb5577746ee51ba" ON "IncidentPublicNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_0edb0291ff3e97197269d77dc48" FOREIGN KEY ("incidentInternalNoteId") REFERENCES "IncidentInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d" FOREIGN KEY ("incidentPublicNoteId") REFERENCES "IncidentPublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_0edb0291ff3e97197269d77dc48"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_81a5bc92f59cb5577746ee51ba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_e5c4a5671b2bb51a9918f1f203"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "IncidentPublicNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b30b49d21a553c06bd0ff3acf5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0edb0291ff3e97197269d77dc4"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "IncidentInternalNoteFile"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763477560906 implements MigrationInterface {
|
||||
public name = "MigrationName1763477560906";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_fddb744dc7cf400724befe5ba91" PRIMARY KEY ("scheduledMaintenanceInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ac92a60535a6d598c9619fd199" ON "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_daee340befeece208b507a4242" ON "ScheduledMaintenanceInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_373f78b83aa76e5250df8ebaed7" PRIMARY KEY ("scheduledMaintenancePublicNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_af6905f89ca8108ed0f478fd37" ON "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f09af6332e0b89f134472f0442" ON "ScheduledMaintenancePublicNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_ac92a60535a6d598c9619fd1999" FOREIGN KEY ("scheduledMaintenanceInternalNoteId") REFERENCES "ScheduledMaintenanceInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_daee340befeece208b507a42423" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_af6905f89ca8108ed0f478fd376" FOREIGN KEY ("scheduledMaintenancePublicNoteId") REFERENCES "ScheduledMaintenancePublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_f09af6332e0b89f134472f0442a" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_f09af6332e0b89f134472f0442a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_af6905f89ca8108ed0f478fd376"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_daee340befeece208b507a42423"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_ac92a60535a6d598c9619fd1999"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f09af6332e0b89f134472f0442"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_af6905f89ca8108ed0f478fd37"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "ScheduledMaintenancePublicNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_daee340befeece208b507a4242"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ac92a60535a6d598c9619fd199"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TABLE "ScheduledMaintenanceInternalNoteFile"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763480947474 implements MigrationInterface {
|
||||
public name = "MigrationName1763480947474";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "StatusPageAnnouncementFile" ("statusPageAnnouncementId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1323a0215e608ece58a96816134" PRIMARY KEY ("statusPageAnnouncementId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b152a77a26a67d2e76160ba15e" ON "StatusPageAnnouncementFile" ("statusPageAnnouncementId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2f78e2d073bf58013c962ce482" ON "StatusPageAnnouncementFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "AlertInternalNoteFile" ("alertInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_a5370c68590b3db5c3635d364aa" PRIMARY KEY ("alertInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_09507cdab877a482edcc4c0593" ON "AlertInternalNoteFile" ("alertInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_77dc8a31bd4ebb0882450abdde" ON "AlertInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3" FOREIGN KEY ("statusPageAnnouncementId") REFERENCES "StatusPageAnnouncement"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_2f78e2d073bf58013c962ce4827" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_09507cdab877a482edcc4c05933" FOREIGN KEY ("alertInternalNoteId") REFERENCES "AlertInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_09507cdab877a482edcc4c05933"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_2f78e2d073bf58013c962ce4827"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_77dc8a31bd4ebb0882450abdde"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_09507cdab877a482edcc4c0593"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "AlertInternalNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2f78e2d073bf58013c962ce482"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b152a77a26a67d2e76160ba15e"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "StatusPageAnnouncementFile"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763643080445 implements MigrationInterface {
|
||||
public name = "MigrationName1763643080445";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "enableMasterPassword" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "masterPassword" character varying(64)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "masterPassword"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "enableMasterPassword"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1764324618043 implements MigrationInterface {
|
||||
public name = "MigrationName1764324618043";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "declaredAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "Incident" SET "declaredAt" = "createdAt" WHERE "declaredAt" IS NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ALTER COLUMN "declaredAt" SET DEFAULT now()`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ALTER COLUMN "declaredAt" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b26979b9f119310661734465a4" ON "Incident" ("declaredAt") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b26979b9f119310661734465a4"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "Incident" DROP COLUMN "declaredAt"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1764762146063 implements MigrationInterface {
|
||||
public name = "MigrationName1764762146063";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "IncidentPostmortemAttachmentFile" ("incidentId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_40b17c7d5bcfbde48d7ebab4130" PRIMARY KEY ("incidentId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_62b9c09c42e05df3f134aa14a4" ON "IncidentPostmortemAttachmentFile" ("incidentId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7e09116a3b9672622bba9f8b2e" ON "IncidentPostmortemAttachmentFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "showPostmortemOnStatusPage" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPostmortemAttachmentFile" ADD CONSTRAINT "FK_62b9c09c42e05df3f134aa14a46" FOREIGN KEY ("incidentId") REFERENCES "Incident"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPostmortemAttachmentFile" ADD CONSTRAINT "FK_7e09116a3b9672622bba9f8b2e3" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPostmortemAttachmentFile" DROP CONSTRAINT "FK_7e09116a3b9672622bba9f8b2e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPostmortemAttachmentFile" DROP CONSTRAINT "FK_62b9c09c42e05df3f134aa14a46"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "showPostmortemOnStatusPage"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7e09116a3b9672622bba9f8b2e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_62b9c09c42e05df3f134aa14a4"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "IncidentPostmortemAttachmentFile"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1764767371788 implements MigrationInterface {
|
||||
public name = "MigrationName1764767371788";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1764789433216 implements MigrationInterface {
|
||||
public name = "MigrationName1764789433216";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "subscriberNotificationStatusOnPostmortemPublished" character varying NOT NULL DEFAULT 'Pending'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "subscriberNotificationStatusMessageOnPostmortemPublished" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "notifySubscribersOnPostmortemPublished" boolean NOT NULL DEFAULT true`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "notifySubscribersOnPostmortemPublished"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "subscriberNotificationStatusMessageOnPostmortemPublished"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "subscriberNotificationStatusOnPostmortemPublished"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,15 @@ import { MigrationName1761232578396 } from "./1761232578396-MigrationName";
|
||||
import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
|
||||
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
|
||||
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
|
||||
import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
|
||||
import { MigrationName1763471659817 } from "./1763471659817-MigrationName";
|
||||
import { MigrationName1763477560906 } from "./1763477560906-MigrationName";
|
||||
import { MigrationName1763480947474 } from "./1763480947474-MigrationName";
|
||||
import { MigrationName1763643080445 } from "./1763643080445-MigrationName";
|
||||
import { MigrationName1764324618043 } from "./1764324618043-MigrationName";
|
||||
import { MigrationName1764762146063 } from "./1764762146063-MigrationName";
|
||||
import { MigrationName1764767371788 } from "./1764767371788-MigrationName";
|
||||
import { MigrationName1764789433216 } from "./1764789433216-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -366,4 +375,13 @@ export default [
|
||||
MigrationName1761834523183,
|
||||
MigrationName1762181014879,
|
||||
MigrationName1762554602716,
|
||||
MigrationName1762890441920,
|
||||
MigrationName1763471659817,
|
||||
MigrationName1763477560906,
|
||||
MigrationName1763480947474,
|
||||
MigrationName1763643080445,
|
||||
MigrationName1764324618043,
|
||||
MigrationName1764762146063,
|
||||
MigrationName1764767371788,
|
||||
MigrationName1764789433216,
|
||||
];
|
||||
|
||||
@@ -24,7 +24,9 @@ export default class ProjectMiddleware {
|
||||
@CaptureSpan()
|
||||
public static getProjectId(req: ExpressRequest): ObjectID | null {
|
||||
let projectId: ObjectID | null = null;
|
||||
if (req.params && req.params["tenantid"]) {
|
||||
if (req.params && req.params["projectId"]) {
|
||||
projectId = new ObjectID(req.params["projectId"]);
|
||||
} else if (req.params && req.params["tenantid"]) {
|
||||
projectId = new ObjectID(req.params["tenantid"]);
|
||||
} else if (req.query && req.query["tenantid"]) {
|
||||
projectId = new ObjectID(req.query["tenantid"] as string);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import Alert from "../../Models/DatabaseModels/Alert";
|
||||
import AlertService from "./AlertService";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
||||
alertId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
@@ -50,6 +63,11 @@ export class Service extends DatabaseService<Model> {
|
||||
alertId: alertId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/alert-internal-note/attachment",
|
||||
);
|
||||
|
||||
await AlertFeedService.createAlertFeedItem({
|
||||
alertId: createdItem.alertId!,
|
||||
projectId: createdItem.projectId!,
|
||||
@@ -59,7 +77,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Alert ${alertNumber}](${(await AlertService.getAlertLinkInDashboard(createdItem.projectId!, alertId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -104,6 +122,10 @@ ${createdItem.note}
|
||||
|
||||
for (const updatedItem of updatedItems) {
|
||||
const alert: Alert = updatedItem.alert!;
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/alert-internal-note/attachment",
|
||||
);
|
||||
await AlertFeedService.createAlertFeedItem({
|
||||
alertId: updatedItem.alertId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
@@ -113,7 +135,7 @@ ${createdItem.note}
|
||||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Alert ${alert.alertNumber}](${(await AlertService.getAlertLinkInDashboard(alert.projectId!, alert.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -124,6 +146,57 @@ ${updatedItem.note}
|
||||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import IncidentService from "./IncidentService";
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
@@ -51,6 +64,11 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/incident-internal-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: createdItem.incidentId!,
|
||||
projectId: createdItem.projectId!,
|
||||
@@ -60,7 +78,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(createdItem.projectId!, incidentId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -105,6 +123,11 @@ ${createdItem.note}
|
||||
for (const updatedItem of updatedItems) {
|
||||
const incident: Incident = updatedItem.incident!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/incident-internal-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: updatedItem.incidentId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
@@ -114,7 +137,7 @@ ${createdItem.note}
|
||||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -125,6 +148,57 @@ ${updatedItem.note}
|
||||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -12,6 +12,8 @@ import IncidentService from "./IncidentService";
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -24,6 +26,7 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
@@ -32,6 +35,16 @@ export class Service extends DatabaseService<Model> {
|
||||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
publicNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
@@ -84,6 +97,11 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/incident-public-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: createdItem.incidentId!,
|
||||
projectId: createdItem.projectId!,
|
||||
@@ -92,7 +110,7 @@ export class Service extends DatabaseService<Model> {
|
||||
userId: userId || undefined,
|
||||
feedInfoInMarkdown: `📄 posted **public note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(projectId!, incidentId!)).toString()}) on status page:
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -138,6 +156,11 @@ ${createdItem.note}
|
||||
for (const updatedItem of updatedItems) {
|
||||
const incident: Incident = updatedItem.incident!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/incident-public-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: updatedItem.incidentId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
@@ -147,7 +170,7 @@ ${createdItem.note}
|
||||
|
||||
feedInfoInMarkdown: `📄 updated **Public Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -158,6 +181,57 @@ ${updatedItem.note}
|
||||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -480,6 +480,14 @@ export class Service extends DatabaseService<Model> {
|
||||
const projectId: ObjectID =
|
||||
createBy.props.tenantId || createBy.data.projectId!;
|
||||
|
||||
if (!createBy.data.declaredAt) {
|
||||
createBy.data.declaredAt = OneUptimeDate.getCurrentDate();
|
||||
} else {
|
||||
createBy.data.declaredAt = OneUptimeDate.fromString(
|
||||
createBy.data.declaredAt as Date,
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the initial incident state
|
||||
let initialIncidentStateId: ObjectID | undefined = undefined;
|
||||
|
||||
@@ -975,6 +983,7 @@ ${incident.remediationNotes || "No remediation notes provided."}
|
||||
notifyOwners: false,
|
||||
rootCause: createdItem.rootCause,
|
||||
stateChangeLog: createdItem.createdStateLog,
|
||||
timelineStartsAt: createdItem.declaredAt,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
@@ -1345,6 +1354,19 @@ ${incident.remediationNotes || "No remediation notes provided."}
|
||||
sendWorkspaceNotification: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Set subscriber notification status to Pending so the cron job will send notifications
|
||||
await this.updateOneById({
|
||||
id: incidentId,
|
||||
data: {
|
||||
subscriberNotificationStatusOnPostmortemPublished:
|
||||
StatusPageSubscriberNotificationStatus.Pending,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
ignoreHooks: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let shouldAddIncidentFeed: boolean = false;
|
||||
@@ -1790,6 +1812,7 @@ ${incidentSeverity.name}
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
monitors: {
|
||||
_id: true,
|
||||
@@ -1821,6 +1844,18 @@ ${incidentSeverity.name}
|
||||
incident.monitors,
|
||||
);
|
||||
}
|
||||
|
||||
if (incident.projectId && incident.id) {
|
||||
await MetricService.deleteBy({
|
||||
query: {
|
||||
projectId: incident.projectId,
|
||||
serviceId: incident.id,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1838,6 +1873,7 @@ ${incidentSeverity.name}
|
||||
rootCause: string | undefined;
|
||||
stateChangeLog: JSONObject | undefined;
|
||||
props: DatabaseCommonInteractionProps | undefined;
|
||||
timelineStartsAt?: Date | string | undefined;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
projectId,
|
||||
@@ -1849,8 +1885,13 @@ ${incidentSeverity.name}
|
||||
rootCause,
|
||||
stateChangeLog,
|
||||
props,
|
||||
timelineStartsAt,
|
||||
} = data;
|
||||
|
||||
const declaredTimelineStart: Date | undefined = timelineStartsAt
|
||||
? OneUptimeDate.fromString(timelineStartsAt as Date)
|
||||
: undefined;
|
||||
|
||||
// get last monitor status timeline.
|
||||
const lastIncidentStatusTimeline: IncidentStateTimeline | null =
|
||||
await IncidentStateTimelineService.findOneBy({
|
||||
@@ -1888,6 +1929,10 @@ ${incidentSeverity.name}
|
||||
statusTimeline.shouldStatusPageSubscribersBeNotified =
|
||||
shouldNotifyStatusPageSubscribers;
|
||||
|
||||
if (!lastIncidentStatusTimeline && declaredTimelineStart) {
|
||||
statusTimeline.startsAt = declaredTimelineStart;
|
||||
}
|
||||
|
||||
// Map boolean to enum value
|
||||
statusTimeline.subscriberNotificationStatus = isSubscribersNotified
|
||||
? StatusPageSubscriberNotificationStatus.Success
|
||||
@@ -1914,6 +1959,7 @@ ${incidentSeverity.name}
|
||||
id: data.incidentId,
|
||||
select: {
|
||||
projectId: true,
|
||||
declaredAt: true,
|
||||
monitors: {
|
||||
_id: true,
|
||||
name: true,
|
||||
@@ -1970,6 +2016,7 @@ ${incidentSeverity.name}
|
||||
|
||||
await MetricService.deleteBy({
|
||||
query: {
|
||||
projectId: incident.projectId,
|
||||
serviceId: data.incidentId,
|
||||
},
|
||||
props: {
|
||||
@@ -1983,6 +2030,7 @@ ${incidentSeverity.name}
|
||||
|
||||
const incidentStartsAt: Date =
|
||||
firstIncidentStateTimeline?.startsAt ||
|
||||
incident.declaredAt ||
|
||||
incident.createdAt ||
|
||||
OneUptimeDate.getCurrentDate();
|
||||
|
||||
@@ -2075,6 +2123,7 @@ ${incidentSeverity.name}
|
||||
|
||||
timeToAcknowledgeMetric.time =
|
||||
ackIncidentStateTimeline?.startsAt ||
|
||||
incident.declaredAt ||
|
||||
incident.createdAt ||
|
||||
OneUptimeDate.getCurrentDate();
|
||||
timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
||||
@@ -2140,6 +2189,7 @@ ${incidentSeverity.name}
|
||||
|
||||
timeToResolveMetric.time =
|
||||
resolvedIncidentStateTimeline?.startsAt ||
|
||||
incident.declaredAt ||
|
||||
incident.createdAt ||
|
||||
OneUptimeDate.getCurrentDate();
|
||||
timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
||||
@@ -2200,6 +2250,7 @@ ${incidentSeverity.name}
|
||||
|
||||
incidentDurationMetric.time =
|
||||
lastIncidentStateTimeline?.startsAt ||
|
||||
incident.declaredAt ||
|
||||
incident.createdAt ||
|
||||
OneUptimeDate.getCurrentDate();
|
||||
incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
||||
|
||||
@@ -106,6 +106,7 @@ import StatusPageHistoryChartBarColorRuleService from "./StatusPageHistoryChartB
|
||||
import StatusPageOwnerTeamService from "./StatusPageOwnerTeamService";
|
||||
import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
|
||||
import StatusPagePrivateUserService from "./StatusPagePrivateUserService";
|
||||
import StatusPagePrivateUserSessionService from "./StatusPagePrivateUserSessionService";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
// Status Page
|
||||
import StatusPageService from "./StatusPageService";
|
||||
@@ -125,6 +126,7 @@ import UserNotificationSettingService from "./UserNotificationSettingService";
|
||||
import UserOnCallLogService from "./UserOnCallLogService";
|
||||
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
||||
import UserService from "./UserService";
|
||||
import UserSessionService from "./UserSessionService";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
@@ -266,6 +268,7 @@ const services: Array<BaseService> = [
|
||||
StatusPageOwnerTeamService,
|
||||
StatusPageOwnerUserService,
|
||||
StatusPagePrivateUserService,
|
||||
StatusPagePrivateUserSessionService,
|
||||
StatusPageResourceService,
|
||||
StatusPageService,
|
||||
StatusPageSsoService,
|
||||
@@ -278,6 +281,7 @@ const services: Array<BaseService> = [
|
||||
TeamService,
|
||||
|
||||
UserService,
|
||||
UserSessionService,
|
||||
UserCallService,
|
||||
UserEmailService,
|
||||
UserNotificationRuleService,
|
||||
|
||||
@@ -63,14 +63,13 @@ import MonitorFeedService from "./MonitorFeedService";
|
||||
import { MonitorFeedEventType } from "../../Models/DatabaseModels/MonitorFeed";
|
||||
import { Gray500, Green500 } from "../../Types/BrandColors";
|
||||
import LabelService from "./LabelService";
|
||||
import QueryOperator from "../../Types/BaseDatabase/QueryOperator";
|
||||
import { FindWhere } from "../../Types/BaseDatabase/Query";
|
||||
import logger from "../Utils/Logger";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import MetricService from "./MetricService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -136,12 +135,26 @@ export class Service extends DatabaseService<Model> {
|
||||
protected override async onBeforeDelete(
|
||||
deleteBy: DeleteBy<Model>,
|
||||
): Promise<OnDelete<Model>> {
|
||||
if (deleteBy.query._id) {
|
||||
// delete all the status page resource for this monitor.
|
||||
const monitorsPendingDeletion: Array<Model> = await this.findBy({
|
||||
query: deleteBy.query,
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: deleteBy.props,
|
||||
});
|
||||
|
||||
for (const monitor of monitorsPendingDeletion) {
|
||||
if (!monitor.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete all the status page resources for this monitor.
|
||||
await StatusPageResourceService.deleteBy({
|
||||
query: {
|
||||
monitorId: new ObjectID(deleteBy.query._id as string),
|
||||
monitorId: monitor.id,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
@@ -150,37 +163,19 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
});
|
||||
|
||||
let projectId: FindWhere<ObjectID> | QueryOperator<ObjectID> | undefined =
|
||||
deleteBy.query.projectId || deleteBy.props.tenantId;
|
||||
const projectId: ObjectID | undefined = monitor.projectId as
|
||||
| ObjectID
|
||||
| undefined;
|
||||
|
||||
if (!projectId) {
|
||||
// fetch this monitor from the database to get the projectId.
|
||||
const monitor: Model | null = await this.findOneById({
|
||||
id: new ObjectID(deleteBy.query._id as string) as ObjectID,
|
||||
select: {
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!monitor) {
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
if (!monitor.id) {
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
projectId = monitor.projectId!;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await WorkspaceNotificationRuleService.archiveWorkspaceChannels({
|
||||
projectId: projectId as ObjectID,
|
||||
projectId: projectId,
|
||||
notificationFor: {
|
||||
monitorId: new ObjectID(deleteBy.query._id as string) as ObjectID,
|
||||
monitorId: monitor.id,
|
||||
},
|
||||
sendMessageBeforeArchiving: {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
@@ -189,12 +184,17 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error while archiving workspace channels for monitor ${deleteBy.query._id}: ${error}`,
|
||||
`Error while archiving workspace channels for monitor ${monitor.id?.toString()}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: null };
|
||||
return {
|
||||
deleteBy,
|
||||
carryForward: {
|
||||
monitors: monitorsPendingDeletion,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
@@ -208,6 +208,24 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
if (onDelete.carryForward && onDelete.carryForward.monitors) {
|
||||
for (const monitor of onDelete.carryForward.monitors as Array<Model>) {
|
||||
if (!monitor.projectId || !monitor.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await MetricService.deleteBy({
|
||||
query: {
|
||||
projectId: monitor.projectId,
|
||||
serviceId: monitor.id,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return onDelete;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
||||
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
||||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
@@ -52,6 +65,11 @@ export class Service extends DatabaseService<Model> {
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/scheduled-maintenance-internal-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
|
||||
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
|
||||
projectId: createdItem.projectId!,
|
||||
@@ -62,7 +80,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(createdItem.projectId!, scheduledMaintenanceId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -109,6 +127,11 @@ ${createdItem.note}
|
||||
const scheduledMaintenance: ScheduledMaintenance =
|
||||
updatedItem.scheduledMaintenance!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/scheduled-maintenance-internal-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
|
||||
{
|
||||
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
|
||||
@@ -120,7 +143,7 @@ ${createdItem.note}
|
||||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -132,6 +155,57 @@ ${updatedItem.note}
|
||||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -12,6 +12,8 @@ import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
||||
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -63,6 +65,11 @@ export class Service extends DatabaseService<Model> {
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/scheduled-maintenance-public-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
|
||||
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
|
||||
projectId: createdItem.projectId!,
|
||||
@@ -72,7 +79,7 @@ export class Service extends DatabaseService<Model> {
|
||||
userId: userId || undefined,
|
||||
feedInfoInMarkdown: `📄 posted **public note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(projectId!, scheduledMaintenanceId!)).toString()}) on status page:
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -119,6 +126,11 @@ ${createdItem.note}
|
||||
const scheduledMaintenance: ScheduledMaintenance =
|
||||
updatedItem.scheduledMaintenance!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/scheduled-maintenance-public-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
|
||||
{
|
||||
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
|
||||
@@ -130,7 +142,7 @@ ${createdItem.note}
|
||||
|
||||
feedInfoInMarkdown: `📄 updated **Public Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
@@ -149,6 +161,7 @@ ${updatedItem.note}
|
||||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
@@ -157,6 +170,16 @@ ${updatedItem.note}
|
||||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
publicNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
@@ -164,6 +187,57 @@ ${updatedItem.note}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -27,7 +27,7 @@ import User from "../../Models/DatabaseModels/User";
|
||||
import Recurring from "../../Types/Events/Recurring";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import UpdateBy from "../Types/Database/UpdateBy";
|
||||
import { FileRoute } from "../../ServiceRoute";
|
||||
import { StatusPageApiRoute } from "../../ServiceRoute";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
||||
import SMS from "../../Types/SMS/SMS";
|
||||
@@ -193,6 +193,13 @@ export class Service extends DatabaseService<Model> {
|
||||
const statusPageName: string =
|
||||
statuspage.pageTitle || statuspage.name || "Status Page";
|
||||
|
||||
const scheduledEventDetailsUrl: string =
|
||||
event.id && statusPageURL
|
||||
? URL.fromString(statusPageURL)
|
||||
.addRoute(`/scheduled-events/${event.id.toString()}`)
|
||||
.toString()
|
||||
: statusPageURL;
|
||||
|
||||
// Send email to Email subscribers.
|
||||
|
||||
const resourcesAffected: string =
|
||||
@@ -280,6 +287,8 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
||||
|
||||
if (subscriber.subscriberEmail) {
|
||||
// send email here.
|
||||
const statusPageIdString: string | null =
|
||||
statuspage.id?.toString() || statuspage._id?.toString() || null;
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
@@ -289,12 +298,14 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
||||
vars: {
|
||||
statusPageName: statusPageName,
|
||||
statusPageUrl: statusPageURL,
|
||||
logoUrl: statuspage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statuspage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
detailsUrl: scheduledEventDetailsUrl,
|
||||
logoUrl:
|
||||
statuspage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
isPublicStatusPage: statuspage.isPublicStatusPage
|
||||
? "true"
|
||||
: "false",
|
||||
@@ -374,27 +385,60 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
||||
(updateBy.data.startsAt as Date) ||
|
||||
(scheduledMaintenance.startsAt! as Date);
|
||||
|
||||
const notificationSettings: Array<Recurring> =
|
||||
(updateBy.data
|
||||
.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>) ||
|
||||
(scheduledMaintenance.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>);
|
||||
let notificationSettings: Array<Recurring> | null = null;
|
||||
|
||||
const updatedNotificationSettings: Array<Recurring> | null | undefined =
|
||||
updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent as
|
||||
| Array<Recurring>
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
updatedNotificationSettings !== null &&
|
||||
updatedNotificationSettings !== undefined
|
||||
) {
|
||||
notificationSettings = updatedNotificationSettings;
|
||||
} else {
|
||||
const existingNotificationSettings:
|
||||
| Array<Recurring>
|
||||
| null
|
||||
| undefined =
|
||||
scheduledMaintenance.sendSubscriberNotificationsOnBeforeTheEvent as
|
||||
| Array<Recurring>
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
existingNotificationSettings !== null &&
|
||||
existingNotificationSettings !== undefined
|
||||
) {
|
||||
notificationSettings = existingNotificationSettings;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Using startsAt: ${startsAt} and notificationSettings: ${JSON.stringify(notificationSettings)}`,
|
||||
);
|
||||
|
||||
const nextTimeToNotifyBeforeTheEvent: Date | null =
|
||||
this.getNextTimeToNotify({
|
||||
eventScheduledDate: startsAt,
|
||||
sendSubscriberNotifiationsOn: notificationSettings,
|
||||
});
|
||||
if (!notificationSettings || notificationSettings.length === 0) {
|
||||
logger.debug(
|
||||
"No subscriber notification schedule configured. Clearing nextSubscriberNotificationBeforeTheEventAt.",
|
||||
);
|
||||
updateBy.data.nextSubscriberNotificationBeforeTheEventAt = null;
|
||||
} else {
|
||||
const nextTimeToNotifyBeforeTheEvent: Date | null =
|
||||
this.getNextTimeToNotify({
|
||||
eventScheduledDate: startsAt,
|
||||
sendSubscriberNotifiationsOn: notificationSettings,
|
||||
});
|
||||
|
||||
updateBy.data.nextSubscriberNotificationBeforeTheEventAt =
|
||||
nextTimeToNotifyBeforeTheEvent;
|
||||
updateBy.data.nextSubscriberNotificationBeforeTheEventAt =
|
||||
nextTimeToNotifyBeforeTheEvent;
|
||||
|
||||
logger.debug(
|
||||
`nextSubscriberNotificationBeforeTheEventAt set to: ${nextTimeToNotifyBeforeTheEvent}`,
|
||||
);
|
||||
logger.debug(
|
||||
`nextSubscriberNotificationBeforeTheEventAt set to: ${nextTimeToNotifyBeforeTheEvent}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set notification status based on shouldStatusPageSubscribersBeNotifiedOnEventCreated if it's being updated
|
||||
@@ -472,7 +516,7 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
||||
|
||||
public getNextTimeToNotify(data: {
|
||||
eventScheduledDate: Date;
|
||||
sendSubscriberNotifiationsOn: Array<Recurring>;
|
||||
sendSubscriberNotifiationsOn?: Array<Recurring> | null | undefined;
|
||||
}): Date | null {
|
||||
let recurringDate: Date | null = null;
|
||||
|
||||
@@ -483,7 +527,23 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
||||
`Calculating next time to notify for event scheduled date: ${data.eventScheduledDate}`,
|
||||
);
|
||||
|
||||
for (const recurringItem of data.sendSubscriberNotifiationsOn) {
|
||||
const notificationSchedules: Array<Recurring> = Array.isArray(
|
||||
data.sendSubscriberNotifiationsOn,
|
||||
)
|
||||
? (data.sendSubscriberNotifiationsOn as Array<Recurring>)
|
||||
: [];
|
||||
|
||||
if (notificationSchedules.length === 0) {
|
||||
logger.debug(
|
||||
"No sendSubscriberNotifiationsOn entries. Returning null for next notification time.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const recurringItem of notificationSchedules) {
|
||||
if (!recurringItem) {
|
||||
continue;
|
||||
}
|
||||
const notificationDate: Date = Recurring.getNextDateInterval(
|
||||
data.eventScheduledDate,
|
||||
recurringItem,
|
||||
|
||||
@@ -510,12 +510,30 @@ export class Service extends DatabaseService<ScheduledMaintenanceStateTimeline>
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
nextSubscriberNotificationBeforeTheEventAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const hasProgressedBeyondScheduledState: boolean = Boolean(
|
||||
scheduledMaintenanceState && !scheduledMaintenanceState.isScheduledState,
|
||||
);
|
||||
|
||||
if (
|
||||
hasProgressedBeyondScheduledState &&
|
||||
scheduledMaintenanceEvent?.nextSubscriberNotificationBeforeTheEventAt
|
||||
) {
|
||||
await ScheduledMaintenanceService.updateOneById({
|
||||
id: createdItem.scheduledMaintenanceId!,
|
||||
data: {
|
||||
nextSubscriberNotificationBeforeTheEventAt: null,
|
||||
},
|
||||
props: onCreate.createBy.props,
|
||||
});
|
||||
}
|
||||
|
||||
if (isOngoingState) {
|
||||
if (
|
||||
scheduledMaintenanceEvent &&
|
||||
|
||||
@@ -48,19 +48,26 @@ export class Service extends DatabaseService<StatusPageDomain> {
|
||||
);
|
||||
}
|
||||
|
||||
if (createBy.data.subdomain) {
|
||||
// trim and lowercase the subdomain.
|
||||
createBy.data.subdomain = createBy.data.subdomain.trim().toLowerCase();
|
||||
let normalizedSubdomain: string =
|
||||
createBy.data.subdomain?.trim().toLowerCase() || "";
|
||||
|
||||
if (normalizedSubdomain === "@") {
|
||||
normalizedSubdomain = "";
|
||||
}
|
||||
|
||||
createBy.data.subdomain = normalizedSubdomain;
|
||||
|
||||
if (domain) {
|
||||
createBy.data.fullDomain = (
|
||||
createBy.data.subdomain +
|
||||
"." +
|
||||
domain.domain?.toString()
|
||||
)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
const baseDomain: string =
|
||||
domain.domain?.toString().toLowerCase().trim() || "";
|
||||
|
||||
if (!baseDomain) {
|
||||
throw new BadDataException("Please select a valid domain.");
|
||||
}
|
||||
|
||||
createBy.data.fullDomain = normalizedSubdomain
|
||||
? `${normalizedSubdomain}.${baseDomain}`
|
||||
: baseDomain;
|
||||
}
|
||||
|
||||
createBy.data.cnameVerificationToken = ObjectID.generate().toString();
|
||||
|
||||
@@ -5,7 +5,7 @@ import logger from "../Utils/Logger";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import MailService from "./MailService";
|
||||
import StatusPageService from "./StatusPageService";
|
||||
import { FileRoute } from "../../ServiceRoute";
|
||||
import { StatusPageApiRoute } from "../../ServiceRoute";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import Hostname from "../../Types/API/Hostname";
|
||||
import Protocol from "../../Types/API/Protocol";
|
||||
@@ -106,6 +106,8 @@ export class Service extends DatabaseService<Model> {
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
const statusPageIdString: string | null =
|
||||
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
@@ -115,12 +117,13 @@ export class Service extends DatabaseService<Model> {
|
||||
vars: {
|
||||
statusPageName: statusPageName!,
|
||||
statusPageUrl: statusPageURL,
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statusPage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statusPage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
homeURL: statusPageURL,
|
||||
tokenVerifyUrl: URL.fromString(statusPageURL)
|
||||
.addRoute("/reset-password/" + token)
|
||||
|
||||
368
Common/Server/Services/StatusPagePrivateUserSessionService.ts
Normal file
368
Common/Server/Services/StatusPagePrivateUserSessionService.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import { EncryptionSecret } from "../EnvironmentConfig";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import Text from "../../Types/Text";
|
||||
import logger from "../Utils/Logger";
|
||||
import Exception from "../../Types/Exception/Exception";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
|
||||
export interface SessionMetadata {
|
||||
session: Model;
|
||||
refreshToken: string;
|
||||
refreshTokenExpiresAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
projectId: ObjectID;
|
||||
statusPageId: ObjectID;
|
||||
statusPagePrivateUserId: ObjectID;
|
||||
refreshToken?: string | undefined;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface RenewSessionOptions {
|
||||
session: Model;
|
||||
refreshTokenExpiresAt?: Date | undefined;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
deviceName?: string | undefined;
|
||||
deviceType?: string | undefined;
|
||||
deviceOS?: string | undefined;
|
||||
deviceBrowser?: string | undefined;
|
||||
additionalInfo?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
export interface TouchSessionOptions {
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
}
|
||||
|
||||
export interface RevokeSessionOptions {
|
||||
reason?: string | undefined;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
|
||||
private static readonly SHORT_TEXT_LIMIT: number = 100;
|
||||
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
options: CreateSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string =
|
||||
options.refreshToken || Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const session: Model = this.buildSessionModel(options, {
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
});
|
||||
|
||||
try {
|
||||
const createdSession: Model = await this.create({
|
||||
data: session,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
session: createdSession,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error as Exception;
|
||||
}
|
||||
}
|
||||
|
||||
public async findActiveSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
): Promise<Model | null> {
|
||||
const hashedValue: string = await HashedString.hashValue(
|
||||
refreshToken,
|
||||
EncryptionSecret,
|
||||
);
|
||||
|
||||
const session: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
refreshToken: new HashedString(hashedValue, true),
|
||||
isRevoked: false,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
statusPageId: true,
|
||||
statusPagePrivateUserId: true,
|
||||
refreshTokenExpiresAt: true,
|
||||
lastActiveAt: true,
|
||||
additionalInfo: true,
|
||||
deviceName: true,
|
||||
deviceType: true,
|
||||
deviceOS: true,
|
||||
deviceBrowser: true,
|
||||
ipAddress: true,
|
||||
userAgent: true,
|
||||
isRevoked: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!session.refreshTokenExpiresAt ||
|
||||
OneUptimeDate.hasExpired(session.refreshTokenExpiresAt)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public async renewSessionWithNewRefreshToken(
|
||||
options: RenewSessionOptions,
|
||||
): Promise<SessionMetadata> {
|
||||
const refreshToken: string = Service.generateRefreshToken();
|
||||
const refreshTokenExpiresAt: Date =
|
||||
options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
|
||||
|
||||
const updatePayload: Partial<Model> = {
|
||||
refreshToken: HashedString.fromString(refreshToken),
|
||||
refreshTokenExpiresAt: refreshTokenExpiresAt,
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
isRevoked: false,
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
updatePayload.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
updatePayload.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
updatePayload.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
updatePayload.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
if (options.additionalInfo || options.session.additionalInfo) {
|
||||
updatePayload.additionalInfo = {
|
||||
...(options.session.additionalInfo || {}),
|
||||
...(options.additionalInfo || {}),
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const updatedSession: Model | null = await this.updateOneByIdAndFetch({
|
||||
id: options.session.id!,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedSession) {
|
||||
throw new BadDataException("Unable to renew status page user session");
|
||||
}
|
||||
|
||||
return {
|
||||
session: updatedSession,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async touchSession(
|
||||
sessionId: ObjectID,
|
||||
options: TouchSessionOptions,
|
||||
): Promise<void> {
|
||||
const updatePayload: Partial<Model> = {
|
||||
lastActiveAt: OneUptimeDate.getCurrentDate(),
|
||||
};
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
|
||||
if (ipAddress) {
|
||||
updatePayload.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
if (options.userAgent) {
|
||||
updatePayload.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: updatePayload as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to update status page session activity for session ${sessionId.toString()}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async revokeSessionById(
|
||||
sessionId: ObjectID,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
await this.updateOneById({
|
||||
id: sessionId,
|
||||
data: {
|
||||
isRevoked: true,
|
||||
revokedAt: OneUptimeDate.getCurrentDate(),
|
||||
revokedReason: options?.reason ?? null,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async revokeSessionByRefreshToken(
|
||||
refreshToken: string,
|
||||
options?: RevokeSessionOptions,
|
||||
): Promise<void> {
|
||||
const session: Model | null =
|
||||
await this.findActiveSessionByRefreshToken(refreshToken);
|
||||
|
||||
if (!session || !session.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.revokeSessionById(session.id, options);
|
||||
}
|
||||
|
||||
private buildSessionModel(
|
||||
options: CreateSessionOptions,
|
||||
tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date },
|
||||
): Model {
|
||||
const session: Model = new Model();
|
||||
session.projectId = options.projectId;
|
||||
session.statusPageId = options.statusPageId;
|
||||
session.statusPagePrivateUserId = options.statusPagePrivateUserId;
|
||||
session.refreshToken = HashedString.fromString(tokenMeta.refreshToken);
|
||||
session.refreshTokenExpiresAt = tokenMeta.refreshTokenExpiresAt;
|
||||
session.lastActiveAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (options.userAgent) {
|
||||
session.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
const deviceName: string | undefined = Text.truncate(
|
||||
options.deviceName,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceName) {
|
||||
session.deviceName = deviceName;
|
||||
}
|
||||
|
||||
const deviceType: string | undefined = Text.truncate(
|
||||
options.deviceType,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceType) {
|
||||
session.deviceType = deviceType;
|
||||
}
|
||||
|
||||
const deviceOS: string | undefined = Text.truncate(
|
||||
options.deviceOS,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceOS) {
|
||||
session.deviceOS = deviceOS;
|
||||
}
|
||||
|
||||
const deviceBrowser: string | undefined = Text.truncate(
|
||||
options.deviceBrowser,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (deviceBrowser) {
|
||||
session.deviceBrowser = deviceBrowser;
|
||||
}
|
||||
|
||||
const ipAddress: string | undefined = Text.truncate(
|
||||
options.ipAddress,
|
||||
Service.SHORT_TEXT_LIMIT,
|
||||
);
|
||||
if (ipAddress) {
|
||||
session.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
session.additionalInfo = {
|
||||
...(options.additionalInfo || {}),
|
||||
} as JSONObject;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private static generateRefreshToken(): string {
|
||||
return ObjectID.generate().toString();
|
||||
}
|
||||
|
||||
private static getRefreshTokenExpiry(): Date {
|
||||
return OneUptimeDate.getSomeDaysAfter(
|
||||
Service.DEFAULT_REFRESH_TOKEN_TTL_DAYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -42,11 +42,12 @@ import StatusPageSubscriberService from "./StatusPageSubscriberService";
|
||||
import StatusPageSubscriber from "../../Models/DatabaseModels/StatusPageSubscriber";
|
||||
import MailService from "./MailService";
|
||||
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
||||
import { FileRoute } from "../../ServiceRoute";
|
||||
import { StatusPageApiRoute } from "../../ServiceRoute";
|
||||
import ProjectSMTPConfigService from "./ProjectSmtpConfigService";
|
||||
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
|
||||
import MonitorGroupResourceService from "./MonitorGroupResourceService";
|
||||
import QueryHelper from "../Types/Database/QueryHelper";
|
||||
@@ -61,6 +62,11 @@ import IP from "../../Types/IP/IP";
|
||||
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
||||
import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
||||
import CommonAPI from "../API/CommonAPI";
|
||||
import MasterPasswordRequiredException from "../../Types/Exception/MasterPasswordRequiredException";
|
||||
import {
|
||||
MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
||||
MASTER_PASSWORD_REQUIRED_MESSAGE,
|
||||
} from "../../Types/StatusPage/MasterPassword";
|
||||
|
||||
export interface StatusPageReportItem {
|
||||
resourceName: string;
|
||||
@@ -389,6 +395,8 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
_id: true,
|
||||
isPublicStatusPage: true,
|
||||
ipWhitelist: true,
|
||||
enableMasterPassword: true,
|
||||
masterPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -462,6 +470,34 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldEnforceMasterPassword: boolean = Boolean(
|
||||
statusPage &&
|
||||
statusPage.enableMasterPassword &&
|
||||
statusPage.masterPassword &&
|
||||
!statusPage.isPublicStatusPage,
|
||||
);
|
||||
|
||||
if (shouldEnforceMasterPassword) {
|
||||
const hasValidMasterPassword: boolean =
|
||||
this.hasValidMasterPasswordCookie({
|
||||
req,
|
||||
statusPageId,
|
||||
});
|
||||
|
||||
if (hasValidMasterPassword) {
|
||||
return {
|
||||
hasReadAccess: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasReadAccess: false,
|
||||
error: new MasterPasswordRequiredException(
|
||||
MASTER_PASSWORD_REQUIRED_MESSAGE,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// if it does not have public access, check if this user has access.
|
||||
|
||||
const items: Array<StatusPage> = await this.findBy({
|
||||
@@ -493,6 +529,33 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
};
|
||||
}
|
||||
|
||||
private hasValidMasterPasswordCookie(data: {
|
||||
req: ExpressRequest;
|
||||
statusPageId: ObjectID;
|
||||
}): boolean {
|
||||
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
|
||||
data.req,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
return (
|
||||
payload["statusPageId"] === data.statusPageId.toString() &&
|
||||
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getMonitorStatusTimelineForStatusPage(data: {
|
||||
monitorIds: Array<ObjectID>;
|
||||
@@ -750,6 +813,9 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
const statusPageName: string =
|
||||
statuspage.pageTitle || statuspage.name || "Status Page";
|
||||
|
||||
const statusPageIdString: string | null =
|
||||
statuspage.id?.toString() || statuspage._id?.toString() || null;
|
||||
|
||||
const report: StatusPageReport = await this.getReportByStatusPage({
|
||||
statusPageId: statuspage.id!,
|
||||
historyDays: statuspage.reportDataInDays || 14,
|
||||
@@ -775,14 +841,16 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
subscriberEmailNotificationFooterText:
|
||||
Service.getSubscriberEmailFooterText(statuspage),
|
||||
statusPageUrl: statusPageURL,
|
||||
detailsUrl: statusPageURL,
|
||||
hasResources: report.totalResources > 0 ? "true" : "false",
|
||||
report: report as any,
|
||||
logoUrl: statuspage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statuspage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statuspage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
isPublicStatusPage: statuspage.isPublicStatusPage
|
||||
? "true"
|
||||
: "false",
|
||||
@@ -806,6 +874,7 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
if (data.email) {
|
||||
// force send to this email instead of sending to all subscribers.
|
||||
await sendEmail(data.email, null);
|
||||
return; // don't notify subscribers when explicitly sending a test email.
|
||||
}
|
||||
|
||||
const subscribers: Array<StatusPageSubscriber> =
|
||||
|
||||
@@ -16,7 +16,7 @@ import ProjectCallSMSConfigService from "./ProjectCallSMSConfigService";
|
||||
import ProjectService, { CurrentPlan } from "./ProjectService";
|
||||
import SmsService from "./SmsService";
|
||||
import StatusPageService from "./StatusPageService";
|
||||
import { FileRoute } from "../../ServiceRoute";
|
||||
import { StatusPageApiRoute } from "../../ServiceRoute";
|
||||
import Hostname from "../../Types/API/Hostname";
|
||||
import Protocol from "../../Types/API/Protocol";
|
||||
import URL from "../../Types/API/URL";
|
||||
@@ -522,6 +522,8 @@ Stay informed about service availability! 🚀`;
|
||||
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
logger.debug(`HTTP Protocol: ${httpProtocol}`);
|
||||
const statusPageIdString: string | null =
|
||||
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
||||
|
||||
const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink({
|
||||
statusPageUrl: statusPageURL,
|
||||
@@ -547,12 +549,13 @@ Stay informed about service availability! 🚀`;
|
||||
templateType: EmailTemplateType.ConfirmStatusPageSubscription,
|
||||
vars: {
|
||||
statusPageName: statusPageName,
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statusPage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statusPage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
statusPageUrl: statusPageURL,
|
||||
isPublicStatusPage: statusPage.isPublicStatusPage
|
||||
? "true"
|
||||
@@ -656,6 +659,8 @@ Stay informed about service availability! 🚀`;
|
||||
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
logger.debug(`HTTP Protocol: ${httpProtocol}`);
|
||||
const statusPageIdString: string | null =
|
||||
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
||||
|
||||
const unsubscribeLink: string = this.getUnsubscribeLink(
|
||||
URL.fromString(statusPageURL),
|
||||
@@ -675,12 +680,13 @@ Stay informed about service availability! 🚀`;
|
||||
templateType: EmailTemplateType.SubscribedToStatusPage,
|
||||
vars: {
|
||||
statusPageName: statusPageName,
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute("/image/" + statusPage.logoFileId)
|
||||
.toString()
|
||||
: "",
|
||||
logoUrl:
|
||||
statusPage.logoFileId && statusPageIdString
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(StatusPageApiRoute)
|
||||
.addRoute(`/logo/${statusPageIdString}`)
|
||||
.toString()
|
||||
: "",
|
||||
statusPageUrl: statusPageURL,
|
||||
isPublicStatusPage: statusPage.isPublicStatusPage
|
||||
? "true"
|
||||
|
||||
@@ -44,10 +44,11 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private async isSCIMEnabled(projectId: ObjectID): Promise<boolean> {
|
||||
private async isSCIMPushGroupsEnabled(projectId: ObjectID): Promise<boolean> {
|
||||
const count: PositiveNumber = await ProjectSCIMService.countBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
enablePushGroups: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -63,12 +64,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
// Check if SCIM is enabled for the project
|
||||
if (
|
||||
!createBy.props.isRoot &&
|
||||
(await this.isSCIMEnabled(
|
||||
(await this.isSCIMPushGroupsEnabled(
|
||||
createBy.data.projectId! || createBy.props.tenantId,
|
||||
))
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Cannot invite team members when SCIM is enabled for this project.",
|
||||
"Cannot invite team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,10 +312,10 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
!deleteBy.props.isRoot &&
|
||||
members.length > 0 &&
|
||||
members[0]?.projectId &&
|
||||
(await this.isSCIMEnabled(members[0].projectId))
|
||||
(await this.isSCIMPushGroupsEnabled(members[0].projectId))
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Cannot delete team members when SCIM is enabled for this project.",
|
||||
"Cannot delete team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -345,7 +346,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
},
|
||||
});
|
||||
|
||||
if (membersInTeam.toNumber() <= 1) {
|
||||
// Skip the one-member guard when SCIM manages membership for the project.
|
||||
const isPushGroupsManaged: boolean = await this.isSCIMPushGroupsEnabled(
|
||||
member.projectId!,
|
||||
);
|
||||
|
||||
if (!isPushGroupsManaged && membersInTeam.toNumber() <= 1) {
|
||||
throw new BadDataException(
|
||||
Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user