mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-01 20:28:17 +00:00
Compare commits
876
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94c9ab7939 | ||
|
|
a211b8593e | ||
|
|
25661613d5 | ||
|
|
3d8ea3576d | ||
|
|
e3a2eba5c3 | ||
|
|
81c446354b | ||
|
|
bf0314f5b1 | ||
|
|
c97180ac95 | ||
|
|
cac36f2c60 | ||
|
|
cd21fd0b7b | ||
|
|
a5a685ee3f | ||
|
|
a16fefe691 | ||
|
|
9c57485031 | ||
|
|
7051b21d86 | ||
|
|
0c4888ccdb | ||
|
|
e1b0e88bd8 | ||
|
|
cef1184908 | ||
|
|
31dda867d1 | ||
|
|
8a92da470c | ||
|
|
b89909ad66 | ||
|
|
2bb280e8de | ||
|
|
00cbadbb45 | ||
|
|
47ad527d38 | ||
|
|
51044a23b3 | ||
|
|
bb29d126c1 | ||
|
|
9d0f449eeb | ||
|
|
0eafa67e6f | ||
|
|
c3937472ec | ||
|
|
84d64424ca | ||
|
|
f0bfc5c94b | ||
|
|
ad86a6a698 | ||
|
|
af3b11ad54 | ||
|
|
d54f381998 | ||
|
|
3fd586753e | ||
|
|
9b3894f9e4 | ||
|
|
4a5f8f679a | ||
|
|
a444f5e5ec | ||
|
|
0d30c1cf83 | ||
|
|
15860519e1 | ||
|
|
cffda86eab | ||
|
|
a93fbfbbd9 | ||
|
|
e1cfc6e1a8 | ||
|
|
1e63df6536 | ||
|
|
8eb7fd0169 | ||
|
|
f8c6a8b747 | ||
|
|
8a9d6c2f9c | ||
|
|
bcb2a0b7cb | ||
|
|
2e181e70af | ||
|
|
ef615adb8e | ||
|
|
e03900811b | ||
|
|
5bc5fb0977 | ||
|
|
011032e3d4 | ||
|
|
17363e74f4 | ||
|
|
63efff6adf | ||
|
|
b5f569e522 | ||
|
|
5c77577992 | ||
|
|
4b9e113185 | ||
|
|
06d505c50a | ||
|
|
c70abfc826 | ||
|
|
db2c7d6c02 | ||
|
|
0a99ce1aa4 | ||
|
|
39c66f030a | ||
|
|
1223e21ac4 | ||
|
|
2177f6725c | ||
|
|
65ab16236f | ||
|
|
b4cc8151d7 | ||
|
|
e8b982998f | ||
|
|
ccd4159c59 | ||
|
|
9e49abb40d | ||
|
|
21a82b64f3 | ||
|
|
2414fcf21e | ||
|
|
94fbdb2226 | ||
|
|
985170d7ae | ||
|
|
b38b8f51e2 | ||
|
|
ea16e62d6b | ||
|
|
6182705410 | ||
|
|
a5d78a97ae | ||
|
|
2b33c22149 | ||
|
|
e32787baff | ||
|
|
f449d8ebd9 | ||
|
|
02627d023a | ||
|
|
cf94c73648 | ||
|
|
6b3241281b | ||
|
|
fe1caa003e | ||
|
|
e980c58507 | ||
|
|
ff5f8672e2 | ||
|
|
d51456ecf7 | ||
|
|
0cb9da13d0 | ||
|
|
f198449c16 | ||
|
|
0d627b0451 | ||
|
|
ff8eb3197d | ||
|
|
abd4e6f90a | ||
|
|
901518d26c | ||
|
|
17a4c0ef82 | ||
|
|
4ab8147149 | ||
|
|
0086528d51 | ||
|
|
bf12598915 | ||
|
|
efb0b58648 | ||
|
|
8a9b58b612 | ||
|
|
837a9d7265 | ||
|
|
d0122a9c20 | ||
|
|
968f75b688 | ||
|
|
877cb9d281 | ||
|
|
59efdb6272 | ||
|
|
7b1049224c | ||
|
|
0369f10b5c | ||
|
|
ecca9106f0 | ||
|
|
efc1f49d92 | ||
|
|
2d46ac3213 | ||
|
|
271174176b | ||
|
|
dcf7aed51c | ||
|
|
7fbce8b5be | ||
|
|
049eca9700 | ||
|
|
3bcd78bb32 | ||
|
|
f603b64679 | ||
|
|
b640465624 | ||
|
|
98282471a9 | ||
|
|
8983430889 | ||
|
|
d3aeca12cd | ||
|
|
ce619942a3 | ||
|
|
71b3f278a5 | ||
|
|
c92542ed90 | ||
|
|
69a9d78642 | ||
|
|
bf4f3cfd8c | ||
|
|
01afa69fcb | ||
|
|
80f3913fd2 | ||
|
|
10293ad6a9 | ||
|
|
e4d0c9c3e6 | ||
|
|
21c57e5620 | ||
|
|
959210e354 | ||
|
|
f10953e949 | ||
|
|
13903b9bb9 | ||
|
|
2e3a13d421 | ||
|
|
fa17fc3001 | ||
|
|
b7171556b8 | ||
|
|
254a638392 | ||
|
|
74b3bd6b46 | ||
|
|
9a29ef5401 | ||
|
|
3f21faaa66 | ||
|
|
bf19d29a1d | ||
|
|
6523f707e3 | ||
|
|
8b0bf84d34 | ||
|
|
9ef4c51ba1 | ||
|
|
19521489fa | ||
|
|
2ce9aad005 | ||
|
|
051fb6e4dd | ||
|
|
2125884c6c | ||
|
|
e3099aa2b2 | ||
|
|
f4c9226ef9 | ||
|
|
12c61d9b5d | ||
|
|
ff3999a6aa | ||
|
|
209470482a | ||
|
|
99c2a9ecf5 | ||
|
|
c0f75d241f | ||
|
|
1e0fb78dff | ||
|
|
c26194554c | ||
|
|
b70d37423d | ||
|
|
4ddd5edcfd | ||
|
|
3bcbba9581 | ||
|
|
67d47a9642 | ||
|
|
6f9c967c96 | ||
|
|
b8bdc5beea | ||
|
|
269f70b309 | ||
|
|
93f452ae05 | ||
|
|
e39378229f | ||
|
|
61a4f724ef | ||
|
|
38a9e21f5f | ||
|
|
1a2153d62f | ||
|
|
d25bfd147c | ||
|
|
d4ba45fd03 | ||
|
|
82f1035a5e | ||
|
|
6c57bac235 | ||
|
|
cf8eed35a3 | ||
|
|
55fc85c544 | ||
|
|
72ed637640 | ||
|
|
890364f597 | ||
|
|
30eec733b2 | ||
|
|
30a41ba144 | ||
|
|
2dc032e7e7 | ||
|
|
7f5981731f | ||
|
|
66309c7bbe | ||
|
|
15048eaf8a | ||
|
|
0e169a3cbc | ||
|
|
b5d933eaf3 | ||
|
|
6fa9bf861a | ||
|
|
9a217dff57 | ||
|
|
e08734c97f | ||
|
|
cd4382c1ff | ||
|
|
54b75ce11b | ||
|
|
0e038d0b12 | ||
|
|
85234fe0dd | ||
|
|
c97a02cb17 | ||
|
|
878ec977b8 | ||
|
|
8006867bc2 | ||
|
|
784b0ef090 | ||
|
|
b736cddf07 | ||
|
|
3d6328fb0b | ||
|
|
4246c7495e | ||
|
|
3d40746ccb | ||
|
|
3ab2f8f27d | ||
|
|
b44d23552c | ||
|
|
8e63757738 | ||
|
|
3b84409003 | ||
|
|
0ce3822300 | ||
|
|
a2c7ce00db | ||
|
|
121e30ce45 | ||
|
|
85c8a4937d | ||
|
|
53920f2f3f | ||
|
|
8c34914806 | ||
|
|
1881d07c4a | ||
|
|
3b8310fdca | ||
|
|
52bda8ebe2 | ||
|
|
275a53dbd1 | ||
|
|
9cefdb226d | ||
|
|
007f3911ff | ||
|
|
8ac0a8a79f | ||
|
|
285de97375 | ||
|
|
e2a7d2b92b | ||
|
|
da23ad76db | ||
|
|
55b0961b82 | ||
|
|
d3c5c0fa63 | ||
|
|
5dbc644d1d | ||
|
|
8eddb9e1bf | ||
|
|
76485f929d | ||
|
|
8e0182aa50 | ||
|
|
a18bdccc09 | ||
|
|
b27298501d | ||
|
|
33ebc91826 | ||
|
|
6948156693 | ||
|
|
0aaa4384c3 | ||
|
|
1f9d0c29ec | ||
|
|
039c652b28 | ||
|
|
653d4f53b5 | ||
|
|
31f8c2c1a6 | ||
|
|
8b93d4e97b | ||
|
|
adeb022818 | ||
|
|
f61c241dde | ||
|
|
e326cab44d | ||
|
|
61109f8ae9 | ||
|
|
3f8dc5cabe | ||
|
|
7f5d157739 | ||
|
|
bce73fb11f | ||
|
|
9473e65deb | ||
|
|
9c55c22230 | ||
|
|
77b9acd2d0 | ||
|
|
e99306adba | ||
|
|
96b947ebdc | ||
|
|
fb521f53cd | ||
|
|
6f7436f33f | ||
|
|
865140e681 | ||
|
|
c4a664572a | ||
|
|
acb631e33d | ||
|
|
7a10f88113 | ||
|
|
e616061c21 | ||
|
|
466f8fb8cc | ||
|
|
e03b9e7a14 | ||
|
|
5989822263 | ||
|
|
2cc8f46418 | ||
|
|
64a3e2652e | ||
|
|
ea44a1cbb6 | ||
|
|
fc9c0b0991 | ||
|
|
a4b5a44e44 | ||
|
|
d99a6aa322 | ||
|
|
3f910f851a | ||
|
|
bd231382eb | ||
|
|
295f1f86ca | ||
|
|
0a5e8001f9 | ||
|
|
9c1979e17a | ||
|
|
b2700d4bb0 | ||
|
|
01c1087e07 | ||
|
|
fcf7fb054e | ||
|
|
288aaeabc0 | ||
|
|
918c3d7416 | ||
|
|
7dedb3bd33 | ||
|
|
1ff5589a2e | ||
|
|
d24e9a1d80 | ||
|
|
7d3ef1ef92 | ||
|
|
df3f2489f9 | ||
|
|
53245b0067 | ||
|
|
dd699614ae | ||
|
|
6919a2a8d0 | ||
|
|
5c29e07450 | ||
|
|
00304262ba | ||
|
|
bc931c6dd8 | ||
|
|
c444d6e956 | ||
|
|
f7efb757bf | ||
|
|
2f140d213a | ||
|
|
df674dd441 | ||
|
|
09dced9fcd | ||
|
|
165b533172 | ||
|
|
b1dc567394 | ||
|
|
a0a1303515 | ||
|
|
c21d977f7c | ||
|
|
399d3492d3 | ||
|
|
b0b557246e | ||
|
|
bb671a90d6 | ||
|
|
f6ababbe7a | ||
|
|
82290285d0 | ||
|
|
c490814a98 | ||
|
|
7616b1e32c | ||
|
|
3ba2c91893 | ||
|
|
e789076ed4 | ||
|
|
3b6ddaaee6 | ||
|
|
8073194604 | ||
|
|
e7f410831f | ||
|
|
cd2c53b5c5 | ||
|
|
adf1b3b97e | ||
|
|
d84b5e184b | ||
|
|
6d608a0457 | ||
|
|
b8be2c7803 | ||
|
|
24fce721e3 | ||
|
|
23a9a27db1 | ||
|
|
0f5ed50af9 | ||
|
|
5f8f6a64ab | ||
|
|
8800ac2736 | ||
|
|
bdb82c2ce4 | ||
|
|
6d9ecc3568 | ||
|
|
7764447adc | ||
|
|
576fdecf89 | ||
|
|
3de1e90fa8 | ||
|
|
5e90950660 | ||
|
|
3ed26e648f | ||
|
|
fff3523152 | ||
|
|
19e4daeea4 | ||
|
|
8624d3f016 | ||
|
|
4f4da5bebe | ||
|
|
f00d2bf41d | ||
|
|
dc1d1400d8 | ||
|
|
d4451153a3 | ||
|
|
06ba759ea3 | ||
|
|
dd0d08ad6e | ||
|
|
ec81082877 | ||
|
|
7f58dc7090 | ||
|
|
43aa379c06 | ||
|
|
c284f8753c | ||
|
|
3a8f2495c4 | ||
|
|
af248716ef | ||
|
|
a57ad41ad4 | ||
|
|
ffddceec3b | ||
|
|
b04d5bb402 | ||
|
|
53df3caf3f | ||
|
|
ab2e35b54f | ||
|
|
bfa77d8f7b | ||
|
|
5002d147ad | ||
|
|
cd55dc0dc4 | ||
|
|
f1d56e4c2c | ||
|
|
bc49f79cb3 | ||
|
|
00163ad894 | ||
|
|
a2fbeb97cc | ||
|
|
288a796f46 | ||
|
|
b5a1e93770 | ||
|
|
1ee49ceab8 | ||
|
|
6eaf9cf6d0 | ||
|
|
075ca747df | ||
|
|
9ead4588c1 | ||
|
|
92b64bb362 | ||
|
|
cb8798d871 | ||
|
|
be70f4da09 | ||
|
|
b436d7de7a | ||
|
|
6780167f2b | ||
|
|
2a3ab4228f | ||
|
|
caeaf62a9d | ||
|
|
8e605aa361 | ||
|
|
fa4267f54a | ||
|
|
47d8ca2760 | ||
|
|
f3c51e5328 | ||
|
|
dcf70fdaae | ||
|
|
70f663374d | ||
|
|
38f2317bb6 | ||
|
|
31e0ca6c91 | ||
|
|
dc063cbf3f | ||
|
|
a4c82ae4a8 | ||
|
|
11a65e11d1 | ||
|
|
73d47f0ff5 | ||
|
|
d36e532c8e | ||
|
|
b2a24e3a49 | ||
|
|
301b6e98ec | ||
|
|
381a401ebb | ||
|
|
ba53934068 | ||
|
|
1b6846532f | ||
|
|
216349f303 | ||
|
|
878c9cf450 | ||
|
|
a654baef13 | ||
|
|
df852449b2 | ||
|
|
04d761ca14 | ||
|
|
9b168612a4 | ||
|
|
a772804dcd | ||
|
|
8a399fbbcf | ||
|
|
94049d6e8e | ||
|
|
c28cc58ac7 | ||
|
|
56a655bb90 | ||
|
|
b3d4a6b028 | ||
|
|
c7bd1f761a | ||
|
|
09c045455e | ||
|
|
52d651126e | ||
|
|
2f11b1e78f | ||
|
|
885e9b71bb | ||
|
|
193031dfdd | ||
|
|
f626a9bb5e | ||
|
|
ac26ad9f44 | ||
|
|
fbe00af6dc | ||
|
|
bfa9929b69 | ||
|
|
bada7000f6 | ||
|
|
2c908d8d95 | ||
|
|
da35633d42 | ||
|
|
cc7e06013f | ||
|
|
5ec36a05b5 | ||
|
|
f61e46c7c1 | ||
|
|
b9013b1493 | ||
|
|
5267fa6d2f | ||
|
|
7f40cbdeed | ||
|
|
b590c9402d | ||
|
|
9546feda11 | ||
|
|
3587f82976 | ||
|
|
94a3962590 | ||
|
|
3b2d79a451 | ||
|
|
da56a645af | ||
|
|
af4e55af1d | ||
|
|
d6a08a8f82 | ||
|
|
c73eb8869f | ||
|
|
929fc97ac6 | ||
|
|
7594a75bea | ||
|
|
ec36a056b2 | ||
|
|
8d30f6ef15 | ||
|
|
08d1e3164f | ||
|
|
a5f3ef20fe | ||
|
|
467445fe05 | ||
|
|
d0448f844a | ||
|
|
b2299a5f2f | ||
|
|
9b42754c69 | ||
|
|
aee33334ac | ||
|
|
cc530fb12d | ||
|
|
19877f1b29 | ||
|
|
e9880bcf68 | ||
|
|
4c7111dcb6 | ||
|
|
e81cfdcce1 | ||
|
|
3eba3f414f | ||
|
|
fbeb8dfd91 | ||
|
|
dda3e8baaf | ||
|
|
c1e4f0b18a | ||
|
|
b0d9f3a468 | ||
|
|
5b117acaa9 | ||
|
|
a5cb607650 | ||
|
|
fe626155c5 | ||
|
|
debbe1c1b1 | ||
|
|
f937a6efe2 | ||
|
|
30cb3c87eb | ||
|
|
fa46294154 | ||
|
|
72554fb27f | ||
|
|
4e6985b572 | ||
|
|
4477647613 | ||
|
|
2b3066abc9 | ||
|
|
da1bcb38da | ||
|
|
4a7524422d | ||
|
|
275af03195 | ||
|
|
0c75cf4619 | ||
|
|
80de277529 | ||
|
|
98f669db66 | ||
|
|
2c8dab5927 | ||
|
|
cb66d4f6c0 | ||
|
|
42a39ee891 | ||
|
|
c9bfe68ea5 | ||
|
|
8cb7144ff4 | ||
|
|
68cc58e2fe | ||
|
|
cbe5556ac9 | ||
|
|
eb80967608 | ||
|
|
66fea0722d | ||
|
|
c917061b97 | ||
|
|
da39a07f25 | ||
|
|
f2f65285de | ||
|
|
cac8f93330 | ||
|
|
bb03b18f4a | ||
|
|
ab280ee152 | ||
|
|
16c8137160 | ||
|
|
b8b903c8cd | ||
|
|
c98a5ff896 | ||
|
|
d68e436a8b | ||
|
|
8efa6790d6 | ||
|
|
a381f50e88 | ||
|
|
d3040cd5a8 | ||
|
|
f726a9883f | ||
|
|
c18a7baa37 | ||
|
|
d638d518f8 | ||
|
|
bdd371ffe8 | ||
|
|
57e6e49c7c | ||
|
|
a5c8de496d | ||
|
|
d478f32760 | ||
|
|
5d8e908339 | ||
|
|
606bae016b | ||
|
|
6910a2f04c | ||
|
|
8bee1788c1 | ||
|
|
b10459cb15 | ||
|
|
3b48dd1613 | ||
|
|
e5cef90a64 | ||
|
|
bac74c9577 | ||
|
|
19d748ae09 | ||
|
|
4db865371f | ||
|
|
75e8b3bf4b | ||
|
|
194cdc66b7 | ||
|
|
53059c0a0d | ||
|
|
ed4ffc8a86 | ||
|
|
9967c172da | ||
|
|
0aa9c065a5 | ||
|
|
d0c2abd38d | ||
|
|
cb98099a91 | ||
|
|
f446e0d5b1 | ||
|
|
17de31f1a6 | ||
|
|
d81a466089 | ||
|
|
9e597fadba | ||
|
|
1e6aa0f4c2 | ||
|
|
41842f7c6a | ||
|
|
0d5564990d | ||
|
|
7f9755493a | ||
|
|
fea587ad36 | ||
|
|
b2b7537fee | ||
|
|
57f99fe7af | ||
|
|
01263017e7 | ||
|
|
e66f9a7244 | ||
|
|
54cb3f8e7a | ||
|
|
4c861cf5e4 | ||
|
|
a89f57e690 | ||
|
|
d5141e5b6a | ||
|
|
8aded98a11 | ||
|
|
0b5a0d0d7e | ||
|
|
5a36762544 | ||
|
|
a4b7573ce7 | ||
|
|
6fc8c0b4a8 | ||
|
|
ced2a97547 | ||
|
|
f579171024 | ||
|
|
885e5f4568 | ||
|
|
81d8cd8a07 | ||
|
|
f153f41357 | ||
|
|
2b9e3ff3bc | ||
|
|
d15cc4642a | ||
|
|
3c4f45678a | ||
|
|
b312849fff | ||
|
|
10580dfa03 | ||
|
|
62d0e8b041 | ||
|
|
17cdaac380 | ||
|
|
e4eb388e18 | ||
|
|
d94c5dee4e | ||
|
|
4bea0816b6 | ||
|
|
fb491328d6 | ||
|
|
4e043d93ce | ||
|
|
0b525b835f | ||
|
|
3c8c39ec26 | ||
|
|
36997403e1 | ||
|
|
1175591521 | ||
|
|
04accb6689 | ||
|
|
58e3c7be94 | ||
|
|
7a6db36554 | ||
|
|
80ebd98f6f | ||
|
|
9e0495fc26 | ||
|
|
f2437a8b3c | ||
|
|
9e87562e23 | ||
|
|
8f5cb13cc3 | ||
|
|
e0a0ec9911 | ||
|
|
419c063c6c | ||
|
|
2aed66d371 | ||
|
|
1f45436c13 | ||
|
|
df8520afd3 | ||
|
|
95187e6b80 | ||
|
|
c8a3ace052 | ||
|
|
c059ac45bf | ||
|
|
f4b8addda4 | ||
|
|
aa3a1cc300 | ||
|
|
0a5f138829 | ||
|
|
9f427f5873 | ||
|
|
bacbf42a04 | ||
|
|
56a9d52466 | ||
|
|
2c964253ec | ||
|
|
e886bea05c | ||
|
|
66affed338 | ||
|
|
a8ae07e6c4 | ||
|
|
0df286a5f7 | ||
|
|
519a8feb14 | ||
|
|
16cb00f46f | ||
|
|
3c59602666 | ||
|
|
3e0dbc7401 | ||
|
|
a24d29b56b | ||
|
|
deb32b5c0f | ||
|
|
d4a6d2fb4a | ||
|
|
01ea0e9561 | ||
|
|
9676b51f01 | ||
|
|
b0d2f86eaf | ||
|
|
f356efb42e | ||
|
|
559d98e884 | ||
|
|
d8623b88df | ||
|
|
7044d6606e | ||
|
|
92aab57d37 | ||
|
|
f661055ae8 | ||
|
|
d310ec2b93 | ||
|
|
301ad369c0 | ||
|
|
4c94a51a9d | ||
|
|
a2c1d79247 | ||
|
|
e5d9ac2fbf | ||
|
|
67a15b36b3 | ||
|
|
f396cdb2d3 | ||
|
|
9cc538cdcd | ||
|
|
01da6becff | ||
|
|
003c9b176c | ||
|
|
0a5315fb95 | ||
|
|
5a4d578d13 | ||
|
|
f1345472b5 | ||
|
|
60214f0e57 | ||
|
|
3a2ce72910 | ||
|
|
ea9c0502b7 | ||
|
|
074d3ce201 | ||
|
|
05c80e2497 | ||
|
|
8a363dd815 | ||
|
|
1e5fe59d9c | ||
|
|
1a2b9617fe | ||
|
|
1f92510a43 | ||
|
|
f7fceb1ea2 | ||
|
|
2aec60a354 | ||
|
|
9bc42a4af0 | ||
|
|
5c98cc1748 | ||
|
|
9ae14ab04a | ||
|
|
5d014f4669 | ||
|
|
5e8dea7255 | ||
|
|
38fc9f521b | ||
|
|
80d7ac6ff9 | ||
|
|
9bacd7a0f5 | ||
|
|
c89ac40039 | ||
|
|
6d6cf590ee | ||
|
|
f063a8f30e | ||
|
|
03536a2180 | ||
|
|
0efd5cc257 | ||
|
|
9439f13b7b | ||
|
|
d936806406 | ||
|
|
9d7ccd4201 | ||
|
|
c8a4c1aae6 | ||
|
|
406db7ccff | ||
|
|
f48a35c7ce | ||
|
|
8d25df74b7 | ||
|
|
831a03402e | ||
|
|
a631338599 | ||
|
|
d943721b4a | ||
|
|
1bbf978cc4 | ||
|
|
856f913ce6 | ||
|
|
dc617b6925 | ||
|
|
3f80062672 | ||
|
|
07eb423814 | ||
|
|
721232448e | ||
|
|
0b0d261aae | ||
|
|
87a8d741d7 | ||
|
|
d03dd6129a | ||
|
|
96c20995a3 | ||
|
|
82470553db | ||
|
|
2408505889 | ||
|
|
7d85693a1f | ||
|
|
d86a011bfd | ||
|
|
4d54ac744b | ||
|
|
132614b551 | ||
|
|
17c541548a | ||
|
|
d3f7664116 | ||
|
|
2ea40004a6 | ||
|
|
82ec2d9493 | ||
|
|
53bef174b3 | ||
|
|
bcfef8359e | ||
|
|
5f18411aed | ||
|
|
1102a6e8e5 | ||
|
|
867aec092b | ||
|
|
ea9e727fcf | ||
|
|
099476f618 | ||
|
|
db78494b1a | ||
|
|
e7ad6e5a7b | ||
|
|
3d7d051df2 | ||
|
|
8e827dc934 | ||
|
|
12b764710a | ||
|
|
b5c95874bb | ||
|
|
b896b1a9b8 | ||
|
|
cf99acc682 | ||
|
|
7fb20964dc | ||
|
|
8751117946 | ||
|
|
10c4ed179b | ||
|
|
8e13541b24 | ||
|
|
4f6a9d4b66 | ||
|
|
56411496c2 | ||
|
|
e55a1cb657 | ||
|
|
3850d4b8ae | ||
|
|
1626d85d31 | ||
|
|
2a6677e4d9 | ||
|
|
74e62c001d | ||
|
|
8237160c6d | ||
|
|
41bf9d4856 | ||
|
|
44dcf1b281 | ||
|
|
104787bce8 | ||
|
|
38a330e578 | ||
|
|
7d0b8755c9 | ||
|
|
7aadf4d9d1 | ||
|
|
af8fa72377 | ||
|
|
c51f262474 | ||
|
|
54d19a78ab | ||
|
|
66f33a2246 | ||
|
|
55d9441d10 | ||
|
|
8b7342652e | ||
|
|
118c2653a5 | ||
|
|
f7d0128d99 | ||
|
|
12cc555491 | ||
|
|
cf5e36db00 | ||
|
|
7c0b87ffc3 | ||
|
|
beb086ad3c | ||
|
|
8c12291096 | ||
|
|
56a063b644 | ||
|
|
3a6054c1c6 | ||
|
|
3a696ecb0e | ||
|
|
1166073b1c | ||
|
|
3026cd0748 | ||
|
|
b48d3ae667 | ||
|
|
5868f21f22 | ||
|
|
873f7ea314 | ||
|
|
2b01b340e4 | ||
|
|
5888088486 | ||
|
|
9e2de28032 | ||
|
|
db2ea09d25 | ||
|
|
8c6b514b7b | ||
|
|
ad03d663bb | ||
|
|
48d4cbc41a | ||
|
|
ae81816aa7 | ||
|
|
ccdff58056 | ||
|
|
c10f85950a | ||
|
|
7d69220461 | ||
|
|
e316bd1a7b | ||
|
|
fd47a746af | ||
|
|
5038c7845f | ||
|
|
9876eab521 | ||
|
|
99a52c3ff2 | ||
|
|
0119cee3c6 | ||
|
|
c7e850cc79 | ||
|
|
5da47e5438 | ||
|
|
b0f9f5c77e | ||
|
|
35f35ddeb3 | ||
|
|
90155a8811 | ||
|
|
ae8baca694 | ||
|
|
82b8751f65 | ||
|
|
78ee64fc45 | ||
|
|
2f30837fed | ||
|
|
f46b1b62db | ||
|
|
8b6cd8a88e | ||
|
|
0c333b476a | ||
|
|
1aa9371245 | ||
|
|
14fcf4a0a1 | ||
|
|
24f824bcd1 | ||
|
|
fcdd0bf571 | ||
|
|
c416a2cb97 | ||
|
|
d03021fc9d | ||
|
|
574cf48726 | ||
|
|
7cef404acf | ||
|
|
3c32c3c7d4 | ||
|
|
d44531b963 | ||
|
|
36c80b480e | ||
|
|
eeef97679c | ||
|
|
c789dae3f6 | ||
|
|
4b20388d45 | ||
|
|
33fd2bd35f | ||
|
|
654c837d3f | ||
|
|
416b58080d | ||
|
|
216defb76f | ||
|
|
0970c748e0 | ||
|
|
aabd741cad | ||
|
|
950c6414b6 | ||
|
|
d1149a4d52 | ||
|
|
4c6f4bfd84 | ||
|
|
9caf3dc67f | ||
|
|
635c4da48c | ||
|
|
44bb205483 | ||
|
|
edb4622684 | ||
|
|
abb46d78da | ||
|
|
04fdc0368a | ||
|
|
50d76622c5 | ||
|
|
238f56cb76 | ||
|
|
e721d6f5f5 | ||
|
|
44df13e820 | ||
|
|
1fd0f79dd5 | ||
|
|
1b92371737 | ||
|
|
bf5b3484a1 | ||
|
|
eb1e248d4b | ||
|
|
0827414051 | ||
|
|
081986607b | ||
|
|
189080199a | ||
|
|
55281e3965 | ||
|
|
db8a6175e8 | ||
|
|
7b0d4dd5bc | ||
|
|
6fd277ef3a | ||
|
|
7b25bfdb69 | ||
|
|
2abc19c589 | ||
|
|
faf6b1df75 | ||
|
|
422be4a422 | ||
|
|
da136e0ef0 | ||
|
|
737cc9b8ea | ||
|
|
23f6b179fd | ||
|
|
03e451c21d | ||
|
|
be061a0fb9 | ||
|
|
c83f6a68d1 | ||
|
|
2646e3e4f6 | ||
|
|
06424d6bcf | ||
|
|
45b82515fc | ||
|
|
a9cc951e34 | ||
|
|
93ccf506fb | ||
|
|
706d0ccfa0 | ||
|
|
c2eec4e95c | ||
|
|
ac3132b6eb | ||
|
|
ee8c5e7a93 | ||
|
|
a2c1f2da6f | ||
|
|
7ecb50315f | ||
|
|
84b3370826 | ||
|
|
aa9e0536a7 | ||
|
|
544d13fac5 | ||
|
|
a4ff07a285 | ||
|
|
28578d3aa8 | ||
|
|
b8ea729a33 | ||
|
|
b383070b68 | ||
|
|
87882b5afc | ||
|
|
6a14c2b459 | ||
|
|
f0bfb6ba8e | ||
|
|
99288fa805 | ||
|
|
35cc0094b3 | ||
|
|
98262bcba9 | ||
|
|
a2c9a462e4 | ||
|
|
c5c089e5d5 | ||
|
|
de06512249 | ||
|
|
9058b80383 | ||
|
|
be64c582e1 | ||
|
|
d6b3e02890 | ||
|
|
d06910515b | ||
|
|
d1b328d481 | ||
|
|
fbc0d2eede | ||
|
|
945955a71c | ||
|
|
9fcbeda470 | ||
|
|
8043635b1c | ||
|
|
cdba41dffa | ||
|
|
5020e8913e | ||
|
|
89369270b9 | ||
|
|
52f17725af | ||
|
|
6da213aebc | ||
|
|
a353a1b45c | ||
|
|
b36ff624b5 | ||
|
|
03c09a525b | ||
|
|
9ea610a405 | ||
|
|
cd97bcbac0 | ||
|
|
52bf2bdef3 | ||
|
|
161d1fa05d | ||
|
|
642bf462ad | ||
|
|
6d61d74d0c | ||
|
|
2bc060b13d | ||
|
|
28efdf95d9 | ||
|
|
1ef9fd4298 | ||
|
|
5c24ec5560 | ||
|
|
962e094a65 | ||
|
|
131b166d8d | ||
|
|
5419f3690c | ||
|
|
6f3eb7965b | ||
|
|
909f0d4a4e | ||
|
|
8c8fdf5f88 | ||
|
|
e9582a51c2 | ||
|
|
0590c20af8 | ||
|
|
05eefe866b | ||
|
|
fa023756c7 | ||
|
|
7f967e5e75 | ||
|
|
2a0b17e45a | ||
|
|
b1f6d765b2 | ||
|
|
3c9ed08515 | ||
|
|
c7ca8297c9 | ||
|
|
d269e9efe4 | ||
|
|
61cc238891 | ||
|
|
b5df916511 | ||
|
|
95da106fb8 | ||
|
|
5730402312 | ||
|
|
c2ea8b69da | ||
|
|
14081bb9ca | ||
|
|
82d9024f24 | ||
|
|
53a6d3957a | ||
|
|
db55a8abde | ||
|
|
8f40b159d6 | ||
|
|
190a63f0eb |
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
coverage
|
||||
.nyc_output
|
||||
*.tgz
|
||||
!protospack.tgz
|
||||
+1
-1
@@ -8,7 +8,7 @@ module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
@@ -11,7 +12,6 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- stg
|
||||
- stg2
|
||||
- prd
|
||||
|
||||
jobs:
|
||||
@@ -29,6 +29,8 @@ jobs:
|
||||
echo "environment=${DEPLOY_ENV}" >> $GITHUB_OUTPUT
|
||||
elif [ ${GITHUB_REF} == "refs/heads/main" ]; then
|
||||
echo "environment=prd" >> $GITHUB_OUTPUT
|
||||
elif [ ${GITHUB_REF} == "refs/heads/beta" ]; then
|
||||
echo "environment=stg" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
id: extract_environment
|
||||
|
||||
@@ -39,54 +41,51 @@ jobs:
|
||||
new_release_version: ${{ (steps.semantic.outputs.new_release_published == 'true' && steps.semantic.outputs.new_release_version) || (github.event_name == 'workflow_dispatch' && '0.0.0') }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- if: github.event_name != 'workflow_dispatch'
|
||||
name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
uses: cycjimmy/semantic-release-action@v3
|
||||
id: semantic
|
||||
with:
|
||||
semantic_version: 16
|
||||
extra_plugins: |
|
||||
conventional-changelog-eslint
|
||||
conventional-changelog-eslint@4.0.0
|
||||
branches: |
|
||||
[
|
||||
'main'
|
||||
'main',
|
||||
{
|
||||
name: 'alpha',
|
||||
prerelease: true
|
||||
},
|
||||
{
|
||||
name: 'beta',
|
||||
prerelease: true
|
||||
}
|
||||
]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-info:
|
||||
if: ${{ github.event_name == 'workflow_dispatch'}}
|
||||
build_ecr_image:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || needs.semantic_release.outputs.new_release_published == 'true' }}
|
||||
needs: [extract_environment, semantic_release]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on:
|
||||
[self-hosted, "prd"]
|
||||
|
||||
steps:
|
||||
- name: Create summary
|
||||
- name: Printing stats
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
run: echo "### Deploy da branch \`$GITHUB_REF_NAME\` no ambiente **$ENV** :rocket:" >> $GITHUB_STEP_SUMMARY
|
||||
run: echo ${GITHUB_REF#refs/heads/}
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || needs.semantic_release.outputs.new_release_published == 'true' }}
|
||||
needs: [extract_environment, semantic_release]
|
||||
runs-on:
|
||||
[self-hosted, "${{ needs.extract_environment.outputs.environment }}"]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Pip
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
python3 -m pip install docker-compose --upgrade
|
||||
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
python3 -m pip install awscli --upgrade
|
||||
@@ -96,14 +95,14 @@ jobs:
|
||||
python3 -m pip install awsebcli --upgrade
|
||||
|
||||
- name: Configure AWS Region
|
||||
uses: aws-actions/configure-aws-credentials@v1-node16
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
id: aws
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to AWS ECR
|
||||
id: login_ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Build, Tag, and Push Image to AWS ECR
|
||||
env:
|
||||
@@ -111,80 +110,43 @@ jobs:
|
||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
ACCOUNT_ID: ${{ steps.aws.outputs.aws-account-id }}
|
||||
run: |
|
||||
docker-compose -f build.docker-compose.yml build
|
||||
docker-compose -f build.docker-compose.yml push
|
||||
docker compose -f build.docker-compose.yml build
|
||||
docker compose -f build.docker-compose.yml push
|
||||
|
||||
- name: Create ZIP file to Deploy AWS Beanstalk
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
ACCOUNT_ID: ${{ steps.aws.outputs.aws-account-id }}
|
||||
NODE_EXPORTER_URL: ${{needs.extract_environment.outputs.environment == 'prd' && '611330257153.dkr.ecr.us-east-1.amazonaws.com\/monitoring\/node_exporter:latest' || '468720548566.dkr.ecr.us-east-1.amazonaws.com\/monitoring\/node_exporter:latest'}}
|
||||
run: |
|
||||
sed -i -e "s/\${ENV}/$ENV/g" docker-compose.yml
|
||||
sed -i -e "s/\${IMAGE_TAG}/$IMAGE_TAG/g" docker-compose.yml
|
||||
sed -i -e "s/\${ACCOUNT_ID}/$ACCOUNT_ID/g" docker-compose.yml
|
||||
sed -i -e "s/\${NODE_EXPORTER_URL}/$NODE_EXPORTER_URL/g" docker-compose.yml
|
||||
zip deploy.zip docker-compose.yml -r .ebextensions
|
||||
# - name: Create ZIP file to Deploy AWS Beanstalk
|
||||
# env:
|
||||
# ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
# IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
# ACCOUNT_ID: ${{ steps.aws.outputs.aws-account-id }}
|
||||
# NODE_EXPORTER_URL: ${{needs.extract_environment.outputs.environment == 'prd' && '611330257153.dkr.ecr.us-east-1.amazonaws.com\/monitoring\/node_exporter:latest' || '468720548566.dkr.ecr.us-east-1.amazonaws.com\/monitoring\/node_exporter:latest'}}
|
||||
# run: |
|
||||
# sed -i -e "s/\${ENV}/$ENV/g" docker-compose.yml
|
||||
# sed -i -e "s/\${IMAGE_TAG}/$IMAGE_TAG/g" docker-compose.yml
|
||||
# sed -i -e "s/\${ACCOUNT_ID}/$ACCOUNT_ID/g" docker-compose.yml
|
||||
# sed -i -e "s/\${NODE_EXPORTER_URL}/$NODE_EXPORTER_URL/g" docker-compose.yml
|
||||
# zip deploy.zip docker-compose.yml -r .ebextensions
|
||||
|
||||
- name: Deploy AWS Beanstalk
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
AWS_REGION: us-east-1
|
||||
APP_NAME: ${{ github.event.repository.name }}
|
||||
run: |
|
||||
eb use $APP_NAME-$ENV
|
||||
echo -e "deploy:\n artifact: deploy.zip" >> .elasticbeanstalk/config.yml
|
||||
eb deploy
|
||||
# - name: Deploy AWS Beanstalk
|
||||
# env:
|
||||
# ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
# AWS_REGION: us-east-1
|
||||
# APP_NAME: ${{ github.event.repository.name }}
|
||||
# run: |
|
||||
# eb use $APP_NAME-$ENV
|
||||
# echo -e "deploy:\n artifact: deploy.zip" >> .elasticbeanstalk/config.yml
|
||||
# eb deploy
|
||||
|
||||
- name: Remove Docker's Trash
|
||||
if: always()
|
||||
run: |
|
||||
docker system prune --volumes -a -f
|
||||
docker system df
|
||||
api_docs:
|
||||
needs: [extract_environment, semantic_release, deploy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Extract Docs BlockId and PageId
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
STG2_DOCS_BLOCK_ID: ${{ secrets.DEV_DOCS_BLOCK_ID }}
|
||||
STG2_DOCS_PAGE_ID: ${{ secrets.DEV_DOCS_PAGE_ID }}
|
||||
STG_DOCS_BLOCK_ID: ${{ secrets.STG_DOCS_BLOCK_ID }}
|
||||
STG_DOCS_PAGE_ID: ${{ secrets.STG_DOCS_PAGE_ID }}
|
||||
PRD_DOCS_BLOCK_ID: ${{ secrets.PRD_DOCS_BLOCK_ID }}
|
||||
PRD_DOCS_PAGE_ID: ${{ secrets.PRD_DOCS_PAGE_ID }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ $ENV == "stg2" ]; then
|
||||
echo "block_id=$STG2_DOCS_BLOCK_ID" >> $GITHUB_OUTPUT
|
||||
echo "page_id=$STG2_DOCS_PAGE_ID" >> $GITHUB_OUTPUT
|
||||
elif [ $ENV == "stg" ]; then
|
||||
echo "block_id=$STG_DOCS_BLOCK_ID" >> $GITHUB_OUTPUT
|
||||
echo "page_id=$STG_DOCS_PAGE_ID" >> $GITHUB_OUTPUT
|
||||
elif [ $ENV == "prd" ]; then
|
||||
echo "block_id=$PRD_DOCS_BLOCK_ID" >> $GITHUB_OUTPUT
|
||||
echo "page_id=$PRD_DOCS_PAGE_ID" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
id: extract_docs_info
|
||||
|
||||
- name: Generate API docs
|
||||
env:
|
||||
DOCS_URL: ${{ secrets.DOCS_URL }}
|
||||
DOCS_API_TOKEN: ${{ secrets.DOCS_API_TOKEN }}
|
||||
DOCS_PAGE_ID: ${{ steps.extract_docs_info.outputs.page_id }}
|
||||
DOCS_BLOCK_ID: ${{ steps.extract_docs_info.outputs.block_id }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
RELEASE_VERSION: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
run: |
|
||||
if [ ${EVENT} != "workflow_dispatch" ]; then
|
||||
sed -i -E "s/(\"title\": )\"Maestro.+\"/\1\"Maestro - $RELEASE_VERSION\"/g" docsfera.json
|
||||
fi
|
||||
sed -i -e "s/\${DOCS_API_TOKEN}/$DOCS_API_TOKEN/g" docsfera.json
|
||||
sed -i -e "s/\${DOCS_PAGE_ID}/$DOCS_PAGE_ID/g" docsfera.json
|
||||
sed -i -e "s/\${DOCS_BLOCK_ID}/$DOCS_BLOCK_ID/g" docsfera.json
|
||||
curl -X POST -H 'Content-Type: application/json' -d @docsfera.json $DOCS_URL
|
||||
k8s-deploy:
|
||||
needs: [extract_environment, semantic_release, build_ecr_image]
|
||||
uses: ./.github/workflows/k8s-deploy.yml
|
||||
with:
|
||||
cloud: 'oracle'
|
||||
environment: ${{ needs.extract_environment.outputs.environment }}
|
||||
image: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
name : K8s deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cloud:
|
||||
description: "Cloud provider for the deployment"
|
||||
required: true
|
||||
default: "azure"
|
||||
type: string
|
||||
environment:
|
||||
description: "Deployment environment"
|
||||
required: true
|
||||
default: "prd"
|
||||
type: string
|
||||
image:
|
||||
description: "Image Tag"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
azure:
|
||||
if: inputs.cloud == 'azure'
|
||||
runs-on: [self-hosted, "prd-azure"]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: 'v3.9.0'
|
||||
|
||||
- name: Install Azure ClI
|
||||
run: |
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
|
||||
|
||||
- uses: azure/login@v2
|
||||
with:
|
||||
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
|
||||
|
||||
- name: Authenticate with cluster
|
||||
env:
|
||||
CLUSTER_NAME: platform-${{ inputs.environment }}
|
||||
run: az aks get-credentials --resource-group dadosfera-prd --name ${CLUSTER_NAME} --overwrite-existing
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v1
|
||||
with:
|
||||
version: 'v1.30.1'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install Helmfile
|
||||
run: |
|
||||
curl -fsSLO https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
||||
tar -xzf helmfile_0.148.0_linux_amd64.tar.gz
|
||||
sudo mv helmfile /usr/local/bin/
|
||||
helmfile --version
|
||||
|
||||
- name: Install Helm Diff Plugin
|
||||
run: helm plugin install https://github.com/databus23/helm-diff || true
|
||||
|
||||
- name: Run Helmfile Apply
|
||||
env:
|
||||
ENV: ${{ inputs.environment }}
|
||||
IMAGE_TAG: ${{ inputs.image }}
|
||||
run: helmfile -f deploy/helmfiles/${ENV}.yaml sync --set image.tag=$IMAGE_TAG
|
||||
|
||||
oracle:
|
||||
if: inputs.cloud == 'oracle'
|
||||
runs-on: [self-hosted, "prd-oracle"]
|
||||
env:
|
||||
HOME: /home/runner
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: 'v3.9.0'
|
||||
|
||||
- name: Install OCI CLI
|
||||
env:
|
||||
HOME: /home/runner
|
||||
run: |
|
||||
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" -- --accept-all-defaults
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Configure OCI CLI
|
||||
run: |
|
||||
mkdir -p ~/.oci || true
|
||||
echo "${{ secrets.OCI_CONFIG }}" > ~/.oci/config
|
||||
echo "${{ secrets.OCI_PRIVATE_KEY }}" > ~/.oci/oci_api_key.pem
|
||||
chmod 600 ~/.oci/oci_api_key.pem
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install Helmfile
|
||||
run: |
|
||||
curl -fsSLO https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
||||
tar -xzf helmfile_0.148.0_linux_amd64.tar.gz
|
||||
sudo mv helmfile /usr/local/bin/
|
||||
helmfile --version
|
||||
|
||||
- name: Install Helm Diff Plugin
|
||||
run: helm plugin install https://github.com/databus23/helm-diff || true
|
||||
|
||||
- name: Authenticate with OKE cluster
|
||||
env:
|
||||
ENV: ${{ inputs.environment }}
|
||||
STG_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaagh3jvln52a3ebm3dodx6emmhv5bmfs7i7sv2k4zkbcbrzcl6v37q"
|
||||
PRD_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaanf3vptl6hc2tzd4enfd2hfpsht3wikxww5xejc3l7cwfm6l3sndq"
|
||||
run: |
|
||||
if [ "$ENV" = "stg" ]; then
|
||||
CLUSTER_ID=$STG_CLUSTER_ID
|
||||
elif [ "$ENV" = "prd" ]; then
|
||||
CLUSTER_ID=$PRD_CLUSTER_ID
|
||||
else
|
||||
echo "Unknown environment: $ENV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
oci ce cluster create-kubeconfig --cluster-id ${CLUSTER_ID} --file $HOME/.kube/config --region sa-saopaulo-1 --token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT
|
||||
|
||||
- name: Run Helmfile Apply
|
||||
env:
|
||||
ENV: ${{ inputs.environment }}
|
||||
IMAGE_TAG: ${{ inputs.image }}
|
||||
run: helmfile -f deploy/helmfiles/${ENV}.yaml sync --set image.tag=$IMAGE_TAG
|
||||
@@ -6,23 +6,18 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: self-hosted
|
||||
runs-on: [self-hosted, prd]
|
||||
env:
|
||||
APP_NAME: ${{ github.event.repository.name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
APP_NAME: ${{ github.event.repository.name }}
|
||||
run: |
|
||||
docker build -t $APP_NAME --target test .
|
||||
run: docker build -t ${APP_NAME}_teste --target test .
|
||||
|
||||
- name: Run Test
|
||||
env:
|
||||
ENV: test
|
||||
APP_NAME: ${{ github.event.repository.name }}
|
||||
run: |
|
||||
docker run --rm --entrypoint="npm" $APP_NAME run test
|
||||
run: docker run ${APP_NAME}_teste
|
||||
|
||||
- name: Remove Docker's Trash
|
||||
if: always()
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Validate K8S Modifications
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
extract_environment:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
environment: ${{ steps.extract_environment.outputs.environment }}
|
||||
steps:
|
||||
- name: Extract Environment
|
||||
run: |
|
||||
if [ "${{ github.event.pull_request.base.ref }}" == "main" ]; then
|
||||
echo "environment=prd" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event.pull_request.base.ref }}" == "beta" ]; then
|
||||
echo "environment=stg" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
id: extract_environment
|
||||
|
||||
helmfile-check:
|
||||
env:
|
||||
HOME: /home/runner
|
||||
needs: [extract_environment]
|
||||
environment: ${{ needs.extract_environment.outputs.environment }}
|
||||
runs-on: [self-hosted, "prd-oracle"]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: 'v3.9.0'
|
||||
|
||||
- name: Determine DNS_HOST based on environment
|
||||
id: set_dns
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
run: |
|
||||
if [ "$ENV" = "prd" ]; then
|
||||
echo "dns_host=dadosfera.ai" >> $GITHUB_OUTPUT
|
||||
elif [ "$ENV" = "stg" ]; then
|
||||
echo "dns_host=stg.dadosfera.ai" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install OCI CLI
|
||||
run: |
|
||||
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" -- --accept-all-defaults
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Configure OCI CLI
|
||||
run: |
|
||||
mkdir -p ~/.oci || true
|
||||
echo "${{ secrets.OCI_CONFIG }}" > ~/.oci/config
|
||||
echo "${{ secrets.OCI_PRIVATE_KEY }}" > ~/.oci/oci_api_key.pem
|
||||
chmod 600 ~/.oci/oci_api_key.pem
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install Helmfile
|
||||
run: |
|
||||
curl -fsSLO https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
||||
tar -xzf helmfile_0.148.0_linux_amd64.tar.gz
|
||||
sudo mv helmfile /usr/local/bin/
|
||||
helmfile --version
|
||||
|
||||
- name: Install Helm Diff plugin
|
||||
run: |
|
||||
helm plugin install https://github.com/databus23/helm-diff --version v3.9.3
|
||||
helm diff version
|
||||
|
||||
- name: Debug Helm env
|
||||
run: |
|
||||
helm env
|
||||
echo "HOME=$HOME"
|
||||
ls -R $HOME/.local/share/helm || true
|
||||
|
||||
- name: Authenticate with OKE cluster
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
STG_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaagh3jvln52a3ebm3dodx6emmhv5bmfs7i7sv2k4zkbcbrzcl6v37q"
|
||||
PRD_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaanf3vptl6hc2tzd4enfd2hfpsht3wikxww5xejc3l7cwfm6l3sndq"
|
||||
run: |
|
||||
if [ "$ENV" = "stg" ]; then
|
||||
CLUSTER_ID=$STG_CLUSTER_ID
|
||||
elif [ "$ENV" = "prd" ]; then
|
||||
CLUSTER_ID=$PRD_CLUSTER_ID
|
||||
else
|
||||
echo "Unknown environment: $ENV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
oci ce cluster create-kubeconfig --cluster-id ${CLUSTER_ID} --file $HOME/.kube/config --region sa-saopaulo-1 --token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v1
|
||||
with:
|
||||
version: 'v1.30.1'
|
||||
|
||||
- name: Run Helmfile Diff
|
||||
env:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
HELM_PLUGINS: /home/runner/.local/share/helm/plugins
|
||||
run: helmfile -f deploy/helmfiles/${ENV}.yaml diff
|
||||
Vendored
+1
@@ -12,6 +12,7 @@
|
||||
"start:debug"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeVersion": "18.17",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
|
||||
+62
-16
@@ -1,21 +1,67 @@
|
||||
# install all dependencies
|
||||
FROM node:18.3.0-alpine3.15 as packages
|
||||
FROM node:20-alpine AS base_image
|
||||
RUN npm install -g npm@10.8.2
|
||||
|
||||
FROM base_image AS build_base
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN apk add --no-cache aws-cli \
|
||||
&& aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1 \
|
||||
&& npm install
|
||||
RUN apk update
|
||||
# needed packages to build dependencies from source
|
||||
RUN apk add --no-cache \
|
||||
aws-cli \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
COPY package*.json ./
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
|
||||
# run aws cli without mounting secret, because CI already has AWS credentials
|
||||
FROM build_base AS ci_image
|
||||
RUN aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
|
||||
RUN npm ci --ignore-scripts
|
||||
COPY . .
|
||||
|
||||
|
||||
# unit test specific build
|
||||
FROM node:18.3.0-alpine3.15 as test
|
||||
WORKDIR /app
|
||||
COPY --from=packages /app/node_modules ./node_modules
|
||||
COPY . ./
|
||||
FROM ci_image AS test
|
||||
ENV DUC_URL=0.0.0.0:50051
|
||||
ENTRYPOINT ["npm", "run", "test"]
|
||||
|
||||
FROM node:18.3.0-alpine3.15
|
||||
WORKDIR /app
|
||||
COPY . /app/
|
||||
COPY --from=packages /app/node_modules ./node_modules
|
||||
|
||||
# dev build
|
||||
FROM build_base AS dev
|
||||
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
|
||||
aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
|
||||
# flag --build-from-source is required to force-build sqlite3
|
||||
RUN npm ci --ignore-scripts
|
||||
COPY . .
|
||||
ENTRYPOINT npm run start:dev
|
||||
|
||||
|
||||
FROM ci_image AS prod_build
|
||||
RUN npm run build
|
||||
EXPOSE 3333
|
||||
ENTRYPOINT npm run start
|
||||
|
||||
|
||||
FROM base_image
|
||||
WORKDIR /app
|
||||
COPY --from=prod_build /app/dist ./dist
|
||||
COPY --from=prod_build /app/node_modules ./node_modules
|
||||
COPY --from=prod_build /app/package*.json ./
|
||||
RUN apk update
|
||||
# needed packages to build dependencies from source
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
ENTRYPOINT npm run start:prod
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
FROM node:22-alpine AS base_image
|
||||
RUN npm install -g npm@latest
|
||||
|
||||
FROM base_image AS build_base
|
||||
WORKDIR /app
|
||||
RUN apk update
|
||||
RUN apk add --no-cache \
|
||||
aws-cli \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
COPY package*.json ./
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
|
||||
# Local build with secrets
|
||||
FROM build_base AS build
|
||||
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
|
||||
aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
|
||||
RUN npm ci --ignore-scripts
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM base_image
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/package*.json ./
|
||||
RUN apk update
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
ENTRYPOINT ["npm", "run", "start:prod"]
|
||||
@@ -0,0 +1,10 @@
|
||||
LICENSE
|
||||
Copyright (C) 2023 Dadosfera
|
||||
|
||||
All rights reserved.
|
||||
|
||||
This software and related documentation are provided under a license agreement containing restrictions on use and disclosure and are protected by intellectual property laws. Except as expressly permitted in your license agreement or allowed by law, you may not use, copy, reproduce, translate, broadcast, modify, license, transmit, distribute, exhibit, perform, publish, or display any part, in any form, or by any means.
|
||||
|
||||
Reverse engineering, disassembly, or decompilation of this software, unless required by law for interoperability, is prohibited.
|
||||
|
||||
If you have any questions about this, please contact Dadosfera.ai at legal@dadosfera.ai
|
||||
@@ -1,68 +1,103 @@
|
||||
<p align="center">
|
||||
<image src="./assets/maestro.svg" style="width:10rem">
|
||||
</p>
|
||||
</p>
|
||||
|
||||
# Maestro
|
||||
Maestro is the Dadosfera's gateway, it's responsible for the communication between the frontend application and Dadosfera's microservices.
|
||||
|
||||
## 🚀 Starting
|
||||
These instructions will allow you to get a working copy of the project on your local machine for development and testing purposes.
|
||||
|
||||
### 📋 Requirements
|
||||
- [NodeJS v18.3.0 LTS / NPM v8.11](https://nodejs.org/pt-br/download/) (you can opt to use [NVM](https://github.com/nvm-sh/nvm) to easily manage node versions)
|
||||
- Request access to AWS Console dev account for **all services** (avoid gradually asking for each needed service. it will slow down your development cycle)
|
||||
- Create your Access Key on the "Security credentials" menu
|
||||
- Set the Access Key on your local development machine
|
||||
- Request access to the dev, stg and prd VPNs
|
||||
Maestro é a API principal da Dadosfera. É responsável pela comunicação do Frontend com nossos microsserviços.
|
||||
|
||||
### 🔧 Installation<a id="installation"></a>
|
||||
- Clone the repository
|
||||
- SSH
|
||||
```
|
||||
git clone git@github.com:dadosfera/maestro.git
|
||||
```
|
||||
or
|
||||
```mermaid
|
||||
graph TD;
|
||||
Frontend<-->Maestro;
|
||||
Maestro<-->duc;
|
||||
Maestro<-->pi-factory;
|
||||
Maestro<-->in-factory;
|
||||
```
|
||||
|
||||
- HTTPS
|
||||
```
|
||||
git clone https://github.com/dadosfera/maestro.git
|
||||
```
|
||||
É uma API REST, desenvolvida em NodeJs utilizando o Framework [NestJs](https://docs.nestjs.com/).
|
||||
|
||||
- Select the correct node version (optional, only if using [NVM](https://github.com/nvm-sh/nvm)):
|
||||
```sh
|
||||
nvm use
|
||||
```
|
||||
## 🚀 Iniciando
|
||||
|
||||
- Install the project dependencies:
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
Estas instruções permitirão que você obtenha uma cópia funcional do projeto em sua máquina local para desenvolvimento e testes.
|
||||
|
||||
- Setup the following enviroment variables:
|
||||
```
|
||||
ENV=
|
||||
DUC_URL=
|
||||
INFACTORY_URL=
|
||||
OTFACTORY_URL=
|
||||
PIFACTORY_URL=
|
||||
SM_OAUTH_PATH=
|
||||
```
|
||||
### 📋 Requisitos
|
||||
|
||||
- Start the server:
|
||||
```sh
|
||||
# dev mode
|
||||
npm run start:dev
|
||||
- [NodeJS v18.17 / NPM v9.6.7](https://nodejs.org/pt-br/download/)
|
||||
|
||||
# or in debug mode
|
||||
npm run start:debug
|
||||
```
|
||||
The service should start successfully.
|
||||
> Dica: Utilize [NVM](https://github.com/nvm-sh/nvm) para gerenciar facilmente as versões do node
|
||||
|
||||
## Authentication decorators
|
||||
Maestro have utilities to ease the user authentication on every controller and route. The following decorators are available:
|
||||
- Solicite acesso à conta de desenvolvimento do AWS Console para **todos os serviços necessários**
|
||||
- Crie sua Access Key no menu "Security credentials"
|
||||
- Defina a Access Key em sua máquina de desenvolvimento local
|
||||
- Solicite acesso às VPNs de stg e prd
|
||||
|
||||
### 🔧 Instalação<a id="installation"></a>
|
||||
|
||||
- Clone o repositório
|
||||
|
||||
```sh
|
||||
git clone git@github.com:dadosfera/maestro.git
|
||||
```
|
||||
|
||||
- Selecione a versão correta do node (opcional, apenas se estiver usando o [NVM](https://github.com/nvm-sh/nvm)):
|
||||
|
||||
```sh
|
||||
nvm use
|
||||
```
|
||||
|
||||
- Instale as dependências do projeto:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
- Configure as seguintes variáveis de ambiente:
|
||||
|
||||
```
|
||||
ENV=
|
||||
DUC_URL=
|
||||
INFACTORY_URL=
|
||||
OTFACTORY_URL=
|
||||
PIFACTORY_URL=
|
||||
SM_OAUTH_PATH=
|
||||
```
|
||||
|
||||
- Inicie o servidor:
|
||||
|
||||
```sh
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
> Se preferir, utilize o debbuger do VSCode apertando F5
|
||||
|
||||
O serviço deve iniciar com sucesso.
|
||||
|
||||
## 📄 Documentação
|
||||
|
||||
O NestJs facilita a documentação de cada rota usando decoradores em todas as propriedades de solicitações e respostas. Ele gera automaticamente um swagger para as informações fornecidas e fornece uma rota para acessá-lo em http://localhost:3333/api. Para mais informações, consulte a [documentação oficial](https://docs.nestjs.com/openapi/introduction).
|
||||
|
||||
### - Documentações múltiplas
|
||||
|
||||
Atualmente, estamos gerando **2 documentações diferentes**: Interna e Externa.
|
||||
|
||||
Todas as rotas que têm o decorador `@ApiInternalOnly()` não serão visíveis no swagger da API **Externa**.
|
||||
|
||||
- Quando você inicia a aplicação com `npm run start:dev`, ela servirá e gerará o JSON do swagger da API **Interna**
|
||||
|
||||
- Quando você executa `npm run docs`, ele gerará o JSON do swagger de ambas as APIs **Interna** e **Externa** e os salvará em `docsfera.json` e `docsfera.external.json`, respectivamente;
|
||||
|
||||
> Link para documentação interna: https://dadosfera.github.io/docsfera
|
||||
|
||||
É importante executar `npm run docs` antes de cada implantação para que sempre tenhamos as documentações mais atualizadas publicadas.
|
||||
|
||||
## Decoradores de autenticação
|
||||
|
||||
O Maestro possui utilitários para facilitar a autenticação do usuário em todos os controladores e rotas. Os seguintes decoradores estão disponíveis:
|
||||
|
||||
### `@Authenticated`
|
||||
If the user must be authenticated to make request, we can use the `@Authenticated` decorator in the controller or route, as needed.
|
||||
|
||||
Se o usuário precisar estar autenticado para fazer uma solicitação, podemos usar o decorador `@Authenticated` no controlador ou rota, conforme necessário.
|
||||
|
||||
```ts
|
||||
import { Authenticated } from '../../authentication/authentication.decorator';
|
||||
@@ -74,7 +109,7 @@ class FooController {
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
this.logger.info('user is authenticated!');
|
||||
this.logger.info('usuário está autenticado!');
|
||||
|
||||
return { authenticated: true };
|
||||
}
|
||||
@@ -91,14 +126,14 @@ class FooController {
|
||||
@Get('bar')
|
||||
@Authenticated()
|
||||
async getBar() {
|
||||
this.logger.info('user is authenticated!');
|
||||
this.logger.info('usuário está autenticado!');
|
||||
|
||||
return { authenticated: true };
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
async postBar() {
|
||||
this.logger.info('user is NOT authenticated!');
|
||||
this.logger.info('usuário NÃO está autenticado!');
|
||||
|
||||
return { authenticated: false };
|
||||
}
|
||||
@@ -106,7 +141,8 @@ class FooController {
|
||||
```
|
||||
|
||||
### `@RequireAllPermissions`
|
||||
This decorator requires that **all permissions** listed are granted to the requesting user.
|
||||
|
||||
Este decorador exige que **todas as permissões** listadas sejam concedidas ao usuário solicitante.
|
||||
|
||||
```ts
|
||||
import { RequireAllPermissions } from '../../authentication/authentication.decorator';
|
||||
@@ -125,7 +161,8 @@ class FooController {
|
||||
```
|
||||
|
||||
### `@RequireSomePermission`
|
||||
In the following case, the user is required to have **at least one** listed permission.
|
||||
|
||||
No caso seguinte, é necessário que o usuário tenha **pelo menos uma** das permissões listadas.
|
||||
|
||||
```ts
|
||||
import { RequireSomePermission } from '../../authentication/authentication.decorator';
|
||||
@@ -144,17 +181,19 @@ class FooController {
|
||||
```
|
||||
|
||||
### `@AuthenticateCondition`
|
||||
If a more complicated authentication check needs to be done, we can use the `@AuthenticateCondition` decorator to define it. The custom function must return `true` to authenticate the request.
|
||||
|
||||
In the following example:
|
||||
- all routes on the `FooController` controller can only be requested from localhost
|
||||
- `POST /foo/bar` can only be requested from localhost **and** by users from customer id `111...eef`
|
||||
Se for necessária uma verificação de autenticação mais complicada, podemos usar o decorador `@AuthenticateCondition` para defini-la. A função personalizada deve retornar `true` para autenticar a solicitação.
|
||||
|
||||
No exemplo a seguir:
|
||||
|
||||
- todas as rotas no controlador `FooController` só podem ser solicitadas a partir do localhost
|
||||
- `POST /foo/bar` só pode ser solicitado a partir do localhost **e** por usuários do cliente com id `111...eef`
|
||||
|
||||
```ts
|
||||
import { AuthenticateCondition } from '../../authentication/authentication.decorator';
|
||||
|
||||
@Controller('foo')
|
||||
// allow requests only from localhost
|
||||
// permitir solicitações apenas do localhost
|
||||
@AuthenticateCondition((request: Request) => request.ip === '::ffff:127.0.0.1')
|
||||
class FooController {
|
||||
/* ... */
|
||||
@@ -165,61 +204,68 @@ class FooController {
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
// allow requests only from a specific customer
|
||||
@AuthenticateCondition((request: Request, user: RequestUser) => user.customer_id === '1113e943-2187-4fdd-9c2c-54338fedaeef')
|
||||
// permitir solicitações apenas de um cliente específico
|
||||
@AuthenticateCondition(
|
||||
(request: Request, user: RequestUser) =>
|
||||
user.customer_id === '1113e943-2187-4fdd-9c2c-54338fedaeef',
|
||||
)
|
||||
async postBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Note on authentication decorators
|
||||
- The old authentication method placed the user data in the `request.body.info` field, this imposes certain issues regarding the request body because this data should be from the frontend without any modification by Maestro. Now this usage is ⚠️ **DEPRECATED** ⚠️. We are working to migrate to the `@User` parameter decorator. The old method is working while the migration is in progress.
|
||||
### Nota sobre decoradores de autenticação
|
||||
|
||||
- Authentication decorators can be used together and all of them **must** pass to the request be authenticated, but in the general case you don't need to (*and wouldn't like to...*) use all of them together, as you can code all of the authentication logic in the `@AuthenticateCondition` decorator.
|
||||
- O método antigo de autenticação colocava os dados do usuário no campo `request.body.info`, o que impõe certos problemas em relação ao corpo da solicitação, pois esses dados devem vir do frontend sem qualquer modificação pelo Maestro. Agora, esse uso está ⚠️ **DESCONTINUADO** ⚠️. Estamos trabalhando para migrar para o decorador de parâmetro `@User`. O método antigo está funcionando enquanto a migração está em andamento.
|
||||
|
||||
```ts
|
||||
import {
|
||||
RequireAllPermissions,
|
||||
RequireSomePermission,
|
||||
AuthenticateCondition,
|
||||
} from '../../authentication/authentication.decorator';
|
||||
import { PERMISSIONS } from '../../authentication/permissions.enum';
|
||||
import { Waa, Baz } from './authenticationFunctions'
|
||||
- Os decoradores de autenticação podem ser usados juntos e todos eles **devem** passar para que a solicitação seja autenticada, mas, no caso geral, você não precisa (_e não gostaria de..._) usar todos eles juntos, pois você pode codificar toda a lógica de autenticação no decorador `@AuthenticateCondition`.
|
||||
|
||||
@Controller('foo')
|
||||
@RequireAllPermissions(Permissions.FOO.USE, Permissions.FOO.REQUEST)
|
||||
@RequireSomePermission(Permissions.FOO.MANAGE, Permissions.FOO.ADMIN)
|
||||
@AuthenticateCondition(Waa)
|
||||
@AuthenticateCondition(Baz)
|
||||
class FooController {
|
||||
```ts
|
||||
import {
|
||||
RequireAllPermissions,
|
||||
RequireSomePermission,
|
||||
AuthenticateCondition,
|
||||
} from '../../authentication/authentication.decorator';
|
||||
import { PERMISSIONS } from '../../authentication/permissions.enum';
|
||||
import { Waa, Baz } from './authenticationFunctions';
|
||||
|
||||
@Controller('foo')
|
||||
@RequireAllPermissions(Permissions.FOO.USE, Permissions.FOO.REQUEST)
|
||||
@RequireSomePermission(Permissions.FOO.MANAGE, Permissions.FOO.ADMIN)
|
||||
@AuthenticateCondition(Waa)
|
||||
@AuthenticateCondition(Baz)
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE, Permissions.BAR.USE, Permissions.BAR.REQUEST)
|
||||
async postBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- If `@RequireAllPermissions` and `@RequireSomePermission` are used with **only a single permission**, they present the **exactly same behavior**.
|
||||
@Post('bar')
|
||||
@RequireAllPermissions(
|
||||
Permissions.BAR.MANAGE,
|
||||
Permissions.BAR.USE,
|
||||
Permissions.BAR.REQUEST,
|
||||
)
|
||||
async postBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// same behavior
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE)
|
||||
@RequireSomePermission(Permissions.BAR.MANAGE)
|
||||
```
|
||||
- Se `@RequireAllPermissions` e `@RequireSomePermission` forem usados com **apenas uma única permissão**, eles apresentam o **exatamente mesmo comportamento**.
|
||||
|
||||
```ts
|
||||
// mesmo comportamento
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE)
|
||||
@RequireSomePermission(Permissions.BAR.MANAGE)
|
||||
```
|
||||
|
||||
## Decorador de parâmetro `@User`
|
||||
|
||||
## `@User` parameter decorator
|
||||
The requesting user data can be obtained using the @User parameter decorator, like in the following snippet:
|
||||
Os dados do usuário solicitante podem ser obtidos usando o decorador de parâmetro @User, como no exemplo a seguir:
|
||||
|
||||
```ts
|
||||
import { User, RequestUser } from '../../authentication/user.decorator';
|
||||
@@ -237,7 +283,7 @@ class FooController {
|
||||
}
|
||||
```
|
||||
|
||||
If the user is required to be logged in, set `required` to `true`, as you would want in the `change-password` operation:
|
||||
Se o usuário precisar estar logado, defina `required` como `true`, como por exemplo:
|
||||
|
||||
```ts
|
||||
import { User, RequestUser } from '../../authentication/user.decorator';
|
||||
@@ -263,39 +309,46 @@ class UserController {
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 Development
|
||||
### ⌨️ Coding Style
|
||||
By default, we use [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) with default settings.
|
||||
## 📦 Desenvolvimento
|
||||
|
||||
**We recommend using Visual Studio Code and installing the recommended extensions to ease the development process.**
|
||||
### ⌨️ Estilo de Codificação
|
||||
|
||||
### Commits pattern
|
||||
Our workflow pipeline follows the conventional commits specs ([cheat sheets](https://cheatography.com/albelop/cheat-sheets/conventional-commits/)) to release versions accordingly.
|
||||
Por padrão, usamos [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) com configurações padrão.
|
||||
|
||||
Format: `<type>[optional scope]: <description>`
|
||||
**Recomendamos usar o Visual Studio Code e instalar as extensões recomendadas para facilitar o processo de desenvolvimento.**
|
||||
|
||||
Example: `FIX: ensure Range headers adhere more closely to RFC 2616`
|
||||
### Padrão de commits
|
||||
|
||||
### Branching naming convention
|
||||
- **Feature**: Any code changes for a new module or use case should be done on a feature branch. This branch is created based on the `main` branch. When all changes are done, a Pull Request/Merge Request is needed to put all of these changes back to the `main` branch. Examples: `feature/integrate-swagger`, `feature/JIRA-1234`, `feature/JIRA-1234_support-dark-theme`.
|
||||
Nosso pipeline de fluxo de trabalho segue as especificações de commits convencionais ([cheat sheets](https://cheatography.com/albelop/cheat-sheets/conventional-commits/)) para liberar versões conforme necessário.
|
||||
|
||||
**It is recommended to use all lower caps letters and hyphen (-) to separate words unless it is a specific item name or ID. Underscore (_) could be used to separate the ID and description.**
|
||||
Formato: `<type>[optional scope]: <description>`
|
||||
|
||||
- **Bug Fix**: If the code changes made from the feature branch were rejected after a release, sprint or demo, any necessary fixes after that should be done on the bugfix branch. Examples: `bugfix/more-gray-shades`, `bugfix/JIRA-1444_gray-on-blur-fix`.
|
||||
Exemplo: `FIX: ensure Range headers adhere more closely to RFC 2616`
|
||||
|
||||
- **Hot Fix**: If there is a need to fix a blocker, do a temporary patch, apply a critical framework or configuration change that should be handled immediately, it should be created as a Hotfix. Examples: `hotfix/disable-endpoint-zero-day-exploit`, `hotfix/increase-scaling-threshold`.
|
||||
### Convenção de nomenclatura de branchs
|
||||
|
||||
- **Experimental**: A branch for playing around. Any new feature or idea that is not part of a release or a sprint. Example: `experimental/dark-theme-support`.
|
||||
- **Feature**: Quaisquer alterações de código para um novo módulo ou caso de uso devem ser feitas em uma branch de feature. Esta branch é criada com base na branch `main`. Quando todas as alterações estiverem concluídas, será necessário um Pull Request/Merge Request para colocar todas essas alterações de volta na branch `main`. Exemplos: `feature/integrate-swagger`, `feature/JIRA-1234`, `feature/JIRA-1234_support-dark-theme`.
|
||||
|
||||
### Making a Pull Request
|
||||
1. Commit your changes
|
||||
2. Open the Pull Request on GitHub
|
||||
3. Send Pull Request link in Microsfera Google Chat Group for review and possible approval
|
||||
**Recomenda-se usar todas as letras em minúsculas e hífen (-) para separar palavras, a menos que seja um nome ou ID de item específico. O sublinhado (\_) pode ser usado para separar o ID e a descrição.**
|
||||
|
||||
## 🛠️ Built with
|
||||
Some technologies used in this project:
|
||||
- **Bug Fix**: Se as alterações de código feitas na branch de feature foram rejeitadas após um lançamento, sprint ou demo, quaisquer correções necessárias após isso devem ser feitas na branch de correção de bug. Exemplos: `bugfix/more-gray-shades`, `bugfix/JIRA-1444_gray-on-blur-fix`.
|
||||
|
||||
- [NestJS](https://docs.nestjs.com) - Framework for building efficient and scalable NodeJS server-side applications
|
||||
- **Hot Fix**: Se houver necessidade de corrigir um bloqueador, fazer um patch temporário, aplicar uma mudança crítica de framework ou configuração que deva ser tratada imediatamente, ela deve ser criada como um Hotfix. Exemplos: `hotfix/disable-endpoint-zero-day-exploit`, `hotfix/increase-scaling-threshold`.
|
||||
|
||||
## ⚙️ Back-end Architecture
|
||||
The architecture can be found at [this link](https://sites.google.com/dadosfera.ai/wikidoproduto/time/back-end).
|
||||
- **Experimental**: Uma branch para experimentar. Qualquer nova feature ou ideia que não faça parte de um lançamento ou sprint. Exemplo: `experimental/dark-theme-support`.
|
||||
|
||||
### Fazendo um Pull Request
|
||||
|
||||
1. Comite suas alterações
|
||||
2. Abra o Pull Request no GitHub
|
||||
3. Envie o link do Pull Request no grupo de chat do Google da Microsfera para revisão e possível aprovação
|
||||
|
||||
## 🛠️ Construído com
|
||||
|
||||
Algumas tecnologias usadas neste projeto:
|
||||
|
||||
- [NestJS](https://docs.nestjs.com) - Framework para construir aplicações NodeJS eficientes e escaláveis no lado do servidor
|
||||
|
||||
## ⚙️ Arquitetura de Back-end
|
||||
|
||||
A arquitetura pode ser encontrada neste [link](https://sites.google.com/dadosfera.ai/wikidoproduto/time/back-end).
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
services:
|
||||
maestro:
|
||||
build: .
|
||||
image: dadosfera/maestro_${ENV}:${IMAGE_TAG}
|
||||
@@ -1,5 +1,4 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
maestro:
|
||||
build: .
|
||||
image: ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_${ENV}:${IMAGE_TAG}
|
||||
image: ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_prd:${IMAGE_TAG}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: maestro
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
@@ -0,0 +1,150 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Values.app_name }}
|
||||
namespace: applications
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Values.app_name }}
|
||||
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: {{ .Values.imagePullSecrets }}
|
||||
nodeSelector:
|
||||
"beta.kubernetes.io/os": linux
|
||||
{{- if .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml .Values.affinity | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
tolerations:
|
||||
- key: "kubernetes.azure.com/scalesetpriority"
|
||||
operator: "Equal"
|
||||
value: "spot"
|
||||
effect: "NoSchedule"
|
||||
|
||||
containers:
|
||||
- name: maestro
|
||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.containerPort }}
|
||||
{{- if .Values.resources }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
# Auth Provider Configuration (cognito or keycloak)
|
||||
- name: AUTH_PROVIDER
|
||||
value: {{ .Values.maestro.auth_provider | default "cognito" | quote }}
|
||||
- name: AWS_IDENTITY_POOL_ID
|
||||
value: {{ .Values.maestro.aws_identity_pool_id }}
|
||||
- name: AWS_REGION
|
||||
value: "us-east-1"
|
||||
- name: BASE_HOST
|
||||
value: "maestro_prd"
|
||||
- name: BUCKET_CUSTOMER_CSV_ASSETS
|
||||
value: {{ .Values.maestro.bucket_customer_csv_assets }}
|
||||
- name: CONNECTORS_INDEX
|
||||
value: "connectors"
|
||||
- name: DUC_URL
|
||||
value: {{ .Values.maestro.duc_url }}
|
||||
- name: ELASTIC_APM_ENVIRONMENT
|
||||
value: {{ .Values.maestro.env }}
|
||||
- name: ELASTIC_APM_SERVER_URL
|
||||
value: "https://apm-server.dadosfera.ai"
|
||||
- name: ELASTIC_APM_SERVICE_NAME
|
||||
value: {{ .Values.maestro.apm_service }}
|
||||
- name: ENV
|
||||
value: {{ .Values.maestro.env }}
|
||||
- name: INFACTORY_URL
|
||||
value: {{ .Values.maestro.in_factory_url }}
|
||||
- name: LOGGER_GELF_HOST
|
||||
value: "logstash-pipelines.dadosfera.ai"
|
||||
- name: LOGGER_GELF_PORT
|
||||
value: "{{ .Values.maestro.logger_gelf_port }}"
|
||||
- name: LOGGER_CONSOLE_EXTRA
|
||||
value: "true"
|
||||
- name: NIMBUS_BASE_URL
|
||||
value: "http://nimbus-api"
|
||||
- name: NPM_TOKEN
|
||||
value: {{ .Values.maestro.npm_token }}
|
||||
- name: PB_TOKEN_PATH
|
||||
value: {{ .Values.maestro.pb_token_path }}
|
||||
- name: PIFACTORY_URL
|
||||
value: {{ .Values.maestro.pi_factory_url }}
|
||||
- name: SM_OAUTH_PATH
|
||||
value: {{ .Values.maestro.sm_oauth_path }}
|
||||
- name: TRFACTORY_URL
|
||||
value: {{ .Values.maestro.tr_factory_url }}
|
||||
- name: UPLOAD_FILE_AGENT_CONNECTION
|
||||
value: {{ .Values.maestro.upload_file_agent_connection }}
|
||||
- name: OPEN_CUSTOMER_ID
|
||||
value: {{ .Values.maestro.open_customer_id }}
|
||||
- name: OPEN_GROUP_ID
|
||||
value: {{ .Values.maestro.open_group_id }}
|
||||
- name: DEDICATED_PROXY
|
||||
value: {{ .Values.maestro.dedicated_proxy }}
|
||||
- name: COOKIE_SECRET
|
||||
value: {{ .Values.maestro.cookie_secret }}
|
||||
- name: REDIS_DATABASE
|
||||
value: "{{ .Values.maestro.redis_database }}"
|
||||
- name: REDIS_HOST
|
||||
value: {{ .Values.maestro.redis_host }}
|
||||
- name: REDIS_PORT
|
||||
value: "{{ .Values.maestro.redis_port }}"
|
||||
- name: REDIS_TLS
|
||||
value: "{{ .Values.maestro.redis_tls }}"
|
||||
- name: PLATFORM_API_URL
|
||||
value: {{ .Values.maestro.platform_api_url }}
|
||||
- name: CONNECTIONS_API_URL
|
||||
value: {{ .Values.maestro.connections_api_url | default "" | quote }}
|
||||
- name: STORAGE_EXPLORER_API_URL
|
||||
value: {{ .Values.maestro.storage_explorer_api_url | quote }}
|
||||
- name: FIREBASE_BASE_URL
|
||||
value: {{ .Values.maestro.firebase_base_url }}
|
||||
- name: JWT_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-duc
|
||||
key: jwt_token
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: AWS_ACCESS_KEY_ID
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: AWS_SECRET_ACCESS_KEY
|
||||
- name: AWS_DEFAULT_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: AWS_DEFAULT_REGION
|
||||
# Elasticsearch
|
||||
- name: ELASTICSEARCH_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: ELASTICSEARCH_URL
|
||||
- name: ELASTICSEARCH_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: ELASTICSEARCH_API_KEY
|
||||
@@ -0,0 +1,41 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: "69.49.241.121/32" # hostgator ip
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/server-snippet: |
|
||||
underscores_in_headers on;
|
||||
ignore_invalid_headers on;
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
|
||||
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "64k"
|
||||
{{- if .Values.maestro.restricted_ip}}
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.maestro.restricted_ip }}
|
||||
{{- end }}
|
||||
|
||||
generation: 1
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
{{- if .Values.maestro.dedicated_proxy}}
|
||||
name: open-data-{{ .Values.app_name }}
|
||||
{{- else }}
|
||||
name: open-data
|
||||
{{- end }}
|
||||
namespace: applications
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: {{ .Values.hostname }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Values.app_name }}
|
||||
port:
|
||||
number: {{ .Values.ingress.port }}
|
||||
path: /open-data/sharing-ocean-data
|
||||
pathType: Prefix
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/server-snippet: |
|
||||
underscores_in_headers on;
|
||||
ignore_invalid_headers on;
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
|
||||
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "64k"
|
||||
{{- if .Values.maestro.restricted_ip}}
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.maestro.restricted_ip }}
|
||||
{{- end }}
|
||||
|
||||
generation: 1
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
name: {{ .Values.app_name }}
|
||||
namespace: applications
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: {{ .Values.hostname }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Values.app_name }}
|
||||
port:
|
||||
number: {{ .Values.ingress.port }}
|
||||
path: /
|
||||
pathType: Prefix
|
||||
@@ -0,0 +1,52 @@
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
namespace: applications
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: secretsmanager-prd
|
||||
kind: SecretStore
|
||||
target:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: AWS_ACCESS_KEY_ID
|
||||
remoteRef:
|
||||
key: prd/microservices/aws_credentials/maestro
|
||||
version: "AWSCURRENT"
|
||||
property: AWS_ACCESS_KEY_ID
|
||||
|
||||
- secretKey: AWS_SECRET_ACCESS_KEY
|
||||
remoteRef:
|
||||
key: prd/microservices/aws_credentials/maestro
|
||||
version: "AWSCURRENT"
|
||||
property: AWS_SECRET_ACCESS_KEY
|
||||
|
||||
- secretKey: AWS_DEFAULT_REGION
|
||||
remoteRef:
|
||||
key: prd/microservices/aws_credentials/maestro
|
||||
version: "AWSCURRENT"
|
||||
property: AWS_DEFAULT_REGION
|
||||
|
||||
- secretKey: jwt_token
|
||||
remoteRef:
|
||||
key: {{ .Values.maestro.jwt_token_secret_id }}
|
||||
version: "AWSCURRENT"
|
||||
property: token
|
||||
|
||||
- secretKey: ELASTICSEARCH_URL
|
||||
remoteRef:
|
||||
key: {{ .Values.maestro.env }}/microservices/elasticsearch
|
||||
version: "AWSCURRENT"
|
||||
property: ELASTICSEARCH_URL
|
||||
|
||||
- secretKey: ELASTICSEARCH_API_KEY
|
||||
remoteRef:
|
||||
key: {{ .Values.maestro.env }}/microservices/elasticsearch
|
||||
version: "AWSCURRENT"
|
||||
property: ELASTICSEARCH_API_KEY
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Values.app_name }}
|
||||
namespace: applications
|
||||
labels:
|
||||
app: {{ .Values.app_name }}
|
||||
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: {{ .Values.app_name }}
|
||||
protocol: TCP
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.targetPort }}
|
||||
selector:
|
||||
app: {{ .Values.app_name }}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
maestro:
|
||||
env: stg
|
||||
duc_url: duc.stg.dadosfera.ai
|
||||
pi_factory_url: pi-factory.stg.dadosfera.ai
|
||||
in_factory_url: in-factory.stg.dadosfera.ai
|
||||
tr_factory_url: in-factory.stg.dadosfera.ai
|
||||
open_customer_id: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||
open_group_id: e3f98a2f-7748-4981-8505-7695c8ca8218
|
||||
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
|
||||
redis_database: "1"
|
||||
platform_api_url: https://xs2hkhq07k.execute-api.us-east-1.amazonaws.com
|
||||
connections_api_url: https://iy40eans64.execute-api.us-east-1.amazonaws.com
|
||||
storage_explorer_api_url: "http://storage-explorer-{customer}.data-apps.svc.cluster.local:8000/api"
|
||||
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/stg
|
||||
|
||||
hostname: maestro.stg.dadosfera.ai
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
affinity: null
|
||||
@@ -0,0 +1,76 @@
|
||||
# Default values for metabase.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 3
|
||||
hostname: maestro.dadosfera.ai
|
||||
image:
|
||||
repository: 611330257153.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_prd
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: 1.56.0
|
||||
app_name: maestro
|
||||
containerPort: 3333
|
||||
imagePullSecrets: "applications-secrets-ecr-auth-token-external-secret"
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3333
|
||||
targetPort: 3333
|
||||
ingress:
|
||||
enabled: false
|
||||
port: 3333
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 1500Mi
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
maestro:
|
||||
# Auth provider: "cognito" (default) or "keycloak"
|
||||
# Note: maestro doesn't connect to Keycloak directly, only duc does
|
||||
auth_provider: "cognito"
|
||||
aws_identity_pool_id: "us-east-1_Mrezsw9Sn"
|
||||
duc_url: duc.dadosfera.ai
|
||||
in_factory_url: in-factory.dadosfera.ai
|
||||
bucket_customer_csv_assets: "customers-csv-assets-prd-611330257153"
|
||||
env: prd
|
||||
apm_service: maestro
|
||||
jwt_token_secret_id: prd/root/jwt_token
|
||||
logger_gelf_port: "1026"
|
||||
npm_token: npm_Xp17h3daMkORcZ55NY95Ez4gRfdzsQ3UvINP
|
||||
pb_token_path: prd/root/productboard_token
|
||||
pi_factory_url: pi-factory.dadosfera.ai
|
||||
sm_oauth_path: prd/root/oauth_applications
|
||||
tr_factory_url: in-factory.dadosfera.ai
|
||||
upload_file_agent_connection: cbc2f881-58c4-4d60-8003-0979b0b5b911
|
||||
open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e
|
||||
open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9
|
||||
platform_api_url: https://oz8v2zid1e.execute-api.us-east-1.amazonaws.com
|
||||
storage_explorer_api_url: "https://storage-explorer-{customer}.dadosfera.ai/api"
|
||||
dedicated_proxy: ""
|
||||
restricted_ip: ""
|
||||
redis_host: "aaapzppmlyamkocqwstpo7zvopczyyiyuy6xzm2g6c5k4mq3a66be4a-0.redis.sa-saopaulo-1.oci.oraclecloud.com"
|
||||
redis_port: "6379"
|
||||
redis_database: "0"
|
||||
redis_tls: "true"
|
||||
cookie_secret: "13cc5e136d3074bcc05bec8697092ec1f5f376bf"
|
||||
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/prd
|
||||
connections_api_url: https://wv6fw7ayj4.execute-api.us-east-1.amazonaws.com
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: name
|
||||
operator: In
|
||||
values:
|
||||
- product
|
||||
@@ -0,0 +1,56 @@
|
||||
releases:
|
||||
- name: maestro
|
||||
chart: ../helm-chart
|
||||
values:
|
||||
- ../helm-chart/values.yaml
|
||||
set:
|
||||
- name: app_name
|
||||
value: maestro
|
||||
- name: maestro.duc_url
|
||||
value: duc.dadosfera.ai
|
||||
- name: hostname
|
||||
value: maestro.dadosfera.ai
|
||||
- name: maestro.pi_factory_url
|
||||
value: pi-factory.dadosfera.ai
|
||||
- name: maestro.in_factory_url
|
||||
value: in-factory.dadosfera.ai
|
||||
- name: maestro.tr_factory_url
|
||||
value: in-factory.dadosfera.ai
|
||||
- name: maestro.open_customer_id
|
||||
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||
- name: maestro.open_group_id
|
||||
value: c0afdcce-c5be-40d0-9d1d-2d271121f14a
|
||||
- name: replicaCount
|
||||
value: 2
|
||||
|
||||
- name: unimed-maestro
|
||||
chart: ../helm-chart
|
||||
values:
|
||||
- ../helm-chart/values.yaml
|
||||
set:
|
||||
- name: app_name
|
||||
value: maestro-unimed
|
||||
- name: maestro.duc_url
|
||||
value: duc.dadosfera.ai
|
||||
- name: hostname
|
||||
value: maestro-unimed.dadosfera.ai
|
||||
- name: maestro.pi_factory_url
|
||||
value: pi-factory.dadosfera.ai
|
||||
- name: maestro.in_factory_url
|
||||
value: in-factory.dadosfera.ai
|
||||
- name: maestro.tr_factory_url
|
||||
value: in-factory.dadosfera.ai
|
||||
- name: maestro.open_customer_id
|
||||
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||
- name: maestro.open_group_id
|
||||
value: c0afdcce-c5be-40d0-9d1d-2d271121f14a
|
||||
# Customer id
|
||||
- name: maestro.dedicated_proxy
|
||||
value: dea2c27f-0973-4588-a2e0-9e31b64c7ffd
|
||||
- name: replicaCount
|
||||
value: 1
|
||||
# 10.70.0.0/16 internal network
|
||||
# 137.131.167.254/32 loadbalancer
|
||||
# 159.112.184.81/32 cluster ip for the uptime request ingest
|
||||
- name: maestro.restricted_ip
|
||||
value: "177.52.172.0/24, 189.84.160.157/32, 186.237.171.146/32, 137.131.167.254/32, 10.70.0.0/16, 159.112.184.81/32, 10.244.0.0/16"
|
||||
@@ -0,0 +1,30 @@
|
||||
charts:
|
||||
- name: maestro
|
||||
chart: ../helm-chart
|
||||
values:
|
||||
- ../helm-chart/values.yaml
|
||||
- ../helm-chart/values-stg.yaml
|
||||
|
||||
|
||||
# Environment to test Network Policies
|
||||
- name: private-maestro
|
||||
chart: ../helm-chart
|
||||
values:
|
||||
- ../helm-chart/values.yaml
|
||||
- ../helm-chart/values-stg.yaml
|
||||
set:
|
||||
- name: app_name
|
||||
value: maestro-private
|
||||
- name: hostname
|
||||
value: private-maestro.stg.dadosfera.ai
|
||||
# Customer id
|
||||
- name: maestro.dedicated_proxy
|
||||
value: 14d52fd4-d83d-4cdd-be34-bf11cc28b3bd
|
||||
- name: replicaCount
|
||||
value: 1
|
||||
- name: affinity
|
||||
value: null
|
||||
- name: resources
|
||||
value: null
|
||||
- name: maestro.restricted_ip
|
||||
value: "137.131.167.254/32, 10.70.0.0/16, 159.112.184.81/32, 10.244.0.0/16"
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
maestro:
|
||||
image: dadosfera/maestro_${ENV}:${IMAGE_TAG}
|
||||
container_name: maestro
|
||||
restart: always
|
||||
ports:
|
||||
- 3333:3333
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
node_exporter:
|
||||
image: ${NODE_EXPORTER_URL}
|
||||
container_name: node_exporter
|
||||
restart: always
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
- /run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket:ro
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.rootfs=/rootfs'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
|
||||
- '--collector.systemd'
|
||||
- '--collector.processes'
|
||||
network_mode: host
|
||||
pid: host
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
maestro:
|
||||
image: ${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_${ENV}:${IMAGE_TAG}
|
||||
File diff suppressed because it is too large
Load Diff
+8965
-2812
File diff suppressed because it is too large
Load Diff
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: 'test';
|
||||
ENV: 'local' | 'stg' | 'prd' | 'test';
|
||||
LOCAL_ENV: 'stg' | 'prd';
|
||||
|
||||
DUC_URL: string;
|
||||
INFACTORY_URL: string;
|
||||
PIFACTORY_URL: string;
|
||||
|
||||
INTERNAL_SWAGGER: 'true' | 'false';
|
||||
|
||||
AWS_REGION: string;
|
||||
OPEN_GROUP_ID: string;
|
||||
OPEN_CUSTOMER_ID: string;
|
||||
DEDICATED_PROXY: string;
|
||||
COOKIE_SECRET: string;
|
||||
REDIS_TLS?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
export {};
|
||||
@@ -11,6 +11,7 @@ const config: Config.InitialOptions = {
|
||||
'<rootDir>/node_modules/',
|
||||
'.*\\.module\\.[jt]s$',
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.ts'],
|
||||
// moduleDirectories: ['node_modules'], // default is already 'node_modules'
|
||||
// rootDir: '.', //No need
|
||||
// testEnvironment: 'node', //Defaults to 'node'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
process.env.DUC_URL="duc:50051"
|
||||
process.env.INFACTORY_URL="in-factory:50052"
|
||||
process.env.PIFACTORY_URL="pi-factory:50053"
|
||||
@@ -4,6 +4,7 @@
|
||||
"compilerOptions": {
|
||||
"assets": [
|
||||
"**/*.proto",
|
||||
"assets/**/*",
|
||||
{
|
||||
"include": "i18n/**/*",
|
||||
"watchAssets": true
|
||||
|
||||
Generated
+7170
-11234
File diff suppressed because it is too large
Load Diff
+75
-49
@@ -6,93 +6,119 @@
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"engines": {
|
||||
"node": "18.3.0"
|
||||
"node": "18.17"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1",
|
||||
"proto-update": "npm i @dadosfera/protospack-v2@latest --save-exact",
|
||||
"co:login": "aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1",
|
||||
"proto-update": "npm i @dadosfera/protospack-v2@v3.40.0-beta.1 --save-exact",
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "npm run build && node dist/main",
|
||||
"start:dev": "export INTERNAL_SWAGGER=true && nest start --watch",
|
||||
"start:debug": "npm run start:dev",
|
||||
"start:prod": "node dist/main",
|
||||
"docs": "export KILL_AFTER_START=1 && nest start && export INTERNAL_SWAGGER=true && nest start",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test": "jest --detectOpenHandles --forceExit",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.112.0",
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
"@aws-sdk/client-dynamodb": "^3.414.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.414.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.940.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
||||
"@aws-sdk/signature-v4": "^3.370.0",
|
||||
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
||||
"@dadosfera/protospack-v2": "3.27.0",
|
||||
"@grpc/grpc-js": "^1.6.7",
|
||||
"@grpc/proto-loader": "^0.6.13",
|
||||
"@nestjs/common": "^8.4.7",
|
||||
"@nestjs/config": "^1.2.1",
|
||||
"@nestjs/core": "^8.4.7",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/microservices": "^8.4.7",
|
||||
"@nestjs/passport": "^8.2.2",
|
||||
"@nestjs/platform-express": "^8.4.7",
|
||||
"@nestjs/schedule": "^1.1.0",
|
||||
"@nestjs/swagger": "^5.2.1",
|
||||
"axios": "^0.27.2",
|
||||
"@dadosfera/protospack-v2": "3.40.0",
|
||||
"@grpc/grpc-js": "^1.9.3",
|
||||
"@grpc/proto-loader": "^0.7.9",
|
||||
"@nestjs/cli": "^9.5.0",
|
||||
"@nestjs/common": "^9.4.3",
|
||||
"@nestjs/config": "^2.3.4",
|
||||
"@nestjs/core": "^9.4.3",
|
||||
"@nestjs/mapped-types": "^1.2.2",
|
||||
"@nestjs/microservices": "^9.4.3",
|
||||
"@nestjs/passport": "^9.0.3",
|
||||
"@nestjs/platform-express": "^9.4.3",
|
||||
"@nestjs/schematics": "^9.2.0",
|
||||
"@nestjs/swagger": "^6.3.0",
|
||||
"@nestjs/testing": "^9.4.3",
|
||||
"axios": "0.30.3",
|
||||
"cache-manager": "^5.1.4",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"cron-parser": "^4.4.0",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron-parser": "^4.9.0",
|
||||
"csv": "^6.3.11",
|
||||
"dotenv": "^14.3.2",
|
||||
"elastic-apm-node": "^3.36.0",
|
||||
"helmet": "^5.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"elastic-apm-node": "^3.50.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
"mixpanel": "^0.17.0",
|
||||
"ms": "^3.0.0-canary.1",
|
||||
"multer": "^2.0.2",
|
||||
"openid-client": "^5.7.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-forcedotcom": "^0.2.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-hubspot-oauth2": "^1.0.3",
|
||||
"passport-mailchimp": "^1.1.0",
|
||||
"protospack": "2.5.2",
|
||||
"puppeteer": "^24.7.2",
|
||||
"redis": "^4.5.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.5",
|
||||
"swagger-ui-express": "^4.4.0"
|
||||
"swagger-ui-express": "^4.6.3"
|
||||
},
|
||||
"overrides": {
|
||||
"multer": "1.4.5-lts.1"
|
||||
"axios": "0.30.3",
|
||||
"form-data": "^4.0.4",
|
||||
"body-parser": "^1.20.3",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"glob": "^10.5.0",
|
||||
"path-to-regexp": "^3.3.0",
|
||||
"semver": "^7.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^8.2.8",
|
||||
"@nestjs/schematics": "^8.0.11",
|
||||
"@nestjs/testing": "^8.4.7",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/cache-manager": "^4.0.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.18.1",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/jwk-to-pem": "^2.0.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^16.11.41",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^16.18.52",
|
||||
"@types/passport-facebook": "^2.1.11",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-oauth2": "^1.4.11",
|
||||
"@types/passport-oauth2": "^1.4.12",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"nock": "^13.2.7",
|
||||
"prettier": "^2.7.1",
|
||||
"nock": "^13.3.3",
|
||||
"prettier": "^2.8.8",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.2.3",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^27.1.5",
|
||||
"ts-loader": "^9.3.1",
|
||||
"ts-node": "^10.8.1",
|
||||
"tsconfig-paths": "^3.14.1",
|
||||
"typescript": "^4.6.3"
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^3.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"axios": "0.30.3"
|
||||
}
|
||||
}
|
||||
|
||||
+32
-10
@@ -17,7 +17,6 @@ import { ConnectionTestModule } from './modules/connection-test/connection-test.
|
||||
import { NetworkConfigModule } from './modules/network-config/network-config.module';
|
||||
import { InputsModule } from './modules/inputs/inputs.module';
|
||||
import { OauthModule } from './modules/oauth/oauth.module';
|
||||
import { PipelinesModule } from './modules/pipelines/pipelines.module';
|
||||
import { TransformationsModule } from './modules/transformations/transformations.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { CatalogModule } from './modules/catalog/catalog.module';
|
||||
@@ -25,9 +24,20 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { PipelinesV2Module } from './modules/pipelinesV2/pipelines.module';
|
||||
import { ProductboardModule } from './modules/productboard/productboard.module';
|
||||
import { MixpanelModule } from './modules/mixpanel/mixpanel.module';
|
||||
import { CustomersModule } from './modules/customers/customers.module';
|
||||
import { OpenDataModule } from './modules/open-data/open-data.module';
|
||||
import { ThemeModule } from './modules/theme/theme.module';
|
||||
import { IdentityProviderModule } from './modules/identity-provider/identity-provider.module';
|
||||
import { NetworkPolicyModule } from './modules/network-policy/network-policy.module';
|
||||
import { AssignModule } from './modules/assign/assign.module';
|
||||
import { ShareMetadataModule } from './modules/share-metadata/share-metadata.module';
|
||||
import { ApiKeyModule } from './modules/api-key/api-key.module';
|
||||
import { PlatformApiModule } from './modules/platform-api/platform-api.module';
|
||||
import { StorageExplorerModule } from './modules/storage-explorer/storage-explorer.module';
|
||||
import { ReleaseNoteModule } from './modules/release_note/release_note.module';
|
||||
|
||||
|
||||
@Module({
|
||||
controllers: [],
|
||||
providers: [
|
||||
DadosferaLogger,
|
||||
{
|
||||
@@ -40,24 +50,36 @@ import { MixpanelModule } from './modules/mixpanel/mixpanel.module';
|
||||
isGlobal: true,
|
||||
}),
|
||||
AuthModule,
|
||||
ConnectionModule,
|
||||
NetworkConfigModule,
|
||||
InputsModule,
|
||||
PipelinesV2Module,
|
||||
CatalogModule,
|
||||
ConnectorModule,
|
||||
PermissionsModule,
|
||||
TermsOfUseModule,
|
||||
ConnectionModule,
|
||||
NetworkConfigModule,
|
||||
ConnectionTestModule,
|
||||
PipelinesModule,
|
||||
PipelinesV2Module,
|
||||
TransformationsModule,
|
||||
HealthModule,
|
||||
CatalogModule,
|
||||
UsersModule,
|
||||
RolesModule,
|
||||
InputsModule,
|
||||
OauthModule,
|
||||
CatalogModule,
|
||||
ProductboardModule,
|
||||
MixpanelModule,
|
||||
CustomersModule,
|
||||
OpenDataModule,
|
||||
ThemeModule,
|
||||
NetworkPolicyModule,
|
||||
AssignModule,
|
||||
ShareMetadataModule,
|
||||
NetworkPolicyModule,
|
||||
ApiKeyModule,
|
||||
IdentityProviderModule,
|
||||
NetworkPolicyModule,
|
||||
PlatformApiModule,
|
||||
StorageExplorerModule,
|
||||
//Always leave HealthModule last, so it is on the bottom of swagger
|
||||
HealthModule,
|
||||
ReleaseNoteModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dadosfera Relatório de PII</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape; /* Alterado para paisagem (landscape) */
|
||||
margin: 15mm 10mm; /* Reduzido para proporcionar mais espaço */
|
||||
}
|
||||
body {
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
color: #5c5c5c;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
font-size: 12px; /* Reduzindo o tamanho da fonte */
|
||||
}
|
||||
.container {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.logo {
|
||||
max-width: 150px; /* Reduzida para economizar espaço */
|
||||
height: auto;
|
||||
}
|
||||
h1 {
|
||||
color: #0d003b;
|
||||
font-weight: 700;
|
||||
margin-left: 20px;
|
||||
font-size: 24px; /* Tamanho ajustado */
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
table-layout: fixed; /* Importante: define larguras fixas */
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
th {
|
||||
background-color: #1700a2;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #3a26b8;
|
||||
font-size: 11px; /* Tamanho ajustado */
|
||||
word-wrap: break-word; /* Permite quebra de palavras */
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d0d0d0;
|
||||
font-size: 11px; /* Tamanho ajustado */
|
||||
word-wrap: break-word; /* Permite quebra de palavras */
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
/* Definindo larguras específicas para cada coluna */
|
||||
th:nth-child(1), td:nth-child(1) { width: 14%; } /* Database */
|
||||
th:nth-child(2), td:nth-child(2) { width: 14%; } /* Schema */
|
||||
th:nth-child(3), td:nth-child(3) { width: 17%; } /* Tabela */
|
||||
th:nth-child(4), td:nth-child(4) { width: 17%; } /* Coluna */
|
||||
th:nth-child(5), td:nth-child(5) { width: 13%; } /* Tipo de Dado */
|
||||
th:nth-child(6), td:nth-child(6) { width: 25%; } /* Regras PII */
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
.info-section {
|
||||
margin-top: 20px;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
.timestamp {
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
margin-top: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="https://dadosfera.ai/wp-content/webp-express/webp-images/uploads/2022/06/Logo-Dadosfera1-1.png.webp" alt="Logo Dadosfera" class="logo">
|
||||
<h1>Relatório de PII</h1>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<p>Este relatório apresenta a estrutura de tabelas e suas as seguintes características de PII identificadas.</p>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Database</th>
|
||||
<th>Schema</th>
|
||||
<th>Tabela</th>
|
||||
<th>Coluna</th>
|
||||
<th>Tipo de Dado</th>
|
||||
<th>Regras PII</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each dados}}
|
||||
<tr>
|
||||
<td>{{database_name}}</td>
|
||||
<td>{{table_schema}}</td>
|
||||
<td>{{table_name}}</td>
|
||||
<td>{{column_name}}</td>
|
||||
<td>{{data_type}}</td>
|
||||
<td>{{pii_rules}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="timestamp">Gerado em: {{dataGeracao}}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Request } from 'express';
|
||||
import { CustomDecorator } from '@nestjs/common';
|
||||
import { PermissionsObjectPermission } from './permissions.enum';
|
||||
import { RequestUser } from './user.decorator';
|
||||
|
||||
export const AUTH_FUNCTION_KEY = '__AUTH_FUNCTION__';
|
||||
|
||||
export type AuthenticationFunction = (req: Request, user: any) => boolean;
|
||||
|
||||
// implementation copied from SetMetadata, but tweaked to get existing values
|
||||
// of the metadataKey and accumulate it with the new metadataValue
|
||||
function SetMultipleMetadata(metadataKey, metadataValue): CustomDecorator {
|
||||
const decoratorFactory = (target, key, descriptor) => {
|
||||
// .start: tweak
|
||||
// descriptor?.value = function decorator; target = class decorator
|
||||
const accumulatedVal =
|
||||
Reflect.getMetadata(metadataKey, descriptor?.value ?? target) ?? [];
|
||||
accumulatedVal.push(metadataValue);
|
||||
// .end: tweak
|
||||
|
||||
if (descriptor) {
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, descriptor.value);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, target);
|
||||
return target;
|
||||
};
|
||||
|
||||
decoratorFactory.KEY = metadataKey;
|
||||
return decoratorFactory as any;
|
||||
}
|
||||
|
||||
export function RequireAllPermissions(
|
||||
...permissions: PermissionsObjectPermission[]
|
||||
) {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) =>
|
||||
permissions.every(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function RequireSomePermission(
|
||||
...permissions: PermissionsObjectPermission[]
|
||||
) {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) =>
|
||||
permissions.some(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthenticateCondition(func: AuthenticationFunction) {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, func);
|
||||
}
|
||||
|
||||
export function Authenticated() {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, () => true);
|
||||
}
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
RequireSomePermission,
|
||||
} from './authentication.decorator';
|
||||
} from '../decorators/authentication.decorator';
|
||||
import { AuthenticationGuard } from './authentication.guard';
|
||||
import { PERMISSIONS_GROUPS } from './permissions.enum';
|
||||
import { AuthClientService } from '../modules/auth/auth.service';
|
||||
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||
|
||||
const logger = {
|
||||
info: (...args) => args,
|
||||
@@ -44,7 +45,7 @@ class NoClassAuthController {
|
||||
@Get('has-all-permissions')
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
||||
PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE,
|
||||
PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
|
||||
)
|
||||
async hasAllPermissions(@Body() body) {
|
||||
return { body };
|
||||
@@ -53,7 +54,7 @@ class NoClassAuthController {
|
||||
@Get('has-some-permission')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
||||
PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE,
|
||||
PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
|
||||
)
|
||||
async hasSomePermission(@Body() body) {
|
||||
return { body };
|
||||
@@ -62,7 +63,7 @@ class NoClassAuthController {
|
||||
|
||||
@Controller('class-auth-condition')
|
||||
@AuthenticateCondition((req) => req.get('x-on-class') === 'ok')
|
||||
@RequireSomePermission(PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE)
|
||||
@RequireSomePermission(PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE)
|
||||
class ClassAuthConditionController {
|
||||
@Get('body')
|
||||
async getBody(@Body() body) {
|
||||
@@ -99,6 +100,7 @@ describe('authentication.guard', () => {
|
||||
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
||||
customer_name: 'dadosfera',
|
||||
customer_tier: 'BASIC',
|
||||
customer_modules: []
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -120,6 +122,12 @@ describe('authentication.guard', () => {
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthenticationGuard,
|
||||
},
|
||||
{
|
||||
provide: ApiKeyService,
|
||||
useValue: {
|
||||
get: () => Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
],
|
||||
controllers: [NoClassAuthController, ClassAuthConditionController],
|
||||
}).compile();
|
||||
@@ -440,18 +448,18 @@ describe('authentication.guard', () => {
|
||||
NoClassAuthTest(null, null);
|
||||
ClassAuthConditionTest(null, null);
|
||||
|
||||
const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]);
|
||||
NoClassAuthTest(tokenZ, ['zendesk']);
|
||||
ClassAuthConditionTest(tokenZ, ['zendesk']);
|
||||
// const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]);
|
||||
// NoClassAuthTest(tokenZ, ['zendesk']);
|
||||
// ClassAuthConditionTest(tokenZ, ['zendesk']);
|
||||
|
||||
const tokenM = CreateToken([PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE]);
|
||||
NoClassAuthTest(tokenM, ['metabase']);
|
||||
ClassAuthConditionTest(tokenM, ['metabase']);
|
||||
// const tokenM = CreateToken([PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE]);
|
||||
// NoClassAuthTest(tokenM, ['metabase']);
|
||||
// ClassAuthConditionTest(tokenM, ['metabase']);
|
||||
|
||||
const tokenZM = CreateToken([
|
||||
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
||||
PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE,
|
||||
]);
|
||||
NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
|
||||
ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
|
||||
// const tokenZM = CreateToken([
|
||||
// PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
||||
// PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
|
||||
// ]);
|
||||
// NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
|
||||
// ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
OnApplicationBootstrap,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import assert from 'assert';
|
||||
@@ -13,10 +14,11 @@ import { AuthClientService } from '../modules/auth/auth.service';
|
||||
import {
|
||||
AuthenticationFunction,
|
||||
AUTH_FUNCTION_KEY,
|
||||
} from './authentication.decorator';
|
||||
import { RequestUser } from './user.decorator';
|
||||
} from '../decorators/authentication.decorator';
|
||||
import { RequestUser } from '../decorators/user.decorator';
|
||||
import ErrorBuilder from '../utils/ErrorBuilder';
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationGuard
|
||||
@@ -31,6 +33,7 @@ export class AuthenticationGuard
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private reflector: Reflector,
|
||||
private authClient: AuthClientService,
|
||||
private apiKeyService: ApiKeyService
|
||||
) {
|
||||
this.pems = new Map();
|
||||
this.logger = dadosferaLogger.logger;
|
||||
@@ -48,20 +51,40 @@ export class AuthenticationGuard
|
||||
});
|
||||
}
|
||||
|
||||
canActivate(ctx: ExecutionContext): boolean {
|
||||
async canActivate(ctx: ExecutionContext): Promise<boolean> {
|
||||
const authFunctions = this.reflector.getAllAndMerge<
|
||||
AuthenticationFunction[]
|
||||
>(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]);
|
||||
const mustBeAuthenticated = authFunctions.length > 0;
|
||||
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const accessToken = this.validateToken(request, mustBeAuthenticated);
|
||||
|
||||
if (!mustBeAuthenticated) {
|
||||
// no need to be authenticated
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const apiKey = request.get('X-api-key');
|
||||
if (apiKey) {
|
||||
const {
|
||||
api_key
|
||||
} = await this.apiKeyService.get(apiKey);
|
||||
|
||||
request.user = {
|
||||
user_id: api_key.user_id,
|
||||
username: api_key.username,
|
||||
permissions: api_key.permissions,
|
||||
customer_id: api_key.customer_id,
|
||||
customer_name: api_key.customer_name,
|
||||
customer_tier: api_key.customer_tier,
|
||||
customer_modules: api_key.customer_modules,
|
||||
access_token: apiKey,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const accessToken = this.validateToken(request, mustBeAuthenticated);
|
||||
|
||||
if (!accessToken) {
|
||||
// couldn't load valid token
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
||||
@@ -113,14 +136,28 @@ export class AuthenticationGuard
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bloquear outros customer de usar o maestor dedicado
|
||||
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
||||
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== accessTokenPayload.customer_id) {
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN);
|
||||
}
|
||||
|
||||
// Bloquear o customer de acesso o maestro publico
|
||||
const hasNetworkPolicyModule = accessTokenPayload.customer_modules.includes('network-policy');
|
||||
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
||||
throw new ForbiddenException(ErrorCodes.AUTH.FORBIDDEN);
|
||||
}
|
||||
|
||||
request.accessTokenPayload = accessTokenPayload;
|
||||
request.user = {
|
||||
user_id: accessTokenPayload.user_id,
|
||||
username: accessTokenPayload.username,
|
||||
permissions: accessTokenPayload.permissions,
|
||||
roles: accessTokenPayload.roles,
|
||||
customer_id: accessTokenPayload.customer_id,
|
||||
customer_name: accessTokenPayload.customer_name,
|
||||
customer_tier: accessTokenPayload.customer_tier,
|
||||
customer_modules: accessTokenPayload.customer_modules,
|
||||
access_token: accessToken,
|
||||
};
|
||||
// TODO: for backwards compatibility. remove in the future
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
export function extractUserFrom(aRawJwt: string) {
|
||||
const decodedToken = jwt.decode(aRawJwt, {
|
||||
complete: true,
|
||||
});
|
||||
|
||||
const payload = decodedToken.payload as JwtPayload;
|
||||
|
||||
return {
|
||||
user_id: payload.user_id,
|
||||
username: payload.username,
|
||||
permissions: payload.permissions,
|
||||
roles: payload.roles,
|
||||
customer_id: payload.customer_id,
|
||||
customer_name: payload.customer_name,
|
||||
customer_tier: payload.customer_tier,
|
||||
customer_modules: payload.customer_modules,
|
||||
access_token: aRawJwt,
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,18 @@ export const PERMISSIONS_GROUPS = {
|
||||
'es-es': '',
|
||||
},
|
||||
},
|
||||
GENERATE_TOKEN: {
|
||||
seqid: 46,
|
||||
claim: 'customer:generate-token',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Gerar Token de Acesso',
|
||||
'en-us': 'Generate Acess Token',
|
||||
'es-es': 'Gerar Token de Acceso',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
PIPELINE: {
|
||||
title: {
|
||||
'pt-br': 'Coletar | Pipelines',
|
||||
@@ -107,7 +116,44 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
IMPORT_FILES: {
|
||||
title: {
|
||||
'pt-br': 'Coletar | Importar arquivos',
|
||||
'en-us': 'Collect | Import files',
|
||||
'es-es': 'Colecta | Importar archivos',
|
||||
},
|
||||
permissions: {
|
||||
VIEW: {
|
||||
seqid: 48,
|
||||
claim: 'import-file:view',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Importar arquivos',
|
||||
'en-us': 'Import files',
|
||||
'es-es': 'Importar archivos',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AI_CHAT: {
|
||||
title: {
|
||||
'pt-br': 'AutodriveDDF',
|
||||
'en-us': 'AutodriveDDF',
|
||||
'es-es': 'AutodriveDDF',
|
||||
},
|
||||
permissions: {
|
||||
VIEW: {
|
||||
seqid: 49,
|
||||
claim: 'ai-chat:view',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'AutodriveDDF',
|
||||
'en-us': 'AutodriveDDF',
|
||||
'es-es': 'AutodriveDDF',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CONNECTION: {
|
||||
title: {
|
||||
'pt-br': 'Coletar | Fontes de dados',
|
||||
@@ -147,7 +193,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
NETWORK_CONFIG: {
|
||||
title: {
|
||||
'pt-br': 'Coletar | Redes',
|
||||
@@ -177,7 +222,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
// },
|
||||
},
|
||||
},
|
||||
|
||||
OUTPUT: {
|
||||
title: {
|
||||
'pt-br': 'Output',
|
||||
@@ -227,7 +271,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
TRANSFORMATIONS: {
|
||||
title: {
|
||||
'pt-br': 'Micro-transformações',
|
||||
@@ -277,7 +320,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
CATALOG: {
|
||||
title: {
|
||||
'pt-br': 'Explorar | Catálogo',
|
||||
@@ -315,6 +357,16 @@ export const PERMISSIONS_GROUPS = {
|
||||
'es-es': 'Crear y editar atributos en el catálogo',
|
||||
},
|
||||
},
|
||||
CERTIFY: {
|
||||
seqid: 53,
|
||||
claim: 'catalog:certify',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Alterar o status de certificação dos Ativos',
|
||||
'en-us': "Change Assets' certification status",
|
||||
'es-es': 'Cambiar el estado de certificación de los Activos',
|
||||
},
|
||||
},
|
||||
DELETE: {
|
||||
seqid: 1,
|
||||
claim: 'catalog:delete',
|
||||
@@ -336,9 +388,56 @@ export const PERMISSIONS_GROUPS = {
|
||||
'es-es': 'Gestor de catálogos. Puede ver y editar todos los activos.',
|
||||
},
|
||||
},
|
||||
TRIGGER_CATALOG_TASK: {
|
||||
seqid: 45,
|
||||
claim: 'catalog:trigger-task',
|
||||
usage: PermissionUsages.INTERNAL,
|
||||
name: {
|
||||
'pt-br': 'Executar a catalogação de um ativo',
|
||||
'en-us': 'Trigger an asset cataloging',
|
||||
'es-es': 'Realizar el listado de un activo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
LINEAGE: {
|
||||
title: {
|
||||
'pt-br': 'Explorar | Linhagem',
|
||||
'en-us': 'Explore | Lineage',
|
||||
'es-es': 'Explorar | Linaje',
|
||||
},
|
||||
permissions: {
|
||||
VIEW: {
|
||||
seqid: 50,
|
||||
claim: 'lineage:view',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Acessar ao módulo de Linhagem',
|
||||
'en-us': 'Access to Lineage module',
|
||||
'es-es': 'Acceda al módulo de Linaje',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
EMBED: {
|
||||
title: {
|
||||
'pt-br': 'Analisar | Incorporação',
|
||||
'en-us': 'Analyze | Embedding',
|
||||
'es-es': 'Analizar | Incorporación',
|
||||
},
|
||||
permissions: {
|
||||
EMBED_ANALYTICS: {
|
||||
seqid: 44,
|
||||
claim: 'catalog:embed',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Acessar Módulo de Incorporação de Ativos',
|
||||
'en-us': 'Access Embedding analytics Module',
|
||||
'es-es': 'Acceder al Módulo de Incorporación de Activos',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
CONNECTORS: {
|
||||
title: {
|
||||
'pt-br': 'Conectores',
|
||||
@@ -388,7 +487,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
SNOWFLAKE: {
|
||||
title: {
|
||||
'pt-br': 'Explorar | Consolidar',
|
||||
@@ -408,7 +506,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ZENDESK: {
|
||||
title: {
|
||||
'pt-br': 'Zendesk',
|
||||
@@ -418,18 +515,17 @@ export const PERMISSIONS_GROUPS = {
|
||||
permissions: {
|
||||
OPEN: {
|
||||
seqid: 16,
|
||||
claim: 'GET /zendesk',
|
||||
claim: 'support-chat:access',
|
||||
usage: PermissionUsages.INTERNAL,
|
||||
name: {
|
||||
'pt-br': 'Acessar Zendesk',
|
||||
'en-us': 'Access Zendesk',
|
||||
'es-es': 'Acceso Zendesk',
|
||||
'pt-br': 'Acessar chat de suporte',
|
||||
'en-us': 'Access support chat',
|
||||
'es-es': 'Acceder al chat de soporte',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ANALYZE: {
|
||||
DATAVIZ: {
|
||||
title: {
|
||||
'pt-br': 'Analisar | Visualização',
|
||||
'en-us': 'Analyze | Visualization',
|
||||
@@ -446,6 +542,15 @@ export const PERMISSIONS_GROUPS = {
|
||||
'es-es': 'Acceso Metabase',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
INTELLIGENCE: {
|
||||
title: {
|
||||
'pt-br': 'Analisar | Inteligência',
|
||||
'en-us': 'Analyze | Intelligence',
|
||||
'es-es': 'Analizar | Inteligencia',
|
||||
},
|
||||
permissions: {
|
||||
INTELLIGENCE: {
|
||||
seqid: 31,
|
||||
claim: 'intelligence:open',
|
||||
@@ -458,7 +563,6 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MODULES: {
|
||||
title: {
|
||||
'pt-br': 'Módulos da Dadosfera',
|
||||
@@ -555,8 +659,96 @@ export const PERMISSIONS_GROUPS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
CUSTOMER: {
|
||||
title: {
|
||||
'pt-br': 'Organização',
|
||||
'en-us': 'Organization',
|
||||
'es-es': 'Organización',
|
||||
},
|
||||
permissions: {
|
||||
MONITORING_DASHBOARD: {
|
||||
seqid: 47,
|
||||
claim: 'customer:monitoring-dashboard',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Ver o dashboard de monitoramento',
|
||||
'en-us': 'View the monitoring dashboard',
|
||||
'es-es': 'Ver el dashboard de monitoreo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
STORAGE_EXPLORER: {
|
||||
title: {
|
||||
'pt-br': 'Storage Explorer',
|
||||
'en-us': 'Storage Explorer',
|
||||
'es-es': 'Storage Explorer',
|
||||
},
|
||||
permissions: {
|
||||
READ: {
|
||||
seqid: 51,
|
||||
claim: 'storage-explorer:read',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Ler dados do Storage Explorer',
|
||||
'en-us': 'Read Storage Explorer data',
|
||||
'es-es': 'Leer datos del Storage Explorer',
|
||||
},
|
||||
},
|
||||
WRITE: {
|
||||
seqid: 52,
|
||||
claim: 'storage-explorer:write',
|
||||
usage: PermissionUsages.PUBLIC,
|
||||
name: {
|
||||
'pt-br': 'Escrever dados no Storage Explorer',
|
||||
'en-us': 'Write Storage Explorer data',
|
||||
'es-es': 'Escribir datos en Storage Explorer',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export interface DadosferaModule {
|
||||
name: string;
|
||||
description: string;
|
||||
key: string;
|
||||
permissionSeqId: number;
|
||||
}
|
||||
|
||||
export const DADOSFERA_MODULES_KEYS = {
|
||||
LOG_DASHBOARD: 'logs-dashboard',
|
||||
ACCESS_DASHBOARD: 'access-dashboard',
|
||||
DANGER_ZONE: 'danger-zone',
|
||||
PII: 'pii',
|
||||
EMBED: 'embedded-analytics',
|
||||
EMBED_ASSIGNED: 'embed-assigned',
|
||||
CATALOG: 'catalog',
|
||||
COLLECT: 'collect',
|
||||
}
|
||||
|
||||
export const DADOSFERA_MODULES: Array<DadosferaModule> = [
|
||||
{
|
||||
name: 'Intelligence Module',
|
||||
description: 'Orchest Module',
|
||||
key: 'intelligence',
|
||||
permissionSeqId:
|
||||
PERMISSIONS_GROUPS.INTELLIGENCE.permissions.INTELLIGENCE.seqid,
|
||||
},
|
||||
{
|
||||
name: 'Proccessing Module',
|
||||
description: 'Proccessing Module',
|
||||
key: 'process',
|
||||
permissionSeqId:
|
||||
PERMISSIONS_GROUPS.PROCESS.permissions.TRANSFORMATION.seqid,
|
||||
},
|
||||
{
|
||||
name: 'Embedded Analytics',
|
||||
description: 'Embedded Analytics Module',
|
||||
key: 'embedded-analytics',
|
||||
permissionSeqId:
|
||||
PERMISSIONS_GROUPS.PROCESS.permissions.TRANSFORMATION.seqid,
|
||||
},
|
||||
];
|
||||
// traverses the object searching for duplicate seqids or claims (executes at runtime)
|
||||
let nextAvailableSeqid = 0;
|
||||
const seqids = Object.values(PERMISSIONS_GROUPS).flatMap((namespace) =>
|
||||
@@ -576,4 +768,9 @@ Object.values(PERMISSIONS_GROUPS).map((namespace) =>
|
||||
}),
|
||||
);
|
||||
|
||||
DADOSFERA_MODULES.map((m) => m.key).forEach((m, i, arr) => {
|
||||
if (arr.indexOf(m) !== i)
|
||||
throw new Error(`DADOSFERA_MODULES[${i}] does not have a unique key`);
|
||||
});
|
||||
|
||||
logger.log(`next available seqid ${nextAvailableSeqid + 1}`);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiSecurity } from '@nestjs/swagger';
|
||||
import { type Request } from 'express';
|
||||
import { type PermissionsObjectPermission } from '../authentication/permissions.enum';
|
||||
import { type RequestUser } from './user.decorator';
|
||||
import { SetMultipleMetadata } from './shared';
|
||||
|
||||
export const AUTH_FUNCTION_KEY = '__AUTH_FUNCTION__';
|
||||
|
||||
export type AuthenticationFunction = (req: Request, user: any) => boolean;
|
||||
|
||||
export function RequireAllPermissions(
|
||||
...permissions: PermissionsObjectPermission[]
|
||||
) {
|
||||
return createAuthenticatedDecorator((req, user: RequestUser) =>
|
||||
permissions.every(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function RequireSomePermission(
|
||||
...permissions: PermissionsObjectPermission[]
|
||||
) {
|
||||
return createAuthenticatedDecorator((req, user: RequestUser) =>
|
||||
permissions.some(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function RequireModule(
|
||||
key: string
|
||||
) {
|
||||
return createAuthenticatedDecorator((_, user: RequestUser) =>
|
||||
user.customer_modules.some(module => module === key),
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthenticateCondition(func: AuthenticationFunction) {
|
||||
return createAuthenticatedDecorator(func);
|
||||
}
|
||||
|
||||
export function Authenticated() {
|
||||
return createAuthenticatedDecorator(() => true);
|
||||
}
|
||||
|
||||
function createAuthenticatedDecorator(metadataValue: AuthenticationFunction) {
|
||||
return applyDecorators(
|
||||
ApiSecurity('access-token'),
|
||||
SetMultipleMetadata(AUTH_FUNCTION_KEY, metadataValue),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
|
||||
export const Language: () => ParameterDecorator = createParamDecorator(
|
||||
(options: any, ctx: ExecutionContext): LanguageEnum => {
|
||||
const headers = ctx.switchToHttp()?.getRequest()?.headers;
|
||||
|
||||
if (!headers) return LanguageEnum.ptbr;
|
||||
|
||||
let language: LanguageEnum =
|
||||
headers['dadosfera-lang'] ?? headers['accept-language'];
|
||||
language = language?.toLowerCase().trim() as LanguageEnum;
|
||||
|
||||
if (!language) return LanguageEnum.ptbr;
|
||||
|
||||
Object.values(LanguageEnum).some((l) => {
|
||||
if (language === l || l.includes(language)) {
|
||||
language = l;
|
||||
return true;
|
||||
}
|
||||
language = LanguageEnum.enus;
|
||||
});
|
||||
return language;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type CustomDecorator } from '@nestjs/common';
|
||||
|
||||
// implementation copied from SetMetadata, but tweaked to get existing values
|
||||
// of the metadataKey and accumulate it with the new metadataValue
|
||||
export function SetMultipleMetadata(
|
||||
metadataKey,
|
||||
metadataValue,
|
||||
): CustomDecorator {
|
||||
const decoratorFactory: CustomDecorator = (target, key?, descriptor?) => {
|
||||
// .start: tweak
|
||||
// descriptor?.value = function decorator; target = class decorator
|
||||
const accumulatedVal =
|
||||
Reflect.getMetadata(metadataKey, descriptor?.value ?? target) ?? [];
|
||||
accumulatedVal.push(metadataValue);
|
||||
// .end: tweak
|
||||
|
||||
if (descriptor) {
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, descriptor.value);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, target);
|
||||
return target;
|
||||
};
|
||||
|
||||
decoratorFactory.KEY = metadataKey;
|
||||
return decoratorFactory;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiExcludeController, ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||
|
||||
export function ApiInternalOnlyEndpoint() {
|
||||
if (process.env.INTERNAL_SWAGGER !== 'true')
|
||||
return applyDecorators(ApiExcludeEndpoint());
|
||||
return () => null;
|
||||
}
|
||||
export function ApiInternalOnlyController() {
|
||||
if (process.env.INTERNAL_SWAGGER !== 'true')
|
||||
return applyDecorators(ApiExcludeController());
|
||||
return () => null;
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import request from 'supertest';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { HttpStatus, INestApplication } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { AuthenticationGuard } from './authentication.guard';
|
||||
import { Controller, Get, HttpStatus, INestApplication } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import request from 'supertest';
|
||||
import { AuthenticationGuard } from '../authentication/authentication.guard';
|
||||
import { PERMISSIONS_GROUPS } from '../authentication/permissions.enum';
|
||||
import { AuthClientService } from '../modules/auth/auth.service';
|
||||
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
import { User } from './user.decorator';
|
||||
import { PERMISSIONS_GROUPS } from './permissions.enum';
|
||||
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||
|
||||
const logger = {
|
||||
info: (...args) => args,
|
||||
@@ -49,12 +47,13 @@ describe('user.decorator', () => {
|
||||
const fakeUserPayload = {
|
||||
user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125',
|
||||
username: 'super.admin',
|
||||
permissions: [PERMISSIONS_GROUPS.ANALYZE.permissions.METABASE].map(
|
||||
permissions: [PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE].map(
|
||||
({ seqid }) => seqid,
|
||||
),
|
||||
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
||||
customer_name: 'dadosfera',
|
||||
customer_tier: 'BASIC',
|
||||
customer_modules: [],
|
||||
access_token: '',
|
||||
};
|
||||
|
||||
@@ -77,6 +76,12 @@ describe('user.decorator', () => {
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthenticationGuard,
|
||||
},
|
||||
{
|
||||
provide: ApiKeyService,
|
||||
useValue: {
|
||||
get: () => Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
],
|
||||
controllers: [UserController],
|
||||
}).compile();
|
||||
@@ -178,5 +183,5 @@ describe('user.decorator', () => {
|
||||
|
||||
const token = CreateToken();
|
||||
fakeUserPayload.access_token = token;
|
||||
UserTest(token);
|
||||
// UserTest(token);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import ErrorBuilder from '../utils/ErrorBuilder';
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
@@ -11,6 +11,8 @@ export interface RequestUser {
|
||||
customer_name: string;
|
||||
customer_tier: string;
|
||||
access_token: string;
|
||||
customer_modules: string[];
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export const User: (options?: { required?: boolean }) => ParameterDecorator =
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import {
|
||||
ReadService,
|
||||
ProtoServices,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
|
||||
import { PipelinesClientConfiguration } from 'src/modules/pipelinesV2/pipelines-client';
|
||||
import { PlatformApiService } from 'src/modules/platform-api/platform-api.service';
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
|
||||
@Injectable()
|
||||
export class PipelineExecutionGuard implements CanActivate {
|
||||
logger: DadosferaLogger;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private readonly platformApiService: PlatformApiService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
try {
|
||||
this.logger.info(
|
||||
'PipelineExecutionGuard: Checking if pipeline can be executed...',
|
||||
);
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const pipelineId = request.params.pipelineId;
|
||||
const user = request.user;
|
||||
const idRegex = /[^0-9a-zA-Z_$]+/g;
|
||||
const convertedId = pipelineId.replace(idRegex, '_');
|
||||
|
||||
const status = await this.platformApiService.proxy(
|
||||
'GET',
|
||||
`/pipeline/${convertedId}/pipeline_run`,
|
||||
user,
|
||||
);
|
||||
|
||||
const currentStatus = status[status.length - 1]
|
||||
|
||||
this.logger.info('Pipeline current status response:' + JSON.stringify(currentStatus));
|
||||
|
||||
if (currentStatus.last_status.toLowerCase() === 'running') {
|
||||
this.logger.error('Pipeline is running, cannot update input now');
|
||||
throw new BadRequestException('Pipeline is running, cannot update input now');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error in PipelineExecutionGuard: ' + error.message);
|
||||
throw new BadRequestException('Error checking pipeline status: ' + error.message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+52
-10
@@ -3,11 +3,14 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import helmet from 'helmet';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { json, urlencoded } from 'express';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
async function bootstrap() {
|
||||
DadosferaLogger.setupLogger({
|
||||
serviceName: 'maestro',
|
||||
@@ -15,20 +18,46 @@ async function bootstrap() {
|
||||
});
|
||||
const logger = new DadosferaLogger();
|
||||
|
||||
const corsOrigins = [];
|
||||
|
||||
if (process.env.ENV === 'local') {
|
||||
corsOrigins.push('http://localhost:4200');
|
||||
} else {
|
||||
corsOrigins.push(
|
||||
'https://app.stg.dadosfera.ai',
|
||||
'https://app.dadosfera.ai',
|
||||
'https://private-frontend.stg.dadosfera.ai',
|
||||
'https://unimed.dadosfera.ai',
|
||||
'https://boston-scientific.dadosfera.ai',
|
||||
'https://plataforma.dadosfera.ai'
|
||||
);
|
||||
}
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger,
|
||||
cors: {
|
||||
origin: '*',
|
||||
origin: corsOrigins,
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204,
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
app.use(helmet());
|
||||
configureSwagger(app);
|
||||
app.use(cookieParser(process.env.COOKIE_SECRET));
|
||||
|
||||
if (process.env.ENV !== 'local') {
|
||||
app.use('/catalog/register-dataset', json({ limit: '10mb' }));
|
||||
app.use(
|
||||
'/catalog/register-dataset',
|
||||
urlencoded({ extended: true, limit: '10mb' }),
|
||||
);
|
||||
}
|
||||
|
||||
configureSwagger(app);
|
||||
await app.listen(3333);
|
||||
if (process.env.KILL_AFTER_START) await app.close();
|
||||
}
|
||||
|
||||
function configureSwagger(app: INestApplication) {
|
||||
@@ -39,24 +68,36 @@ function configureSwagger(app: INestApplication) {
|
||||
.replace('\n', '')
|
||||
.trim();
|
||||
} catch (error) {
|
||||
console.log("Can't get branch name", error);
|
||||
console.warn("Can't get branch name", error);
|
||||
}
|
||||
const swaggerTitle = branchName ? `Maestro - ${branchName}` : 'Maestro';
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle(swaggerTitle)
|
||||
.setDescription('Documentation for Maestro gateway')
|
||||
.addBearerAuth(
|
||||
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
||||
'Authorization',
|
||||
)
|
||||
.build();
|
||||
.addSecurity('access-token', {
|
||||
type: 'apiKey',
|
||||
name: 'Authorization',
|
||||
in: 'header',
|
||||
})
|
||||
.setDescription('This is the Maestro API');
|
||||
|
||||
if (process.env.INTERNAL_SWAGGER !== 'true')
|
||||
swaggerConfig.addServer('https://maestro.dadosfera.ai', 'Dadosfera API');
|
||||
|
||||
const config = swaggerConfig.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
const swaggerJsonFile =
|
||||
process.env.INTERNAL_SWAGGER === 'true'
|
||||
? 'docsfera.json'
|
||||
: 'docsfera.external.json';
|
||||
|
||||
if (process.env.ENV === 'local') SwaggerModule.setup('api', app, document);
|
||||
|
||||
writeFileSync(
|
||||
'docsfera.json',
|
||||
swaggerJsonFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
service: '${DOCS_API_TOKEN}',
|
||||
@@ -70,3 +111,4 @@ function configureSwagger(app: INestApplication) {
|
||||
);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Controller, Get, Post, Body, Param, Delete, UseFilters, Inject } from '@nestjs/common';
|
||||
import { ApiKeyService } from './api-key.service';
|
||||
import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto';
|
||||
import { Authenticated } from 'src/decorators/authentication.decorator';
|
||||
import { ApiHeaders, ApiTags, ApiResponse } from '@nestjs/swagger';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
@Controller('api-key')
|
||||
@Authenticated()
|
||||
@ApiTags('ApiKey')
|
||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
export class ApiKeyController {
|
||||
logger: DadosferaLogger;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({ type: CreateApiKeyResponseDto })
|
||||
async create(@Body() createApiKeyDto: CreateApiKeyDto, @User() user: RequestUser): Promise<CreateApiKeyResponseDto> {
|
||||
this.logger.info('POST /api-key', {
|
||||
permissions: createApiKeyDto.permissions,
|
||||
method: 'create'
|
||||
});
|
||||
const result = await this.apiKeyService.create(createApiKeyDto, user);
|
||||
this.logger.info('POST /api-key success', {
|
||||
id: result.id,
|
||||
method: 'create'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiResponse({ type: [ApiKeyBaseResponseDto] })
|
||||
async findAll(@User() user: RequestUser): Promise<ApiKeyBaseResponseDto[]> {
|
||||
this.logger.info('GET /api-key', {
|
||||
method: 'findAll'
|
||||
});
|
||||
const result = await this.apiKeyService.findAll(user);
|
||||
this.logger.info('GET /api-key success', {
|
||||
count: result.length,
|
||||
method: 'findAll'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id') id: string, @User() user: RequestUser): Promise<void> {
|
||||
this.logger.info('DELETE /api-key/:id', {
|
||||
id,
|
||||
method: 'remove'
|
||||
});
|
||||
await this.apiKeyService.remove(id, user);
|
||||
this.logger.info('DELETE /api-key/:id success', {
|
||||
id,
|
||||
method: 'remove'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ApiKeyService } from './api-key.service';
|
||||
import { ApiKeyController } from './api-key.controller';
|
||||
import { ClientsModule } from '@nestjs/microservices';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
|
||||
const ducClient = new DucClient();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClientsModule.register([ducClient.providerOptions])
|
||||
],
|
||||
controllers: [ApiKeyController],
|
||||
providers: [ApiKeyService, DadosferaLogger],
|
||||
exports: [ApiKeyService]
|
||||
})
|
||||
export class ApiKeyModule {}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto';
|
||||
import { RequestUser } from 'src/decorators/user.decorator';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { ApiKeyWriteProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService implements OnModuleInit {
|
||||
private apiKeyService: ApiKeyWriteProtoService;
|
||||
|
||||
constructor(
|
||||
@Inject(DucClient.name) private readonly client: ClientGrpc,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.apiKeyService = this.client.getService<ApiKeyWriteProtoService>(ProtoServices.ApiKeyWriteProtoService);
|
||||
}
|
||||
|
||||
create(createApiKeyDto: CreateApiKeyDto, user: RequestUser): Promise<CreateApiKeyResponseDto> {
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
return lastValueFrom(this.apiKeyService.CreateApiKey({
|
||||
permissions: createApiKeyDto.permissions
|
||||
}, metadata));
|
||||
}
|
||||
|
||||
async findAll(user: RequestUser): Promise<ApiKeyBaseResponseDto[]> {
|
||||
const metadata = PackTheMetadata(user);
|
||||
console.log(metadata)
|
||||
|
||||
const data = await lastValueFrom(this.apiKeyService.ListApiKeys({}, metadata));
|
||||
return data.api_keys;
|
||||
}
|
||||
|
||||
async remove(id: string, user: RequestUser) {
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
await lastValueFrom(this.apiKeyService.DeleteApiKey({ id }, metadata));
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
const metadata = PackTheMetadata({});
|
||||
|
||||
return await lastValueFrom(this.apiKeyService.GetApiKey({ key }, metadata));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNumber } from 'class-validator';
|
||||
|
||||
export class PermissionDto {
|
||||
@ApiProperty({ type: Number })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class ApiKeyBaseResponseDto {
|
||||
@ApiProperty({ type: String, format: 'uuid' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
key_mask: string;
|
||||
|
||||
@ApiProperty({ type: [PermissionDto] })
|
||||
permissions: PermissionDto[];
|
||||
|
||||
@ApiProperty({ type: String, format: 'date-time' })
|
||||
created_at: string;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export class CreateApiKeyResponseDto extends ApiKeyBaseResponseDto {
|
||||
@ApiProperty({ type: String })
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class CreateApiKeyDto {
|
||||
@ApiProperty({ type: [Number], description: 'Array of permission IDs' })
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
permissions: number[];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller, Body, Put, Get, NotFoundException} from '@nestjs/common';
|
||||
import { AssignService } from './assign.service';
|
||||
import { CreateAssignDto } from './dto/create-assign.dto';
|
||||
import { Authenticated, RequireModule, RequireSomePermission } from 'src/decorators/authentication.decorator';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
|
||||
@Controller('assign')
|
||||
@Authenticated()
|
||||
export class AssignController {
|
||||
constructor(private readonly assignService: AssignService) {}
|
||||
|
||||
@Put('/public-key')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||
)
|
||||
@RequireModule(DADOSFERA_MODULES_KEYS.EMBED_ASSIGNED)
|
||||
create(@Body() createAssignDto: CreateAssignDto, @User() user: RequestUser) {
|
||||
const metadata = PackTheMetadata(user);
|
||||
return this.assignService.create(createAssignDto, metadata);
|
||||
}
|
||||
|
||||
@Get('/public-key')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||
)
|
||||
@RequireModule(DADOSFERA_MODULES_KEYS.EMBED_ASSIGNED)
|
||||
async get(@User() user: RequestUser) {
|
||||
const metadata = PackTheMetadata(user);
|
||||
try {
|
||||
return await this.assignService.get(metadata);
|
||||
} catch (error) {
|
||||
throw new NotFoundException(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AssignService } from './assign.service';
|
||||
import { AssignController } from './assign.controller';
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
import { ClientsModule } from '@nestjs/microservices';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
|
||||
const client = new DucClient();
|
||||
|
||||
@Module({
|
||||
imports: [ClientsModule.register([client.providerOptions])],
|
||||
controllers: [AssignController],
|
||||
providers: [AssignService, DadosferaLogger]
|
||||
})
|
||||
export class AssignModule {}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { CreateAssignDto } from './dto/create-assign.dto';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { DucClient } from 'src/modules/duc/client.config';
|
||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
import { lastValueFrom } from 'rxjs';import { AssingProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
|
||||
@Injectable()
|
||||
export class AssignService implements OnModuleInit {
|
||||
|
||||
ducService: AssingProtoService;
|
||||
logger: DadosferaLogger;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.ducService =this.grpcClient.getService<AssingProtoService>(
|
||||
ProtoServices.AssingProtoService,
|
||||
);
|
||||
}
|
||||
|
||||
async create(createAssignDto: CreateAssignDto, metadata: Metadata) {
|
||||
const data = await lastValueFrom(this.ducService.CreateOrUpdateAssignPublicKey(createAssignDto, metadata))
|
||||
return data;
|
||||
}
|
||||
|
||||
async get(metadata: Metadata) {
|
||||
return await lastValueFrom(this.ducService.GetAssignPublicKey({}, metadata))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class CreateAssignDto {
|
||||
publicKey: string;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Auth
|
||||
|
||||
## SSO/oAuth
|
||||
|
||||
### Strategy
|
||||
We are using [PassportJs](https://www.passportjs.org/) to handle oAuth authentications.
|
||||
|
||||
When the client (front end) makes a `GET /auth/oauth/{strategy}` Passport automatically redirects the user to the `strategy` login page. To do that we must configure and use a **Passport Strategy**. We must also have a callback route, conventionally `GET /auth/oauth/{strategy}/callback`, so the oAuth app can report the status of the user's login.
|
||||
|
||||
- If the oAuth is successfull we call DUC's `AuthOauthSignIn` request that gets the tokens from Cognito and saves them on cache temporarily under a key we call `session`. Duc returns that `session` to maestro which then redirects the user to our app login page with that `session` as a query param.
|
||||
|
||||
- If the oAuth login is not successfull for some reason or the user **does not** exist on DUC's database we redirect the user to our login page with an `error` and `error_description` as query params.
|
||||
|
||||
### Routes
|
||||
|
||||
So in order to have an SSO login, besides configuring the Strategy, we must have two routes for each Strategy, like in the example below:
|
||||
|
||||
```ts
|
||||
@Get('oauth/google')
|
||||
@UseGuards(AuthGuard('google-login'))
|
||||
googleOauth() {
|
||||
this.logger.info('/oauth/google');
|
||||
return true;
|
||||
}
|
||||
|
||||
@Get('oauth/google/callback')
|
||||
@UseGuards(AuthGuard('google-login'))
|
||||
@Redirect()
|
||||
async googleOauthCallback(@Req() req) {
|
||||
const { url, email, token, language = 'pt-br' } = await this.callback(req);
|
||||
if (url.searchParams.get('error')) {
|
||||
this.logger.error('/oauth/google - ERROR');
|
||||
return { url: url.href };
|
||||
}
|
||||
// ... Rest of the logic
|
||||
return { url: url.href };
|
||||
}
|
||||
```
|
||||
@@ -8,8 +8,19 @@ import {
|
||||
Inject,
|
||||
UseFilters,
|
||||
Get,
|
||||
UseGuards,
|
||||
Redirect,
|
||||
Req,
|
||||
Param,
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiHeaders,
|
||||
ApiOkResponse,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
AuthChangePasswordRequest,
|
||||
AuthResetPasswordRequest,
|
||||
@@ -17,52 +28,147 @@ import {
|
||||
AuthConfirmResetPasswordRequest,
|
||||
AuthEnableTotpMfaRequest,
|
||||
AuthDisableTotpMfaRequest,
|
||||
AuthVerifyTotpMfaRequest,
|
||||
AuthVerifyTotpMfaRequest
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||
|
||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
} from 'src/authentication/authentication.decorator';
|
||||
} from 'src/decorators/authentication.decorator';
|
||||
import { AuthClientService } from './auth.service';
|
||||
import { UserDTO } from './dtos/login';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter';
|
||||
import { RequestUser, User } from 'src/authentication/user.decorator';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import {
|
||||
AuthRefreshAccessTokenReq,
|
||||
AuthRefreshAccessTokenRes,
|
||||
AuthSignInReq,
|
||||
AuthSignInRes,
|
||||
BulkEditRequest,
|
||||
} from './dtos/login';
|
||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Request, Response } from 'express';
|
||||
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { Language } from 'src/decorators/language.decorator';
|
||||
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
||||
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||
|
||||
type CookiesValues = {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
userId?: string
|
||||
}
|
||||
|
||||
@ApiTags('Auth')
|
||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
logger: DadosferaLogger;
|
||||
redirectUrl: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private authClient: AuthClientService,
|
||||
private apiKeyService: ApiKeyService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
|
||||
switch (process.env.ENV) {
|
||||
case 'stg':
|
||||
this.redirectUrl = `https://app.${process.env.ENV}.dadosfera.ai/auth/login`;
|
||||
break;
|
||||
case 'prd':
|
||||
this.redirectUrl = `https://app.dadosfera.ai/auth/login`;
|
||||
break;
|
||||
default:
|
||||
this.redirectUrl = `http://localhost:4200/auth/login`;
|
||||
}
|
||||
}
|
||||
|
||||
@Post('sign-in')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async signIn(
|
||||
@Body() { username, password, totp }: AuthSignInReq,
|
||||
@Headers('Dadosfera-Lang') language: string,
|
||||
): Promise<AuthSignInRes> {
|
||||
this.logger.info('/auth - SignIn');
|
||||
const metadata = new Metadata();
|
||||
metadata.add('language', language || 'pt-br');
|
||||
return this.authClient.signIn({ username, password, totp }, metadata);
|
||||
@Language() language: LanguageEnum,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
try {
|
||||
this.logger.info('/auth - SignIn');
|
||||
const metadata = PackTheMetadata({ language });
|
||||
this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON()));
|
||||
const data = await this.authClient.signIn({ username, password, totp }, metadata);
|
||||
|
||||
if (data.tokens) {
|
||||
this.authClient.writeAuthSession(res, {
|
||||
accessToken: data.tokens.accessToken,
|
||||
refreshToken: data.tokens.refreshToken,
|
||||
userId: data.user.id
|
||||
});
|
||||
}
|
||||
|
||||
return res.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error('/auth - SignIn - ERROR', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Post('sign-out')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async signOut(
|
||||
@Language() language: LanguageEnum,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
try {
|
||||
this.logger.info('/auth - SignOut');
|
||||
|
||||
this.authClient.cleanUpAuthSession(res);
|
||||
|
||||
return res.send();
|
||||
} catch (error) {
|
||||
this.logger.error('/auth - SignIn - ERROR', error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Post('refresh-access-token')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOkResponse({ type: AuthRefreshAccessTokenRes })
|
||||
async refreshAccessToken(
|
||||
@Body() body: AuthRefreshAccessTokenReq,
|
||||
@Language() language: LanguageEnum,
|
||||
@Headers('origin') origin: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.logger.info('/auth - RefreshAccessToken');
|
||||
const frontHost = origin.replace(/^https?:\/\//, '');
|
||||
const { refreshToken, userId } = body;
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
language,
|
||||
custom_host: frontHost,
|
||||
});
|
||||
|
||||
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
||||
|
||||
this.authClient.writeAuthSession(res, {
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
userId
|
||||
});
|
||||
|
||||
return res.send(data);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('/sso/snowflake')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.SNOWFLAKE.permissions.OPEN)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -78,23 +184,7 @@ export class AuthController {
|
||||
});
|
||||
}
|
||||
|
||||
@Post('refresh-access-token')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async refreshAccessToken(
|
||||
@Body()
|
||||
{ refreshToken, customerName: customer_name }: AuthRefreshAccessTokenReq,
|
||||
@Headers('Dadosfera-Lang') language: string,
|
||||
) {
|
||||
this.logger.info('/auth - RefreshAccessToken');
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
customer_name,
|
||||
language,
|
||||
});
|
||||
|
||||
return this.authClient.refreshAccessToken({ refreshToken }, metadata);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('change-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async changePassword(
|
||||
@@ -103,26 +193,36 @@ export class AuthController {
|
||||
) {
|
||||
this.logger.info('/auth - change-password');
|
||||
|
||||
const { oldPassword, newPassword } = body;
|
||||
const { oldPassword, newPassword, totpCode } = body;
|
||||
const { authorization: accessToken } = headers;
|
||||
|
||||
return this.authClient.changePassword({
|
||||
accessToken,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
totpCode,
|
||||
});
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async resetPassword(@Body() body: AuthResetPasswordRequest) {
|
||||
async resetPassword(
|
||||
@Body() body: AuthResetPasswordRequest,
|
||||
@Language() language: LanguageEnum,
|
||||
) {
|
||||
this.logger.info('/auth - reset-password');
|
||||
const metadata = PackTheMetadata({
|
||||
language: language,
|
||||
});
|
||||
|
||||
const { username } = body;
|
||||
|
||||
return this.authClient.resetPassword({ username });
|
||||
await this.authClient.resetPassword({ username }, metadata);
|
||||
return { authProvider: process.env.AUTH_PROVIDER || 'cognito' };
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('verify-reset-password-code')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyResetPasswordCode(
|
||||
@@ -135,20 +235,29 @@ export class AuthController {
|
||||
return this.authClient.verifyResetPasswordCode({ username, code });
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('confirm-reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async confirmResetPassword(@Body() body: AuthConfirmResetPasswordRequest) {
|
||||
async confirmResetPassword(
|
||||
@Body() body: AuthConfirmResetPasswordRequest,
|
||||
@Headers('origin') origin: string,
|
||||
) {
|
||||
this.logger.info('/auth - confirm-reset-password');
|
||||
|
||||
const frontHost = origin.replace(/^https?:\/\//, '');
|
||||
const metadata = PackTheMetadata({ custom_host: frontHost });
|
||||
const { username, code, newPassword } = body;
|
||||
|
||||
return this.authClient.confirmResetPassword({
|
||||
username,
|
||||
code,
|
||||
newPassword,
|
||||
});
|
||||
return this.authClient.confirmResetPassword(
|
||||
{
|
||||
username,
|
||||
code,
|
||||
newPassword,
|
||||
},
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('enable-totp')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async enableTotpMFA(
|
||||
@@ -163,6 +272,7 @@ export class AuthController {
|
||||
return this.authClient.enableTotpMFA({ accessToken, password });
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('disable-totp')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async disableTotpMFA(
|
||||
@@ -177,6 +287,7 @@ export class AuthController {
|
||||
return this.authClient.disableTotpMFA({ accessToken, password });
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('dismiss-totp')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async dismissTotpMFA(@Headers() headers) {
|
||||
@@ -187,6 +298,7 @@ export class AuthController {
|
||||
return this.authClient.dismissTotpMFA({ accessToken });
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('verify-totp')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyTotp(@Body() body: AuthVerifyTotpMfaRequest, @Headers() headers) {
|
||||
@@ -198,10 +310,227 @@ export class AuthController {
|
||||
return this.authClient.verifyTotp({ accessToken, totp });
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Authenticated()
|
||||
@ApiSecurity('access-token')
|
||||
@Get('verify-access-token')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
verifyAccessToken() {
|
||||
return { access_token_status: 'valid' };
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('session/:session')
|
||||
getSession(@Param('session') session: string) {
|
||||
return this.authClient.getSession(session);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('oauth/google')
|
||||
@UseGuards(AuthGuard('google-login'))
|
||||
googleOauth() {
|
||||
this.logger.info('/oauth/google');
|
||||
return true;
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('oauth/google/callback')
|
||||
@UseGuards(AuthGuard('google-login'))
|
||||
@Redirect()
|
||||
async googleOauthCallback(@Req() req) {
|
||||
const { url, email, token, language = 'pt-br' } = await this.callback(req);
|
||||
if (url.searchParams.get('error')) {
|
||||
this.logger.error('/oauth/google - ERROR');
|
||||
return { url: url.href };
|
||||
}
|
||||
|
||||
await this.authClient
|
||||
.oauthSignIn({
|
||||
username: email,
|
||||
token,
|
||||
})
|
||||
.then(({ session }) => {
|
||||
this.logger.info('/oauth/google - SUCESS');
|
||||
url.searchParams.set('session', session);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error('/oauth/google - ERROR');
|
||||
let error = OauthErrors.INVALID_CREDENTIALS[language].error;
|
||||
let error_description =
|
||||
OauthErrors.INVALID_CREDENTIALS[language].error_description;
|
||||
switch (err.details) {
|
||||
case ErrorCodes.USER.NOT_FOUND:
|
||||
error = OauthErrors.USER_NOT_FOUND[language].error;
|
||||
error_description =
|
||||
OauthErrors.USER_NOT_FOUND[language].error_description(email);
|
||||
break;
|
||||
case ErrorCodes.AUTH.UNAUTHORIZED:
|
||||
error = OauthErrors.INVALID_SESSION[language].error;
|
||||
error_description =
|
||||
OauthErrors.INVALID_SESSION[language].error_description;
|
||||
break;
|
||||
}
|
||||
url.searchParams.set('error', error);
|
||||
url.searchParams.set('error_description', error_description);
|
||||
return null;
|
||||
});
|
||||
return { url: url.href };
|
||||
}
|
||||
|
||||
async callback(req: Request) {
|
||||
const { error, state } = req.query;
|
||||
const { authInfo } = req;
|
||||
const url = new URL(this.redirectUrl);
|
||||
let email, token, error_title, error_description;
|
||||
let language: 'pt-br' | 'en-us' = 'pt-br';
|
||||
|
||||
const stateObject = jwt.verify(
|
||||
state as string,
|
||||
process.env.JWT_PRIVATE_KEY,
|
||||
);
|
||||
if (typeof stateObject != 'string') language = stateObject.language;
|
||||
|
||||
if (error || !authInfo) {
|
||||
this.logger.error(error);
|
||||
if (!authInfo) this.logger.error('No authInfo', { request: req });
|
||||
|
||||
error_title = OauthErrors.INVALID_CREDENTIALS[language].error;
|
||||
error_description =
|
||||
OauthErrors.INVALID_CREDENTIALS[language].error_description;
|
||||
if (error) error_description += ` - [${error}]`;
|
||||
} else {
|
||||
const { accessToken } = authInfo as any;
|
||||
const { _json: userInfo } = req.user as any;
|
||||
|
||||
email = userInfo.email;
|
||||
token = accessToken;
|
||||
}
|
||||
|
||||
if (error_title) {
|
||||
url.searchParams.set('error', error_title);
|
||||
url.searchParams.set('error_description', error_description);
|
||||
}
|
||||
|
||||
return { token, email, url, language };
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('users/block')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async blockUsers(
|
||||
@Language() language: LanguageEnum,
|
||||
@User() user: RequestUser,
|
||||
@Body() body: BulkEditRequest,
|
||||
) {
|
||||
this.logger.info('blockUsers - Starting request');
|
||||
|
||||
try {
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
this.logger.debug('Calling blockUsers service', {
|
||||
metadata: {
|
||||
access_token: metadata.get('access_token'),
|
||||
language: metadata.get('language'),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await this.authClient.blockUsers(body.users, metadata);
|
||||
this.logger.info('blockUsers - Success', { result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('blockUsers - Error', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('users/unblock')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async unblockUsers(
|
||||
@Language() language: LanguageEnum,
|
||||
@User() user: RequestUser,
|
||||
@Body() body: BulkEditRequest,
|
||||
) {
|
||||
this.logger.info('unblockUsers');
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
return this.authClient.unblockUsers(body.users, metadata);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Post('users/reset')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async resetUsers(
|
||||
@Language() language: LanguageEnum,
|
||||
@User() user: RequestUser,
|
||||
@Body() body: BulkEditRequest,
|
||||
) {
|
||||
this.logger.info('resetUsers');
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
return this.authClient.resetUsers(body.users, metadata);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
async getMe(@Req() req: Request, @Res() res: Response) {
|
||||
this.logger.info('GET /auth/me ')
|
||||
this.logger.info(JSON.stringify(req.headers));
|
||||
|
||||
// Check for API key header first
|
||||
const apiKey = req.get('X-Api-key');
|
||||
if (apiKey) {
|
||||
this.logger.info('Authenticating via X-Api-key header');
|
||||
const { api_key } = await this.apiKeyService.get(apiKey);
|
||||
|
||||
const userDto: UserDTO = {
|
||||
id: api_key.user_id,
|
||||
name: api_key.username,
|
||||
email: api_key.username,
|
||||
customer: {
|
||||
id: api_key.customer_id,
|
||||
name: api_key.customer_name,
|
||||
tier: api_key.customer_tier,
|
||||
},
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
return res.status(200).json(userDto);
|
||||
}
|
||||
|
||||
// Get token and headers
|
||||
const accessToken = req.cookies['ddf-auth'];
|
||||
const refreshToken = req.cookies['ddf-refresh-auth'];
|
||||
const userId = req.cookies['ddf-user-id'];
|
||||
const resourceHost = req.headers["x-original-url"] as string || "" ;
|
||||
|
||||
const hasUserSession = Boolean(accessToken) && Boolean(userId);
|
||||
this.logger.info('Has User Session: ' + hasUserSession);
|
||||
|
||||
if (!hasUserSession) {
|
||||
throw new UnauthorizedException()
|
||||
}
|
||||
|
||||
try {
|
||||
const userDto = await this.authClient.validateUserSession(accessToken, resourceHost);
|
||||
return res.status(200).json(userDto);
|
||||
} catch (error) {
|
||||
|
||||
if (!refreshToken) {
|
||||
this.logger.error('Invalid refresh token or customer name');
|
||||
throw new UnauthorizedException("Invalid refresh token or customer name");
|
||||
};
|
||||
|
||||
const {
|
||||
authSession,
|
||||
user
|
||||
} = await this.authClient.refreshUserSession(refreshToken, userId, resourceHost);
|
||||
this.authClient.writeAuthSession(res, authSession);
|
||||
return res.status(200).json(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,20 @@ import { AuthController } from './auth.controller';
|
||||
import { AuthClientService } from './auth.service';
|
||||
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { GoogleLoginStrategy } from './passport-strategies/google-strategy';
|
||||
import { getOauthSecrets } from 'src/utils/OauthSecrets';
|
||||
import { ApiKeyModule } from '../api-key/api-key.module';
|
||||
const client = new DucClient();
|
||||
|
||||
@Module({
|
||||
imports: [ClientsModule.register([client.providerOptions])],
|
||||
imports: [ClientsModule.register([client.providerOptions]), ApiKeyModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthClientService, DadosferaLogger],
|
||||
providers: [
|
||||
AuthClientService,
|
||||
DadosferaLogger,
|
||||
GoogleLoginStrategy,
|
||||
{ provide: 'OAUTH_SECRETS', useValue: getOauthSecrets() },
|
||||
],
|
||||
exports: [AuthClientService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { OnModuleInit, Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
OnModuleInit,
|
||||
Inject,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
import { AuthProtoService as AuthServiceInterface } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import {
|
||||
AuthProtoService as AuthServiceInterface,
|
||||
UsersProtoService,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import {
|
||||
AuthSnowflakeSignInRequest,
|
||||
AuthSignInRequest,
|
||||
@@ -17,15 +27,28 @@ import {
|
||||
AuthResetPasswordRequest,
|
||||
AuthVerifyResetPasswordCodeRequest,
|
||||
AuthConfirmResetPasswordRequest,
|
||||
AuthSignInResponse,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { BulkEditResponse, UserDTO } from './dtos/login';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
type AuthSession = {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AuthClientService implements OnModuleInit {
|
||||
logger: DadosferaLogger;
|
||||
|
||||
private authService: AuthServiceInterface;
|
||||
private userService: UsersProtoService;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
@@ -38,6 +61,10 @@ export class AuthClientService implements OnModuleInit {
|
||||
this.authService = this.grpcClient.getService<AuthServiceInterface>(
|
||||
ProtoServices.AuthProtoService,
|
||||
);
|
||||
|
||||
this.userService = this.grpcClient.getService<UsersProtoService>(
|
||||
ProtoServices.UsersProtoService,
|
||||
);
|
||||
}
|
||||
|
||||
async getPublicKeys() {
|
||||
@@ -52,25 +79,63 @@ export class AuthClientService implements OnModuleInit {
|
||||
return lastValueFrom(this.authService.AuthSnowflakeSignIn(input));
|
||||
}
|
||||
|
||||
checkDedicatedProxy({ customer }: AuthSignInResponse) {
|
||||
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
||||
this.logger.info(
|
||||
'SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY,
|
||||
);
|
||||
this.logger.info('Customer ID: ' + customer.id);
|
||||
|
||||
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== customer.id) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
// Bloquear o customer de acesso o maestro publico
|
||||
this.logger.info(
|
||||
'Check if customer have network policy: ' + customer.modules,
|
||||
);
|
||||
const hasNetworkPolicyModule = customer.modules.includes('network-policy');
|
||||
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(
|
||||
{ username, password, totp }: AuthSignInRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
this.logger.info('SignIn');
|
||||
|
||||
return lastValueFrom(
|
||||
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
||||
);
|
||||
let result: AuthSignInResponse;
|
||||
|
||||
try {
|
||||
result = await lastValueFrom(
|
||||
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('SignIn - Error during sign-in');
|
||||
this.logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result.customer) {
|
||||
this.checkDedicatedProxy(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async refreshAccessToken(
|
||||
{ refreshToken }: AuthRefreshAccessTokenRequest,
|
||||
{ refreshToken, userId }: AuthRefreshAccessTokenRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
this.logger.info('RefreshAccessToken');
|
||||
|
||||
return lastValueFrom(
|
||||
this.authService.AuthRefreshAccessToken({ refreshToken }, metadata),
|
||||
this.authService.AuthRefreshAccessToken(
|
||||
{ refreshToken, userId },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,6 +143,7 @@ export class AuthClientService implements OnModuleInit {
|
||||
accessToken,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
totpCode,
|
||||
}: AuthChangePasswordRequest) {
|
||||
this.logger.info('ChangePassword');
|
||||
|
||||
@@ -86,14 +152,20 @@ export class AuthClientService implements OnModuleInit {
|
||||
accessToken,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
totpCode,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async resetPassword({ username }: AuthResetPasswordRequest) {
|
||||
async resetPassword(
|
||||
{ username }: AuthResetPasswordRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
this.logger.info('resetPassword');
|
||||
|
||||
return lastValueFrom(this.authService.AuthResetPassword({ username }));
|
||||
return lastValueFrom(
|
||||
this.authService.AuthResetPassword({ username }, metadata),
|
||||
);
|
||||
}
|
||||
|
||||
async verifyResetPasswordCode({
|
||||
@@ -107,19 +179,21 @@ export class AuthClientService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
|
||||
async confirmResetPassword({
|
||||
username,
|
||||
code,
|
||||
newPassword,
|
||||
}: AuthConfirmResetPasswordRequest) {
|
||||
async confirmResetPassword(
|
||||
{ username, code, newPassword }: AuthConfirmResetPasswordRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
this.logger.info('confirmResetPassword');
|
||||
|
||||
return lastValueFrom(
|
||||
this.authService.AuthConfirmResetPassword({
|
||||
username,
|
||||
code,
|
||||
newPassword,
|
||||
}),
|
||||
this.authService.AuthConfirmResetPassword(
|
||||
{
|
||||
username,
|
||||
code,
|
||||
newPassword,
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,4 +226,273 @@ export class AuthClientService implements OnModuleInit {
|
||||
this.authService.AuthVerifyTotpMfa({ accessToken, totp }),
|
||||
);
|
||||
}
|
||||
|
||||
async getSession(session: string) {
|
||||
return lastValueFrom(this.authService.AuthGetSession({ session }));
|
||||
}
|
||||
|
||||
async oauthSignIn(data: { username: string; token: string }) {
|
||||
const { username, token } = data;
|
||||
return lastValueFrom(
|
||||
this.authService.AuthOauthSignIn({ token, username, refreshToken: '' }),
|
||||
);
|
||||
}
|
||||
|
||||
async blockUsers(
|
||||
users: string[],
|
||||
metadata: Metadata,
|
||||
): Promise<BulkEditResponse> {
|
||||
this.logger.info('blockUsers - Service starting');
|
||||
|
||||
try {
|
||||
this.logger.debug('Calling BlockUser gRPC method', {
|
||||
metadata: {
|
||||
access_token: metadata.get('access_token'),
|
||||
language: metadata.get('language'),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await lastValueFrom<BulkEditResponse>(
|
||||
this.authService.BlockUser({ users }, metadata),
|
||||
);
|
||||
|
||||
this.logger.info('blockUsers - Service success', { response });
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.logger.error('blockUsers - Service error', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async unblockUsers(
|
||||
users: string[],
|
||||
metadata: Metadata,
|
||||
): Promise<BulkEditResponse> {
|
||||
this.logger.info('unblockUsers');
|
||||
return await lastValueFrom(
|
||||
this.authService.UnblockUser({ users }, metadata),
|
||||
);
|
||||
}
|
||||
|
||||
async resetUsers(
|
||||
users: string[],
|
||||
metadata: Metadata,
|
||||
): Promise<BulkEditResponse> {
|
||||
this.logger.info('resetUsers');
|
||||
|
||||
try {
|
||||
this.logger.debug('Calling ResetUser gRPC method', {
|
||||
metadata: {
|
||||
access_token: metadata.get('access_token'),
|
||||
language: metadata.get('language'),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await lastValueFrom<BulkEditResponse>(
|
||||
this.authService.ResetUser({ users }, metadata),
|
||||
);
|
||||
|
||||
this.logger.info('resetUsers - Success', { response });
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.logger.error('resetUsers - Error', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async validateUserSession(accessToken: any, resourceHost: string) {
|
||||
const payload = await this.validateJwtToken(accessToken);
|
||||
|
||||
const userDto = await this.getUserfromPayload(payload);
|
||||
|
||||
this.validateResourceAccess(resourceHost, userDto);
|
||||
return userDto;
|
||||
}
|
||||
|
||||
public async refreshUserSession(
|
||||
refreshToken: string,
|
||||
userId: string,
|
||||
originHeader: string,
|
||||
): Promise<{
|
||||
user: UserDTO;
|
||||
authSession: AuthSession;
|
||||
}> {
|
||||
const metadata = PackTheMetadata({});
|
||||
|
||||
this.logger.info('Call Refresh Token');
|
||||
const refreshCredentials = await this.refreshAccessToken(
|
||||
{ refreshToken, userId },
|
||||
metadata,
|
||||
);
|
||||
this.logger.info('Finish Refresh Token');
|
||||
|
||||
const userDto = await this.validateUserSession(
|
||||
refreshCredentials.accessToken,
|
||||
originHeader,
|
||||
);
|
||||
return {
|
||||
user: userDto,
|
||||
authSession: {
|
||||
accessToken: refreshCredentials.accessToken,
|
||||
refreshToken: refreshCredentials.refreshToken,
|
||||
userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public writeAuthSession(res: Response, data: AuthSession) {
|
||||
let exp = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
if (data.accessToken) {
|
||||
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
|
||||
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
|
||||
|
||||
this.logger.info('Set Cookie ddf-auth');
|
||||
res.cookie('ddf-auth', data.accessToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.refreshToken) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth');
|
||||
res.cookie('ddf-refresh-auth', data.refreshToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.userId) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth');
|
||||
res.cookie('ddf-user-id', data.userId, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public cleanUpAuthSession(res: Response) {
|
||||
const exp = 1000 * 60 * 3;
|
||||
|
||||
res.cookie('ddf-auth', '', {
|
||||
domain: 'dadosfera.ai',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
res.cookie('ddf-refresh-auth', '', {
|
||||
domain: 'dadosfera.ai',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
this.logger.info('Clean cookie sessions');
|
||||
}
|
||||
|
||||
private async validateJwtToken(token: string) {
|
||||
const decoded: any = token && jwt.decode(token, { complete: true });
|
||||
if (!decoded) throw new Error('Invalid token');
|
||||
|
||||
const { kid } = decoded.header;
|
||||
// Busca a chave pública
|
||||
const { keys } = await this.getPublicKeys();
|
||||
const pemValue = keys.find((k) => k.kid === kid)?.pem;
|
||||
if (!pemValue) throw new Error('Public key not found');
|
||||
jwt.verify(token, pemValue);
|
||||
|
||||
return decoded.payload;
|
||||
}
|
||||
|
||||
private async getUserfromPayload(payload: JwtPayload): Promise<UserDTO> {
|
||||
this.logger.info('getUser');
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: payload.customer_id,
|
||||
});
|
||||
|
||||
const { user } = await lastValueFrom(
|
||||
this.userService.UserFindOneById({ id: payload.user_id }, metadata),
|
||||
);
|
||||
|
||||
const userDto: UserDTO = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
jobTitle: user?.jobTitle || null,
|
||||
department: user?.department || null,
|
||||
hierarchy: user?.hierarchy || null,
|
||||
customer: {
|
||||
id: payload.customer_id,
|
||||
name: payload.customer_name,
|
||||
tier: payload.customer_tier,
|
||||
},
|
||||
// Raw permission seqids from the JWT. Consumers own the seqid->meaning
|
||||
// mapping (e.g. Orchest's auth-server); Maestro reports them as-is.
|
||||
permissions: payload.permissions ?? [],
|
||||
};
|
||||
|
||||
return userDto;
|
||||
}
|
||||
|
||||
private validateResourceAccess(host: string, user: UserDTO) {
|
||||
this.logger.info(
|
||||
"Validate whether the source URL is a resource belonging to the user's client",
|
||||
);
|
||||
this.logger.info('Host: ' + host);
|
||||
this.logger.info('Customer: ' + user.customer.name);
|
||||
|
||||
const hostParts = host.split('.');
|
||||
const domain = hostParts[0];
|
||||
const isResouceStg = hostParts[1] === 'stg';
|
||||
|
||||
const notFoundCustomerInDomain = !domain.includes('-')
|
||||
|
||||
if (notFoundCustomerInDomain) {
|
||||
this.logger.info(`Not found Customer Name in domain`);
|
||||
return;
|
||||
}
|
||||
|
||||
const domainParts = domain.split('-');
|
||||
|
||||
const customerInDomain = domainParts[domainParts.length - 1];
|
||||
|
||||
if (isResouceStg && process.env.ENV !== 'stg') {
|
||||
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
||||
throw new HttpException(
|
||||
`Customer ${user.customer.name} cannot access ${host}`,
|
||||
HttpStatus.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if (customerInDomain != user.customer.name) {
|
||||
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
||||
throw new HttpException(
|
||||
`Customer ${user.customer.name} cannot access ${host}`,
|
||||
HttpStatus.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Link } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities';
|
||||
import {
|
||||
AuthSignInRequest,
|
||||
AuthSignInResponse,
|
||||
@@ -66,12 +67,28 @@ export class AuthUser {
|
||||
export class AuthCustomer {
|
||||
@ApiProperty()
|
||||
modules: string[];
|
||||
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
@ApiProperty()
|
||||
tier: string;
|
||||
|
||||
@ApiProperty()
|
||||
scheduleLimit: string;
|
||||
|
||||
@ApiProperty()
|
||||
links: Link[];
|
||||
|
||||
@ApiProperty()
|
||||
themeEnabled: boolean;
|
||||
@ApiProperty()
|
||||
enforceMfa: boolean;
|
||||
}
|
||||
|
||||
export class AuthSignInReq implements AuthSignInRequest {
|
||||
@@ -79,7 +96,10 @@ export class AuthSignInReq implements AuthSignInRequest {
|
||||
username: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
@ApiPropertyOptional()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'TOTP code used to login when Multi-Factor Authentication is enabled',
|
||||
})
|
||||
totp: string;
|
||||
}
|
||||
|
||||
@@ -102,5 +122,36 @@ export class AuthRefreshAccessTokenReq {
|
||||
@ApiProperty()
|
||||
refreshToken: string;
|
||||
@ApiProperty()
|
||||
customerName: string;
|
||||
userId: string;
|
||||
}
|
||||
export class AuthRefreshAccessTokenRes {
|
||||
@ApiProperty()
|
||||
permissions: string[];
|
||||
@ApiProperty()
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface BulkEditRequest {
|
||||
users: string[];
|
||||
}
|
||||
|
||||
export interface BulkEditResponse {
|
||||
message: string;
|
||||
successfulUsers: string[];
|
||||
failedUsers: string[];
|
||||
}
|
||||
|
||||
export type UserDTO = {
|
||||
id: string,
|
||||
name: string,
|
||||
email: string,
|
||||
jobTitle?: string,
|
||||
department?: string,
|
||||
hierarchy?: string,
|
||||
customer: {
|
||||
id: string,
|
||||
name: string,
|
||||
tier: string,
|
||||
},
|
||||
permissions: number[],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
AuthenticateOptionsGoogle,
|
||||
Profile,
|
||||
Strategy,
|
||||
StrategyOptions,
|
||||
} from 'passport-google-oauth20';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OauthSecrets } from 'src/utils/OauthSecrets';
|
||||
import { Request } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleLoginStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'google-login',
|
||||
) {
|
||||
redirect_uri: string;
|
||||
constructor(
|
||||
@Inject('OAUTH_SECRETS')
|
||||
private readonly oauthSecrets: OauthSecrets,
|
||||
) {
|
||||
const options: StrategyOptions = {
|
||||
clientID: oauthSecrets['google-login'].client_id,
|
||||
clientSecret: oauthSecrets['google-login'].client_secret,
|
||||
callbackURL: oauthSecrets['google-login'].redirect_uri,
|
||||
scope: ['email', 'profile', 'openid'],
|
||||
};
|
||||
console.log("GoogleLoginStrategy", options.clientID, options.callbackURL);
|
||||
const verify = (
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
done,
|
||||
) => {
|
||||
return done(null, profile, { accessToken, refreshToken });
|
||||
};
|
||||
|
||||
super(options, verify);
|
||||
}
|
||||
|
||||
authenticate(req: Request, options: AuthenticateOptionsGoogle) {
|
||||
const language = req.headers['dadosfera-lang'] || req.query.language;
|
||||
options.state = jwt.sign({ language }, process.env.JWT_PRIVATE_KEY);
|
||||
super.authenticate(req, options);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
} from '@nestjs/microservices';
|
||||
import { credentials } from '@grpc/grpc-js';
|
||||
import { Catalog } from '@dadosfera/protospack-v2';
|
||||
import { PlatformInterfaces } from '@dadosfera/protospack-v2';
|
||||
|
||||
const isLocalConnection =
|
||||
process.env.PIFACTORY_URL.startsWith('pi-factory:') ||
|
||||
process.env.PIFACTORY_URL.includes('0.0.0.0');
|
||||
|
||||
export class CatalogClientConfiguration {
|
||||
public name = 'CatalogClientConfiguration';
|
||||
@@ -15,14 +20,13 @@ export class CatalogClientConfiguration {
|
||||
package: [
|
||||
Catalog.ProtoPackages.ReadPackage,
|
||||
Catalog.ProtoPackages.WritePackage,
|
||||
PlatformInterfaces.ProtoPackages.WritePackage
|
||||
],
|
||||
credentials:
|
||||
process.env.LOCAL_ENV || process.env.ENV === 'local'
|
||||
? undefined
|
||||
: credentials.createSsl(),
|
||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||
protoPath: [
|
||||
Catalog.ProtoPaths.ReadFilePath,
|
||||
Catalog.ProtoPaths.WriteFilePath,
|
||||
PlatformInterfaces.ProtoPaths.WriteFilePath
|
||||
],
|
||||
loader: {
|
||||
keepCase: true,
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Headers,
|
||||
HttpException,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseFilters,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ValidationPipe } from '../../pipes/object-validation.pipe';
|
||||
import {
|
||||
ApiCreatedResponse,
|
||||
ApiHeaders,
|
||||
ApiOkResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
RequireModule,
|
||||
RequireSomePermission,
|
||||
} from '../../authentication/authentication.decorator';
|
||||
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
|
||||
} from '../../decorators/authentication.decorator';
|
||||
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
|
||||
import { CatalogService } from './catalog.service';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
||||
import { RequestUser, User } from 'src/authentication/user.decorator';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import {
|
||||
BatchRemoveRlsRulesRequest,
|
||||
GetDatasetCatalogTaskRes,
|
||||
ICatalogAllRequest,
|
||||
ICatalogAllResponse,
|
||||
IColumnsMetadataResponse,
|
||||
ICreateDataAsset,
|
||||
@@ -31,11 +47,26 @@ import {
|
||||
IMakeAComment,
|
||||
IOneDataAsset,
|
||||
IPreviewResponse,
|
||||
IUpdateCertificationStatusRequest,
|
||||
IUpdateDataRequest,
|
||||
TriggerCatalogReq,
|
||||
TriggerCatalogRes,
|
||||
} from './dtos';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { Language } from 'src/decorators/language.decorator';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
||||
import {
|
||||
AddRlsRuleRequest,
|
||||
GetNimbusDashboardsRequest,
|
||||
GetRlsRulesRequest,
|
||||
RegisterDatasetWithMetatadaRequest,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||
import { Response } from 'express';
|
||||
import { TypeParser } from 'src/utils/FileParser/parser-types';
|
||||
|
||||
@ApiTags('Catalog')
|
||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||
@Controller('catalog')
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
@Authenticated()
|
||||
@@ -54,9 +85,12 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async searchCatalog(
|
||||
@User() user: RequestUser,
|
||||
@Query() query,
|
||||
@Query() query: ICatalogAllRequest,
|
||||
): Promise<ICatalogAllResponse> {
|
||||
const { user_id, customer_name, customer_id, username, permissions } = user;
|
||||
this.logger.info(`/catalog - searchCatalog`, {
|
||||
@@ -88,7 +122,60 @@ export class CatalogController {
|
||||
return res;
|
||||
}
|
||||
|
||||
@Get('/download')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async dowloadAsserts(
|
||||
@User() user: RequestUser,
|
||||
@Query() query: ICatalogAllRequest,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const { user_id, customer_name, customer_id, username, permissions } = user;
|
||||
this.logger.info(`/catalog/download - searchCatalog`, {
|
||||
user_id,
|
||||
customer_name,
|
||||
});
|
||||
|
||||
const is_data_manager = permissions.includes(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||
);
|
||||
|
||||
const roles = await this.catalogService.getUserRolesIds(user_id);
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
user_id,
|
||||
customer_id,
|
||||
customer_name,
|
||||
username,
|
||||
roles,
|
||||
is_data_manager,
|
||||
});
|
||||
|
||||
const {
|
||||
file,
|
||||
filename
|
||||
} = await this.catalogService.downloadAssets(
|
||||
query,
|
||||
metadata,
|
||||
customer_id,
|
||||
);
|
||||
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
res.end(file);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('data-asset')
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async findByPipelineAndObject(@User() user: RequestUser, @Query() query) {
|
||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||
const { pipeline, object } = query;
|
||||
@@ -98,7 +185,7 @@ export class CatalogController {
|
||||
});
|
||||
|
||||
if (!pipeline || !object) {
|
||||
throw new HttpException('Query params not provided', 400);
|
||||
throw new BadRequestException('Query params not provided');
|
||||
}
|
||||
|
||||
const is_data_manager = permissions.includes(
|
||||
@@ -136,17 +223,20 @@ export class CatalogController {
|
||||
return { data_asset };
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this data asset.',
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('tags')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async findAllTags(@Body() body) {
|
||||
this.logger.info(`/catalog - ON FIND ALL TAGS ROUTE`, {
|
||||
user: body.info.user_id,
|
||||
@@ -165,19 +255,67 @@ export class CatalogController {
|
||||
return res;
|
||||
}
|
||||
|
||||
@Get('schemas')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
async findSchemas(@User() user: RequestUser) {
|
||||
const { username, user_id, customer_id, customer_name } = user;
|
||||
this.logger.info(`/catalog - ON FIND SCHEMAS ROUTE`, {
|
||||
username,
|
||||
customer_name,
|
||||
});
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
username,
|
||||
user_id,
|
||||
customer_id,
|
||||
customer_name,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await this.catalogService.findSchemas(metadata);
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new HttpException(error.message, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Get('custom-properties')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
async getCustomPropertyDefinitions(@User() user: RequestUser) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
|
||||
return this.catalogService.getCustomPropertyDefinitions(metadata);
|
||||
}
|
||||
|
||||
@Get('data-asset/:id')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async getDataAsset(
|
||||
@User() user: RequestUser,
|
||||
@Headers() headers,
|
||||
@Param('id') id,
|
||||
@Param('id') id: string,
|
||||
@Query('shared') shared?: 'true',
|
||||
) {
|
||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||
|
||||
this.logger.info(`/catalog - ON GET ONE DASHBOARD METABASE ROUTE`, {
|
||||
this.logger.info(`GET /data-asset/${id}`, {
|
||||
username,
|
||||
customer_name,
|
||||
});
|
||||
@@ -188,7 +326,7 @@ export class CatalogController {
|
||||
let has_permission = false;
|
||||
const metadata = PackTheMetadata({
|
||||
username,
|
||||
user_id: undefined,
|
||||
user_id,
|
||||
customer_id,
|
||||
customer_name,
|
||||
});
|
||||
@@ -200,27 +338,27 @@ export class CatalogController {
|
||||
id,
|
||||
metadata,
|
||||
});
|
||||
has_permission =
|
||||
is_data_manager ||
|
||||
data_asset?.owner === username ||
|
||||
(user_roles as Array<any>).some((r) => data_asset.p_roles.includes(r)) ||
|
||||
data_asset.p_users.includes(user_id);
|
||||
if (
|
||||
shared === 'true' &&
|
||||
(data_asset.share_type === undefined || data_asset.share_type === 'none')
|
||||
)
|
||||
throw new NotFoundException();
|
||||
if (!has_permission)
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this data asset.',
|
||||
);
|
||||
delete data_asset.p_roles;
|
||||
delete data_asset.p_users;
|
||||
|
||||
if (data_asset?.owner === username) has_permission = true;
|
||||
|
||||
for (const role of user_roles) {
|
||||
if (data_asset.p_roles.includes(role)) has_permission = true;
|
||||
}
|
||||
|
||||
if (data_asset.p_users.includes(user_id)) has_permission = true;
|
||||
|
||||
if (is_data_manager || has_permission) {
|
||||
delete data_asset.p_roles;
|
||||
delete data_asset.p_users;
|
||||
return { data_asset };
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
'You do not have permission to access this data asset.',
|
||||
403,
|
||||
);
|
||||
return { data_asset };
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('data-asset/rls/:id')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
@@ -229,7 +367,7 @@ export class CatalogController {
|
||||
async getDataAssetRls(
|
||||
@User() user: RequestUser,
|
||||
@Headers() headers,
|
||||
@Param('id') id,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||
|
||||
@@ -271,9 +409,8 @@ export class CatalogController {
|
||||
return { data_asset };
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this data asset.',
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,10 +419,13 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async getDataAssetColumnsMetadata(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-Lang') language,
|
||||
@Param('id') id,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
): Promise<IColumnsMetadataResponse> {
|
||||
const { customer_name, customer_id, user_id, username } = user;
|
||||
|
||||
@@ -313,12 +453,15 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async getDataAssetPreview(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-lang') language,
|
||||
@Param('id') id,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
): Promise<IPreviewResponse> {
|
||||
const { customer_name, customer_id, user_id, username } = user;
|
||||
const { customer_name, customer_id, user_id, username, customer_modules } = user;
|
||||
|
||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
||||
user_id,
|
||||
@@ -331,6 +474,7 @@ export class CatalogController {
|
||||
user_id,
|
||||
username,
|
||||
language,
|
||||
is_mask: customer_modules.some(mod => mod === 'pii')
|
||||
});
|
||||
|
||||
const preview = await this.catalogService.getDatasetPreview(id, metadata);
|
||||
@@ -343,10 +487,14 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async getDataAssetDocs(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-Lang') language,
|
||||
@Param('id') id,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Query('asset_type') asset_type: string,
|
||||
): Promise<IDocsResponse> {
|
||||
const { customer_name, customer_id, user_id, username } = user;
|
||||
|
||||
@@ -363,7 +511,7 @@ export class CatalogController {
|
||||
language,
|
||||
});
|
||||
|
||||
const docs = await this.catalogService.getDataDocs(id, metadata);
|
||||
const docs = await this.catalogService.getDataDocs(id, asset_type, metadata);
|
||||
|
||||
return { docs };
|
||||
}
|
||||
@@ -373,10 +521,13 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async updateDataAsset(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-Lang') language,
|
||||
@Param('id') id,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') data_asset_id: string,
|
||||
@Body() body: IUpdateDataRequest,
|
||||
): Promise<IOneDataAsset> {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
@@ -388,9 +539,11 @@ export class CatalogController {
|
||||
language,
|
||||
});
|
||||
|
||||
delete (body as any).certification_status;
|
||||
|
||||
const result = await this.catalogService.updateOneDataAsset({
|
||||
body,
|
||||
data_asset_id: id,
|
||||
data_asset_id,
|
||||
customer_id,
|
||||
metadata,
|
||||
});
|
||||
@@ -401,36 +554,84 @@ export class CatalogController {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put('data-asset/:id/certification-status')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.CERTIFY,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async updateDataAssetCertificationStatus(
|
||||
@User() user: RequestUser,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') data_asset_id: string,
|
||||
@Body(new ValidationPipe()) body: IUpdateCertificationStatusRequest,
|
||||
): Promise<IUpdateCertificationStatusRequest> {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
language,
|
||||
});
|
||||
|
||||
return this.catalogService.updateCertificationStatus({
|
||||
body,
|
||||
data_asset_id,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('data-asset/:id/docs')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async manageDataAssetDocs(
|
||||
@User() user: RequestUser,
|
||||
@Headers() headers,
|
||||
@Param('id') table_id: string,
|
||||
@Body('docs') docs: string,
|
||||
@Query('asset_type') asset_type: string,
|
||||
) {
|
||||
const { user_id, customer_name } = user;
|
||||
const { user_id, customer_name, customer_id, username } = user;
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
|
||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
||||
this.logger.info(`/catalog - ON POST DATA DOCS ROUTE`, {
|
||||
user_id,
|
||||
customer_name,
|
||||
});
|
||||
|
||||
const res = await this.catalogService.createDataDocs({
|
||||
const body = {
|
||||
table_id,
|
||||
docs,
|
||||
asset_type,
|
||||
info: {
|
||||
customer: customer_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const res = await this.catalogService.createDataDocs(body, metadata);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Put('data-asset/:id/manage-permissions')
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async manageDataAssetPermissions(
|
||||
@Param('id') id: string,
|
||||
@User() user: RequestUser,
|
||||
@@ -451,7 +652,11 @@ export class CatalogController {
|
||||
return { data_asset: JSON.parse(response.data_asset) };
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Put('data-asset/:id/revoke-permissions')
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async revokeDataAssetPermissions(
|
||||
@Param('id') id: string,
|
||||
@User() user: RequestUser,
|
||||
@@ -477,6 +682,9 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.CREATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async createDataAsset(
|
||||
@User() user: RequestUser,
|
||||
@Body() body: ICreateDataAsset,
|
||||
@@ -501,6 +709,9 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async commentOnDataAsset(
|
||||
@Param('id') id: string,
|
||||
@User() user: RequestUser,
|
||||
@@ -527,6 +738,9 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DELETE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async deleteDataAsset(@Param('id') id: string, @User() user: RequestUser) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
@@ -548,6 +762,9 @@ export class CatalogController {
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async deleteComment(
|
||||
@Param('id') id: string,
|
||||
@User() user: RequestUser,
|
||||
@@ -561,10 +778,299 @@ export class CatalogController {
|
||||
username,
|
||||
});
|
||||
const response = await this.catalogService.deleteComment(
|
||||
{ data_asset_id: id, comment: body.comment },
|
||||
{
|
||||
data_asset_id: id,
|
||||
comment: { ...body.comment, created_at: undefined, message: undefined },
|
||||
},
|
||||
metadata,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('dataset-catalog-task')
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.TRIGGER_CATALOG_TASK,
|
||||
)
|
||||
@ApiCreatedResponse({ type: TriggerCatalogRes })
|
||||
async triggerCatalog(
|
||||
@User() user: RequestUser,
|
||||
@Body() body: TriggerCatalogReq,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const session = await this.catalogService.triggerCatalog(body, metadata);
|
||||
|
||||
return { session };
|
||||
}
|
||||
|
||||
@Get('dataset-catalog-task/:session')
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.TRIGGER_CATALOG_TASK,
|
||||
)
|
||||
@ApiOkResponse({ type: GetDatasetCatalogTaskRes })
|
||||
async getCatalogTask(
|
||||
@User() user: RequestUser,
|
||||
@Param('session') session: string,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const response = await this.catalogService.getDatasetCatalogTask(
|
||||
session,
|
||||
metadata,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('rls-rule')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
async addRlsRule(@User() user: RequestUser, @Body() body: AddRlsRuleRequest) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const response = await this.catalogService.addRlsRule(body, metadata);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('rls-rule/:id')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
async getOneRlsRule(@Param('id') id: string, @User() user: RequestUser) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const idInt = parseInt(id);
|
||||
const response = await this.catalogService.getOneRlsRule(idInt, metadata);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('rls-rule')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
async getRlsRules(
|
||||
@User() user: RequestUser,
|
||||
@Query() query: GetRlsRulesRequest,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const response = await this.catalogService.getRlsRules(query, metadata);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Delete('rls-rule/:id')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
async removeRlsRule(@Param('id') id: string, @User() user: RequestUser) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const idInt = parseInt(id);
|
||||
const response = await this.catalogService.removeRlsRule(idInt, metadata);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Delete('rls-rule')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
async batchRemoveRlsRules(
|
||||
@Query() query: BatchRemoveRlsRulesRequest,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
await this.catalogService.batchRemoveRlsRule(query, metadata);
|
||||
|
||||
return { message: 'OK' };
|
||||
}
|
||||
|
||||
@Get('nimbus-dashboards')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.CATALOG
|
||||
)
|
||||
async getNimbusDashboards(
|
||||
@User() user: RequestUser,
|
||||
@Body() body: GetNimbusDashboardsRequest,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
const dashboards = await this.catalogService.getNimbusDashboards(
|
||||
body,
|
||||
metadata,
|
||||
);
|
||||
|
||||
return JSON.parse(dashboards);
|
||||
}
|
||||
|
||||
@Post('register-dataset')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.CREATE,
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||
)
|
||||
async registerDatasetWithMetadataRequest(
|
||||
@Body() body: RegisterDatasetWithMetatadaRequest,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
const { customer_id, customer_name, user_id, username } = user;
|
||||
const logMetadata = {
|
||||
customer_name: customer_name,
|
||||
user_id: user_id,
|
||||
method: 'POST',
|
||||
path: '/catalog/register-dataset',
|
||||
};
|
||||
try {
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Request from user ${user_id} for customer ${customer_name}`,
|
||||
logMetadata,
|
||||
);
|
||||
|
||||
// Create table metadata
|
||||
const tableMetadataBody = {
|
||||
table_metadata: body.table_metadata,
|
||||
info: {
|
||||
customer: customer_name,
|
||||
},
|
||||
logMetadata: logMetadata,
|
||||
};
|
||||
|
||||
const table_metadata_id = await this.catalogService.createTableMetadata(
|
||||
tableMetadataBody,
|
||||
);
|
||||
this.logger.info(`table_metadata_id: ${table_metadata_id}`, logMetadata);
|
||||
|
||||
// Create column metadata
|
||||
const columnMetadataBody = {
|
||||
column_metadata: body.column_metadata,
|
||||
info: {
|
||||
customer: customer_name,
|
||||
},
|
||||
};
|
||||
this.logger.info(
|
||||
`Creating column metadata for table ${table_metadata_id}`,
|
||||
logMetadata,
|
||||
);
|
||||
const column_metadata_ids =
|
||||
await this.catalogService.createColumnMetadata(columnMetadataBody);
|
||||
|
||||
// Create data preview
|
||||
const dataPreviewBody = {
|
||||
data_preview: body.data_preview,
|
||||
info: {
|
||||
customer: customer_name,
|
||||
},
|
||||
};
|
||||
this.logger.debug(
|
||||
`Creating data preview for table ${table_metadata_id}`,
|
||||
logMetadata,
|
||||
);
|
||||
const data_preview_id = await this.catalogService.createDataPreview(
|
||||
dataPreviewBody,
|
||||
);
|
||||
|
||||
// Catalog dataset item
|
||||
this.logger.info(
|
||||
`Cataloging dataset item for table ${table_metadata_id}`,
|
||||
logMetadata,
|
||||
);
|
||||
await this.catalogService.catalogDatasetItem(table_metadata_id, metadata);
|
||||
this.logger.info(
|
||||
`Dataset registration completed successfully for table ${table_metadata_id}`,
|
||||
logMetadata,
|
||||
);
|
||||
return {
|
||||
message: 'Dataset registered successfully',
|
||||
table_metadata_id: table_metadata_id,
|
||||
column_metadata_ids: column_metadata_ids,
|
||||
data_preview_id: data_preview_id,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to register dataset. The following error occurred: ${error.response.data}`,
|
||||
logMetadata,
|
||||
);
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
message: 'Ocorreu um erro ao registrar o dataset',
|
||||
error: error.message,
|
||||
code: 'REGISTRATION_FAILED',
|
||||
details: error.message,
|
||||
},
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('pii-reporter')
|
||||
@RequireSomePermission(
|
||||
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.PII
|
||||
)
|
||||
async getPiiReporter(@User() user: RequestUser, @Res() res: Response, @Query('type') contentType: TypeParser = "pdf") {
|
||||
this.logger.info('GET pii-reporter');
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
try {
|
||||
const {
|
||||
file,
|
||||
filename,
|
||||
type
|
||||
} = await this.catalogService.getPiiReporter(metadata, contentType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Type', type);
|
||||
|
||||
// use res.end to send buffer
|
||||
return res.end(file);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,23 @@ import { Module } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import { CatalogController } from './catalog.controller';
|
||||
import { CatalogService } from './catalog.service';
|
||||
import { CatalogClientConfiguration } from './catalog-client';
|
||||
import { ClientsModule } from '@nestjs/microservices';
|
||||
import { PipelinesModule as OldPipelineModule } from 'src/modules/pipelines/pipelines.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { RolesModule } from '../roles/roles.module';
|
||||
import { CustomersModule } from '../customers/customers.module';
|
||||
import { ShareModule } from './share/share.module';
|
||||
import { CatalogService } from './catalog.service';
|
||||
|
||||
const client = new CatalogClientConfiguration();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClientsModule.register([client.providerOptions]),
|
||||
OldPipelineModule,
|
||||
UsersModule,
|
||||
RolesModule,
|
||||
CustomersModule,
|
||||
ShareModule,
|
||||
],
|
||||
controllers: [CatalogController],
|
||||
providers: [CatalogService, DadosferaLogger],
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
Messages,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
|
||||
import {
|
||||
Messages as PlatformInterfaceMessages,
|
||||
WriteService as PlatformInterfaceWriteService,
|
||||
ProtoServices as PlatformInterfacesProtoServices,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/PlatformInterfaces';
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
@@ -18,11 +24,28 @@ import { CatalogClientConfiguration } from './catalog-client';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { RolesService } from '../roles/roles.service';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { IUpdateDataRequest } from './dtos';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import {
|
||||
AssetReporter,
|
||||
BatchRemoveRlsRulesRequest,
|
||||
CreateDataDocsDTO,
|
||||
IUpdateCertificationStatusRequest,
|
||||
IUpdateDataRequest,
|
||||
TriggerCatalogReq,
|
||||
} from './dtos';
|
||||
import {
|
||||
AddRlsRuleRequest,
|
||||
GetNimbusDashboardsRequest,
|
||||
GetRlsRulesRequest,
|
||||
PiiMetadata,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||
import { TypeParser } from 'src/utils/FileParser/parser-types';
|
||||
import { ParserBuilder } from 'src/utils/FileParser/parser.builder';
|
||||
|
||||
class CatalogService implements OnModuleInit {
|
||||
catalogReadService: ReadService.CatalogReadServices;
|
||||
catalogWriteService: WriteService.CatalogWriteServices;
|
||||
platformWriteService: PlatformInterfaceWriteService.PlatformInterfacesWriteServices;
|
||||
logger: any;
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
@@ -44,9 +67,14 @@ class CatalogService implements OnModuleInit {
|
||||
this.grpcClient.getService<WriteService.CatalogWriteServices>(
|
||||
ProtoServices.CatalogWriteServices,
|
||||
);
|
||||
this.platformWriteService =
|
||||
this.grpcClient.getService<PlatformInterfaceWriteService.PlatformInterfacesWriteServices>(
|
||||
PlatformInterfacesProtoServices.PlatformInterfacesWriteServices,
|
||||
);
|
||||
}
|
||||
|
||||
_getNimbusUrl(body) {
|
||||
this.logger.debug(`Body: ${JSON.stringify(body)}`);
|
||||
const customer = body.info.customer.toLowerCase();
|
||||
|
||||
if (process.env.ENV === 'prd') {
|
||||
@@ -59,6 +87,43 @@ class CatalogService implements OnModuleInit {
|
||||
)}.dadosfera.ai`;
|
||||
}
|
||||
|
||||
async getPiiReporter(metadata: Metadata, type: TypeParser) {
|
||||
this.logger.info('getPiiReporter: ' + type);
|
||||
try {
|
||||
const { data } = await lastValueFrom(
|
||||
this.catalogWriteService.GetPiiReporter({}, metadata),
|
||||
);
|
||||
this.logger.info('Finish grpc call');
|
||||
|
||||
const parser = ParserBuilder.build<PiiMetadata>(type);
|
||||
|
||||
this.logger.info('parser file to: ' + type);
|
||||
const file = await parser.parse(data);
|
||||
this.logger.info('finish parser');
|
||||
const mimeTypes: Record<TypeParser, string> = {
|
||||
csv: 'text/csv',
|
||||
html: 'text/html',
|
||||
pdf: 'application/pdf',
|
||||
};
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `relatorio-pii-${timestamp}.${type}`;
|
||||
|
||||
return {
|
||||
file,
|
||||
filename: filename,
|
||||
type: mimeTypes[type],
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getCustomPropertyDefinitions(metadata: Metadata) {
|
||||
return lastValueFrom(this.catalogReadService.GetCustomPropertyDefinitions({}, metadata));
|
||||
}
|
||||
|
||||
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
if (!data.embed) data.embed = undefined;
|
||||
@@ -136,9 +201,11 @@ class CatalogService implements OnModuleInit {
|
||||
async getUserRolesIds(userId: string) {
|
||||
const result = await this.userService.findOneById(userId).catch(() => null);
|
||||
|
||||
const roles_ids = result.user.roles.map((role) => role.id);
|
||||
if (result) {
|
||||
return result.user.roles.map((role) => role.id);
|
||||
}
|
||||
|
||||
return roles_ids;
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchDataAssets(
|
||||
@@ -146,10 +213,81 @@ class CatalogService implements OnModuleInit {
|
||||
metadata: Metadata,
|
||||
customer_id: string,
|
||||
) {
|
||||
this.logger.info('CatalogService - searchDataAssets');
|
||||
this.logger.info('CatalogService - searchDataAssets', { query });
|
||||
|
||||
const { search, page, size, sort_by, order, ...filters } = query;
|
||||
|
||||
this.logger.debug('Extracted filters:', { filters });
|
||||
|
||||
if (
|
||||
filters.manually !== undefined &&
|
||||
filters.manually !== null &&
|
||||
filters.manually !== ''
|
||||
) {
|
||||
filters.manually = Number(filters.manually); // 1 ou 0
|
||||
} else {
|
||||
delete filters.manually;
|
||||
}
|
||||
|
||||
if (filters.owner) {
|
||||
const { users: customer_users } =
|
||||
await this.userService.findAllUsersByCustomerId(customer_id);
|
||||
|
||||
this.logger.info('Available users in database count:', {
|
||||
count: customer_users.length,
|
||||
});
|
||||
this.logger.info('First 5 users:', {
|
||||
users: customer_users
|
||||
.slice(0, 5)
|
||||
.map((u) => ({ id: u.id, email: u.email, name: u.name })),
|
||||
});
|
||||
|
||||
const ownerValues = Array.isArray(filters.owner)
|
||||
? filters.owner
|
||||
: typeof filters.owner === 'string' && filters.owner.includes(',')
|
||||
? filters.owner.split(',').map((o: string) => o.trim())
|
||||
: [filters.owner];
|
||||
|
||||
this.logger.info('Owner values to convert:', {
|
||||
ownerValues,
|
||||
ownerFiltersOriginal: filters.owner,
|
||||
});
|
||||
|
||||
const ownerIds = ownerValues
|
||||
.map((ownerValue: string) => {
|
||||
const normalizedOwner = ownerValue.replace(/\s/g, '+');
|
||||
const user = customer_users.find((u) => {
|
||||
const isIdMatch = u.id === ownerValue;
|
||||
const isEmailMatch =
|
||||
u.email === ownerValue || u.email === normalizedOwner;
|
||||
const isNameMatch =
|
||||
u.name === ownerValue || u.name === normalizedOwner;
|
||||
this.logger.info('Comparing:', {
|
||||
userId: u.id,
|
||||
userEmail: u.email,
|
||||
userName: u.name,
|
||||
filterValue: ownerValue,
|
||||
normalizedFilter: normalizedOwner,
|
||||
idMatch: isIdMatch,
|
||||
emailMatch: isEmailMatch,
|
||||
nameMatch: isNameMatch,
|
||||
});
|
||||
return isIdMatch || isEmailMatch || isNameMatch;
|
||||
});
|
||||
this.logger.info('Looking for owner result:', {
|
||||
ownerValue,
|
||||
found: !!user,
|
||||
userId: user?.id,
|
||||
});
|
||||
return user?.id || ownerValue;
|
||||
})
|
||||
.filter((id: string) => id);
|
||||
|
||||
if (ownerIds.length > 0) {
|
||||
filters.owner = ownerIds;
|
||||
}
|
||||
}
|
||||
|
||||
const { data_assets, total } = await lastValueFrom(
|
||||
this.catalogReadService.GetAllDataAssets(
|
||||
{
|
||||
@@ -164,6 +302,8 @@ class CatalogService implements OnModuleInit {
|
||||
),
|
||||
);
|
||||
|
||||
console.log('MAESTRO RECEBEU RESPOSTA DO PI-FACTORY');
|
||||
|
||||
const result = JSON.parse(data_assets);
|
||||
|
||||
const response = await this.getAssetsUsersAndRoles(
|
||||
@@ -174,6 +314,34 @@ class CatalogService implements OnModuleInit {
|
||||
return { data_assets: response, total };
|
||||
}
|
||||
|
||||
async downloadAssets(
|
||||
query: Record<string, any>,
|
||||
metadata: Metadata,
|
||||
customer_id: string,
|
||||
) {
|
||||
const data = await this.searchDataAssets(query, metadata, customer_id);
|
||||
|
||||
const formatData = data.data_assets.map((asset) => ({
|
||||
id: asset.id,
|
||||
display_name: asset.display_name,
|
||||
data_asset_type: asset.data_asset_type,
|
||||
created_at: asset.created_at,
|
||||
tags: '[' + asset.tags.join(', ') + ']',
|
||||
}));
|
||||
|
||||
const parser = ParserBuilder.build<AssetReporter>('csv');
|
||||
|
||||
const file = await parser.parse(formatData);
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `dadosfera_assets_${timestamp}.csv`;
|
||||
|
||||
return {
|
||||
file,
|
||||
filename,
|
||||
};
|
||||
}
|
||||
|
||||
async getOneDataAsset(data: {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
@@ -221,6 +389,28 @@ class CatalogService implements OnModuleInit {
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
async updateCertificationStatus(data: {
|
||||
data_asset_id: string;
|
||||
body: IUpdateCertificationStatusRequest;
|
||||
metadata: Metadata;
|
||||
}) {
|
||||
const { body, data_asset_id, metadata } = data;
|
||||
|
||||
await lastValueFrom(
|
||||
this.catalogWriteService.UpdateDataAsset(
|
||||
{
|
||||
id: data_asset_id,
|
||||
changes: JSON.stringify({
|
||||
certification_status: body.certification_status,
|
||||
}),
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
|
||||
return { certification_status: body.certification_status };
|
||||
}
|
||||
|
||||
async updateOneDataAsset(data: {
|
||||
data_asset_id: string;
|
||||
customer_id: string;
|
||||
@@ -246,11 +436,11 @@ class CatalogService implements OnModuleInit {
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
async getDataDocs(id: string, metadata: Metadata) {
|
||||
async getDataDocs(id: string, assetType: string, metadata: Metadata) {
|
||||
const { documentation } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
||||
this.catalogReadService.GetDatasetDoc({ id }, metadata),
|
||||
);
|
||||
console.log(documentation);
|
||||
|
||||
const docs = JSON.parse(documentation);
|
||||
return docs;
|
||||
}
|
||||
@@ -277,7 +467,16 @@ class CatalogService implements OnModuleInit {
|
||||
return result;
|
||||
}
|
||||
|
||||
async createDataDocs(body) {
|
||||
async createDataDocs(body: CreateDataDocsDTO, metadata: Metadata) {
|
||||
if (body.asset_type === 'table' || body.asset_type === 'view') {
|
||||
return this.createDataDocsViaNimbus(body);
|
||||
}
|
||||
|
||||
return this.createDataDocsViaGrpc(body, metadata);
|
||||
}
|
||||
|
||||
private async createDataDocsViaNimbus(body: CreateDataDocsDTO) {
|
||||
this.logger.info('Creating data docs via Nimbus for table/view');
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
const { data } = await axios.post(
|
||||
`${nimbusUrl}/api/catalog/data-docs/`,
|
||||
@@ -286,6 +485,29 @@ class CatalogService implements OnModuleInit {
|
||||
return data;
|
||||
}
|
||||
|
||||
private async createDataDocsViaGrpc(body: CreateDataDocsDTO, metadata: Metadata) {
|
||||
this.logger.info('Creating data docs via gRPC for other asset types');
|
||||
try {
|
||||
const response: any = await lastValueFrom(
|
||||
this.catalogWriteService.UpdateDataAssetDoc(
|
||||
{
|
||||
id: body.table_id,
|
||||
docs: body.docs,
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.logger.error('Error creating data asset docs:', error);
|
||||
throw new HttpException(
|
||||
'Failed to create data asset documentation',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findAllTags(data, metadata) {
|
||||
this.logger.info('CatalogService - findAllCustomerTags');
|
||||
|
||||
@@ -303,6 +525,23 @@ class CatalogService implements OnModuleInit {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async findSchemas(metadata: Metadata) {
|
||||
this.logger.info('CatalogService - findSchemas');
|
||||
|
||||
try {
|
||||
const response = await lastValueFrom(
|
||||
this.catalogReadService.GetSchemas({}, metadata),
|
||||
);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Error fetching schemas:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAssetsUsersAndRoles(data_assets: Array<any>, customer_id: string) {
|
||||
const { users: customer_users } =
|
||||
await this.userService.findAllUsersByCustomerId(customer_id);
|
||||
@@ -313,17 +552,20 @@ class CatalogService implements OnModuleInit {
|
||||
return data_assets.map((data_asset) => {
|
||||
const owner = customer_users.find(
|
||||
(u) => u.id === data_asset.owner,
|
||||
)?.username;
|
||||
)?.email;
|
||||
|
||||
const roles = [];
|
||||
const users = [];
|
||||
for (const role_id of data_asset.roles) {
|
||||
const data_asset_roles = data_asset?.roles || [];
|
||||
for (const role_id of data_asset_roles) {
|
||||
const role = customer_roles.find((r) => r.id === role_id);
|
||||
if (role) roles.push({ id: role.id, name: role.name });
|
||||
}
|
||||
for (const user_id of data_asset.users) {
|
||||
|
||||
const data_asset_users = data_asset?.users || [];
|
||||
for (const user_id of data_asset_users) {
|
||||
const user = customer_users.find((r) => r.id === user_id);
|
||||
if (user) users.push({ id: user.id, username: user.username });
|
||||
if (user) users.push({ id: user.id, email: user.email });
|
||||
}
|
||||
return {
|
||||
...data_asset,
|
||||
@@ -333,6 +575,271 @@ class CatalogService implements OnModuleInit {
|
||||
} as typeof data_asset;
|
||||
});
|
||||
}
|
||||
|
||||
async triggerCatalog(data: TriggerCatalogReq, metadata: Metadata) {
|
||||
const { session } = await lastValueFrom(
|
||||
this.catalogWriteService.TriggerDatasetCataloging(data, metadata),
|
||||
);
|
||||
return session;
|
||||
}
|
||||
async getDatasetCatalogTask(session: string, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetCatalogTask({ session }, metadata),
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async addRlsRule(data: AddRlsRuleRequest, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogWriteService.AddRlsRule(data, metadata),
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async removeRlsRule(id: number, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogWriteService.RemoveRlsRule({ id }, metadata),
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async batchRemoveRlsRule(
|
||||
query: BatchRemoveRlsRulesRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
const { id_rls, nimbus_dashboard_id } = query;
|
||||
|
||||
if (id_rls && nimbus_dashboard_id) {
|
||||
throw new BadRequestException(
|
||||
"You can't delete using both parameters. Choose either 'id_rls' or 'nimbus_dashboard_id'",
|
||||
);
|
||||
}
|
||||
if (id_rls) {
|
||||
await lastValueFrom(
|
||||
this.catalogWriteService.RemoveRlsRulesByRlsId({ id_rls }, metadata),
|
||||
);
|
||||
} else if (nimbus_dashboard_id) {
|
||||
await lastValueFrom(
|
||||
this.catalogWriteService.RemoveRlsRulesByDashboardId(
|
||||
{ nimbus_dashboard_id: parseInt(nimbus_dashboard_id) },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
async getRlsRules(data: GetRlsRulesRequest, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetRlsRules(data, metadata),
|
||||
);
|
||||
return res.rls_rules;
|
||||
}
|
||||
|
||||
async getOneRlsRule(id: number, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetOneRlsRule({ id }, metadata),
|
||||
);
|
||||
return res.rls_rule;
|
||||
}
|
||||
|
||||
async getNimbusDashboards(
|
||||
data: GetNimbusDashboardsRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetNimbusDashboards(data, metadata),
|
||||
);
|
||||
return res.dashboards;
|
||||
}
|
||||
|
||||
async createTableMetadata(body: any): Promise<number> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, { ...body.logMetadata });
|
||||
|
||||
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/`;
|
||||
|
||||
this.logger.info(
|
||||
`Creating table metadata for table ${body.table_metadata.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, { ...body.logMetadata });
|
||||
this.logger.debug(`Payload: ${JSON.stringify(body.table_metadata)}`, {
|
||||
...body.logMetadata,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data, status } = await axios.post(endpoint, {
|
||||
...body.table_metadata,
|
||||
});
|
||||
|
||||
this.logger.info(
|
||||
`Table metadata created successfully with status ${status} for table ${body.table_metadata.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
return data.id;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create table metadata for table ${body.table_metadata.table_name} failed with status ${
|
||||
error.response?.status
|
||||
} because of ${JSON.stringify(error.response?.data) || error.message}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
throw new Error(error.response?.data?.message || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async createColumnMetadata(body: any): Promise<number[]> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, body.logMetadata);
|
||||
const endpoint = `${nimbusUrl}/api/catalog/column-metadata/`;
|
||||
|
||||
try {
|
||||
this.logger.info(
|
||||
`Creating column metadata for table ${body.column_metadata.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, { ...body.logMetadata });
|
||||
this.logger.debug(
|
||||
`Payload: ${JSON.stringify(body.column_metadata)}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
const { data, status } = await axios.post(
|
||||
endpoint,
|
||||
body.column_metadata,
|
||||
);
|
||||
|
||||
this.logger.info(
|
||||
`Column metadata created successfully with status ${status} for table ${body.column_metadata.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
return data.map((column) => column.id);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create column metadata failed with status for table ${body.column_metadata.table_name} ${
|
||||
error.response?.status
|
||||
} because of ${error.response?.data || error.message}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
throw new Error(error.response?.data?.message || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async createDataPreview(body: any): Promise<number> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, { ...body.logMetadata });
|
||||
const endpoint = `${nimbusUrl}/api/catalog/data-preview/`;
|
||||
|
||||
this.logger.info(
|
||||
`Creating data preview for table ${body.data_preview.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, { ...body.logMetadata });
|
||||
this.logger.debug(
|
||||
`Payload: ${JSON.stringify(body.data_preview)}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
|
||||
try {
|
||||
const { data, status } = await axios.post(endpoint, body.data_preview);
|
||||
|
||||
this.logger.info(
|
||||
`Data preview created successfully with status ${status} for table ${body.data_preview.table_name}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
return data.id;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create data preview for table ${body.data_preview.table_name} failed with status ${
|
||||
error.response?.status
|
||||
} because of ${error.response?.data || error.message}`,
|
||||
{ ...body.logMetadata },
|
||||
);
|
||||
throw new Error(error.response?.data?.message || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async renameTableOnNimbus(
|
||||
nimbusUrl: string,
|
||||
nimbusId: number,
|
||||
changes: { table_name?: string; table_schema?: string; display_name?: string },
|
||||
): Promise<void> {
|
||||
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/${nimbusId}`;
|
||||
this.logger.info(`Renaming table-metadata ${nimbusId} on Nimbus`, { endpoint, changes });
|
||||
await axios.patch(endpoint, changes);
|
||||
}
|
||||
|
||||
async renameColumnMetadataOnNimbus(
|
||||
nimbusUrl: string,
|
||||
databaseName: string,
|
||||
oldTableName: string,
|
||||
oldTableSchema: string,
|
||||
newTableName: string,
|
||||
newTableSchema: string,
|
||||
): Promise<void> {
|
||||
const listEndpoint = `${nimbusUrl}/api/catalog/column-metadata/?database_name=${encodeURIComponent(databaseName)}&table_name=${encodeURIComponent(oldTableName)}&table_schema=${encodeURIComponent(oldTableSchema)}`;
|
||||
this.logger.info(`Fetching column-metadata records to rename`, { listEndpoint });
|
||||
const { data: columns } = await axios.get(listEndpoint);
|
||||
|
||||
const filtered = Array.isArray(columns) ? columns : [];
|
||||
|
||||
for (const column of filtered) {
|
||||
const patchEndpoint = `${nimbusUrl}/api/catalog/column-metadata/${column.id}`;
|
||||
await axios.patch(patchEndpoint, {
|
||||
table_name: newTableName,
|
||||
table_schema: newTableSchema,
|
||||
});
|
||||
}
|
||||
this.logger.info(`Renamed ${filtered.length} column-metadata records on Nimbus`);
|
||||
}
|
||||
|
||||
async renameDataPreviewOnNimbus(
|
||||
nimbusUrl: string,
|
||||
databaseName: string,
|
||||
oldTableName: string,
|
||||
oldTableSchema: string,
|
||||
newTableName: string,
|
||||
newTableSchema: string,
|
||||
): Promise<void> {
|
||||
const listEndpoint = `${nimbusUrl}/api/catalog/data-preview/?database_name=${encodeURIComponent(databaseName)}&table_name=${encodeURIComponent(oldTableName)}&table_schema=${encodeURIComponent(oldTableSchema)}`;
|
||||
this.logger.info(`Fetching data-preview records to rename`, { listEndpoint });
|
||||
const { data: previews } = await axios.get(listEndpoint);
|
||||
|
||||
const filtered = Array.isArray(previews) ? previews : [];
|
||||
|
||||
for (const preview of filtered) {
|
||||
const patchEndpoint = `${nimbusUrl}/api/catalog/data-preview/${preview.id}`;
|
||||
await axios.patch(patchEndpoint, {
|
||||
table_name: newTableName,
|
||||
table_schema: newTableSchema,
|
||||
});
|
||||
}
|
||||
this.logger.info(`Renamed ${filtered.length} data-preview records on Nimbus`);
|
||||
}
|
||||
|
||||
async catalogDatasetItem(table_metadata_id: number, metadata: Metadata) {
|
||||
const customer_name_raw = metadata.get('customer_name');
|
||||
|
||||
const customer_name = customer_name_raw?.[0]?.toString();
|
||||
if (!customer_name) {
|
||||
throw new BadRequestException('Customer name not found in metadata');
|
||||
}
|
||||
const res = await lastValueFrom(
|
||||
this.platformWriteService.CatalogDataAssets(
|
||||
{
|
||||
data_assets: [
|
||||
{
|
||||
data_asset_id: table_metadata_id.toString(),
|
||||
customer_name: customer_name,
|
||||
data_asset_type: 'dataset',
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export { CatalogService };
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional, PickType } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { CreateDataAssetRequest } from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||
|
||||
export enum DataAssetShareType {
|
||||
none = 'none',
|
||||
public = 'public',
|
||||
private = 'private',
|
||||
}
|
||||
export enum CertificationStatus {
|
||||
draft = 'draft',
|
||||
in_review = 'in_review',
|
||||
approved = 'approved',
|
||||
deprecated = 'deprecated',
|
||||
}
|
||||
export enum OrderEnum {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
export class EmbedObject {
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
@@ -87,6 +108,10 @@ export class IDataAsset {
|
||||
comments: IComment[];
|
||||
@ApiPropertyOptional()
|
||||
embed?: EmbedObject;
|
||||
@ApiPropertyOptional({ enum: DataAssetShareType })
|
||||
share_type?: DataAssetShareType;
|
||||
@ApiPropertyOptional()
|
||||
docs?: string;
|
||||
}
|
||||
|
||||
export class IOneDataAsset {
|
||||
@@ -99,9 +124,61 @@ export class IMakeAComment {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class CommentToDelete extends PickType(IComment, ['id', 'username']) {}
|
||||
export class IDeleteComment {
|
||||
@ApiProperty()
|
||||
comment: IComment;
|
||||
comment: CommentToDelete;
|
||||
}
|
||||
|
||||
export class ICatalogAllRequest {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Texto usado para filtrar os ativos no catálogo.',
|
||||
})
|
||||
search;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Número de registros a ser retornado',
|
||||
maximum: 10000,
|
||||
minimum: 1,
|
||||
})
|
||||
size: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Usado para paginação, em conjunto com `size`',
|
||||
minimum: 1,
|
||||
})
|
||||
page: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Campo do data asset para usar na ordenação. Padrão: `created_at` ',
|
||||
})
|
||||
sort_by: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: OrderEnum,
|
||||
default: OrderEnum.asc,
|
||||
description: 'Tipo de ordenação - `asc`: crescente; `desc`: decrescente ',
|
||||
})
|
||||
order?: OrderEnum;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'ID do usuário owner para filtrar data assets',
|
||||
example: 'user-id-1,user-id-2',
|
||||
})
|
||||
owner?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Data inicial para filtro de catálogo (formato: YYYY-MM-DD)',
|
||||
example: '2025-01-01',
|
||||
})
|
||||
catalog_date_from?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Data final para filtro de catálogo (formato: YYYY-MM-DD)',
|
||||
example: '2025-12-31',
|
||||
})
|
||||
catalog_date_to?: string;
|
||||
}
|
||||
|
||||
export class ICatalogAllResponse {
|
||||
@@ -126,6 +203,27 @@ export class IData {
|
||||
day_opening: number;
|
||||
}
|
||||
|
||||
|
||||
export enum CustomPropertyType {
|
||||
TEXT = 'text',
|
||||
NUMBER = 'number',
|
||||
DATE = 'date',
|
||||
BOOLEAN = 'boolean',
|
||||
}
|
||||
|
||||
export class CustomPropertyDto {
|
||||
@ApiProperty()
|
||||
key: string;
|
||||
@ApiProperty()
|
||||
value: string;
|
||||
@ApiProperty({ enum: CustomPropertyType })
|
||||
type: CustomPropertyType;
|
||||
@ApiPropertyOptional()
|
||||
color?: string;
|
||||
@ApiPropertyOptional()
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export class IUpdateDataRequest {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
@@ -135,7 +233,20 @@ export class IUpdateDataRequest {
|
||||
tags: string[];
|
||||
@ApiPropertyOptional()
|
||||
embed: EmbedObject;
|
||||
@ApiPropertyOptional({ enum: DataAssetShareType })
|
||||
share_type?: DataAssetShareType;
|
||||
@ApiPropertyOptional()
|
||||
docs?: string;
|
||||
@ApiPropertyOptional({ type: [CustomPropertyDto] })
|
||||
custom_properties?: CustomPropertyDto[];
|
||||
}
|
||||
|
||||
export class IUpdateCertificationStatusRequest {
|
||||
@ApiProperty({ enum: CertificationStatus })
|
||||
@IsEnum(CertificationStatus)
|
||||
certification_status: CertificationStatus;
|
||||
}
|
||||
|
||||
export class ICreateDataAsset implements CreateDataAssetRequest {
|
||||
@ApiProperty()
|
||||
display_name: string;
|
||||
@@ -149,6 +260,8 @@ export class ICreateDataAsset implements CreateDataAssetRequest {
|
||||
location: string;
|
||||
@ApiPropertyOptional()
|
||||
embed: EmbedObject;
|
||||
@ApiPropertyOptional()
|
||||
docs: string;
|
||||
}
|
||||
|
||||
export class IPreview {
|
||||
@@ -224,3 +337,67 @@ export class IColumnsMetadataResponse {
|
||||
@ApiProperty({ type: [IColumnMetadata] })
|
||||
columns_metadata: IColumnMetadata[];
|
||||
}
|
||||
|
||||
export class TriggerCatalogReq {
|
||||
@ApiProperty({
|
||||
description: 'Name of the table on Snowflake',
|
||||
example: 'TB__4DXSD4_TEST',
|
||||
})
|
||||
table_name: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Schema where the asset is located. If not set defaults to "PUBLIC"',
|
||||
})
|
||||
table_schema?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Id of the pipeline that generated the asset.',
|
||||
})
|
||||
pipeline_id: string;
|
||||
}
|
||||
|
||||
export class TriggerCatalogRes {
|
||||
@ApiProperty()
|
||||
session: string;
|
||||
}
|
||||
|
||||
export class GetDatasetCatalogTaskRes {
|
||||
@ApiProperty()
|
||||
created_at: string;
|
||||
@ApiProperty()
|
||||
customer_name: string;
|
||||
@ApiProperty()
|
||||
session_id: string;
|
||||
@ApiProperty()
|
||||
status: string;
|
||||
@ApiProperty()
|
||||
table_name: string;
|
||||
@ApiProperty()
|
||||
updated_at: string;
|
||||
@ApiProperty()
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
export class BatchRemoveRlsRulesRequest {
|
||||
@ApiPropertyOptional()
|
||||
nimbus_dashboard_id?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
id_rls?: string;
|
||||
}
|
||||
|
||||
export type AssetReporter = {
|
||||
id: string;
|
||||
display_name: string;
|
||||
data_asset_type: string;
|
||||
created_at: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export type CreateDataDocsDTO = {
|
||||
table_id: string;
|
||||
docs: string;
|
||||
asset_type: string;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export class PiiDto {
|
||||
database_name: string;
|
||||
table_schema: string;
|
||||
table_name: string;
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
pii_rules: string;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Req,
|
||||
UseFilters,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiHeaders,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import {
|
||||
IColumnsMetadataResponse,
|
||||
IDocsResponse,
|
||||
IPreviewResponse,
|
||||
|
||||
} from '../dtos';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { Language } from 'src/decorators/language.decorator';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { ShareService } from './share.service';
|
||||
import { Request } from 'express';
|
||||
|
||||
@ApiTags('Catalog')
|
||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||
@Controller('catalog/data-asset/share')
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
export class ShareController {
|
||||
logger: DadosferaLogger;
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private catalogShareService: ShareService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
async getShareDataAsset(
|
||||
@Param('id') id: string,
|
||||
@Req() request: Request
|
||||
) {
|
||||
this.logger.info(`GET //:id`);
|
||||
return await this.catalogShareService.getOneDataAssetPublic(id, request);
|
||||
}
|
||||
|
||||
@Get('/:id/columns-metadata')
|
||||
async getShareDataAssetColumnsMetadata(
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Req() request: Request
|
||||
): Promise<IColumnsMetadataResponse> {
|
||||
this.logger.info(`GET /:id/columns-metadata`);
|
||||
|
||||
const columns_metadata =
|
||||
await this.catalogShareService.getDatasetColumnsMetadata(id, request);
|
||||
|
||||
|
||||
return { columns_metadata };
|
||||
}
|
||||
|
||||
@Get('/:id/preview')
|
||||
async getShareDataAssetPreview(
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Req() request: Request
|
||||
): Promise<IPreviewResponse> {
|
||||
this.logger.info(`GET /:id/preview`);
|
||||
const preview = await this.catalogShareService.getDatasetPreview(id, request);
|
||||
|
||||
return { preview };
|
||||
}
|
||||
|
||||
@Get('/:id/docs')
|
||||
async getShareDataAssetDocs(
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Req() request: Request
|
||||
): Promise<IDocsResponse> {
|
||||
this.logger.info(`GET /:id/docs`);
|
||||
const docs = await this.catalogShareService.getDataDocs(id, request);
|
||||
|
||||
return { docs };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { CatalogClientConfiguration } from "../catalog-client";
|
||||
import { ClientsModule } from "@nestjs/microservices";
|
||||
import { RolesModule } from "src/modules/roles/roles.module";
|
||||
import { UsersModule } from "src/modules/users/users.module";
|
||||
import { CustomersModule } from "src/modules/customers/customers.module";
|
||||
import { ShareMetadataModule } from "src/modules/share-metadata/share-metadata.module";
|
||||
import { ShareController } from "./share.controller";
|
||||
import DadosferaLogger from "@dadosfera/dadosfera-logs";
|
||||
import { ShareService } from "./share.service";
|
||||
import { MixpanelModule } from "src/modules/mixpanel/mixpanel.module";
|
||||
import { AuthModule } from "src/modules/auth/auth.module";
|
||||
|
||||
const client = new CatalogClientConfiguration();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClientsModule.register([client.providerOptions]),
|
||||
UsersModule,
|
||||
RolesModule,
|
||||
CustomersModule,
|
||||
ShareMetadataModule,
|
||||
MixpanelModule,
|
||||
AuthModule
|
||||
],
|
||||
controllers: [ShareController],
|
||||
providers: [ShareService, DadosferaLogger],
|
||||
exports: [ShareModule],
|
||||
})
|
||||
export class ShareModule {}
|
||||
@@ -0,0 +1,276 @@
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
import {
|
||||
ProtoServices,
|
||||
ReadService,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
|
||||
import {
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { CatalogClientConfiguration } from '../catalog-client';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { UsersService } from 'src/modules/users/users.service';
|
||||
import { RolesService } from 'src/modules/roles/roles.service';
|
||||
import { RequestUser } from 'src/decorators/user.decorator';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { ShareMetadataService } from 'src/modules/share-metadata/share-metadata.service';
|
||||
import { isJWT } from 'class-validator';
|
||||
import { MixpanelService } from 'src/modules/mixpanel/mixpanel.service';
|
||||
import { Request } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AuthClientService } from 'src/modules/auth/auth.service';
|
||||
|
||||
|
||||
export class ShareService implements OnModuleInit {
|
||||
catalogReadService: ReadService.CatalogReadServices;
|
||||
logger: DadosferaLogger;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
@Inject(CatalogClientConfiguration.name)
|
||||
private readonly grpcClient: ClientGrpc,
|
||||
private readonly userService: UsersService,
|
||||
private readonly roleService: RolesService,
|
||||
private readonly shareMetadataService: ShareMetadataService,
|
||||
private readonly mixpanelService: MixpanelService,
|
||||
private authClient: AuthClientService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.catalogReadService =
|
||||
this.grpcClient.getService<ReadService.CatalogReadServices>(
|
||||
ProtoServices.CatalogReadServices,
|
||||
);
|
||||
}
|
||||
|
||||
async getDatasetColumnsMetadata(id: string, request: Request) {
|
||||
const shareMetadata = await this.getShareMetadata(id, request);
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: shareMetadata.customerId,
|
||||
customer_name: shareMetadata.customerName,
|
||||
});
|
||||
const { columns_metadata } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetColumnsMetadata(
|
||||
{ id: shareMetadata.assetId, type: undefined },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
const result = JSON.parse(columns_metadata);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getDatasetPreview(id: string, request: Request) {
|
||||
const shareMetadata = await this.getShareMetadata(id, request);
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: shareMetadata.customerId,
|
||||
customer_name: shareMetadata.customerName,
|
||||
});
|
||||
const { preview } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetPreview(
|
||||
{ id: shareMetadata.assetId, type: undefined },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
const result = JSON.parse(preview);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getOneDataAssetPublic(id: string, request: Request) {
|
||||
this.logger.info("getOneDataAssetPublic: " + JSON.stringify({
|
||||
id
|
||||
}))
|
||||
try {
|
||||
const user = await this.getUserFromRequest(request);
|
||||
|
||||
const shareMetadata = await this.getShareMetadata(id, request);
|
||||
|
||||
const mixpanelTracker = {
|
||||
asset: shareMetadata.assetId,
|
||||
type: isJWT(id) ? 'assigned' : shareMetadata.type,
|
||||
customer: shareMetadata.customerName
|
||||
}
|
||||
|
||||
if (user) {
|
||||
await this.mixpanelService.track("share_page", user, request, mixpanelTracker);
|
||||
} else {
|
||||
await this.mixpanelService.trackShare(request, mixpanelTracker);
|
||||
}
|
||||
|
||||
this.logger.info("shareMetadata: " + JSON.stringify(shareMetadata))
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: shareMetadata.customerId,
|
||||
customer_name: shareMetadata.customerName,
|
||||
});
|
||||
|
||||
const { data_asset } = await this.getOneDataAsset({
|
||||
customer_id: shareMetadata.customerId,
|
||||
id: shareMetadata.assetId,
|
||||
metadata,
|
||||
});
|
||||
this.logger.info('found asset: ' + JSON.stringify(data_asset));
|
||||
delete data_asset.p_roles;
|
||||
delete data_asset.p_users;
|
||||
data_asset.share_type = 'public';
|
||||
if (data_asset.share_type !== 'public') throw new NotFoundException();
|
||||
|
||||
return { data_asset };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getOneDataAsset(data: {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
metadata: Metadata;
|
||||
}) {
|
||||
const { customer_id, id, metadata } = data;
|
||||
const { data_asset } = await lastValueFrom(
|
||||
this.catalogReadService.GetOneDataAsset(
|
||||
{ id, type: undefined },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
let asset = JSON.parse(data_asset);
|
||||
asset = {
|
||||
...asset,
|
||||
p_roles: asset.roles,
|
||||
p_users: asset.users,
|
||||
};
|
||||
asset = await this.getAssetsUsersAndRoles([asset], customer_id);
|
||||
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
async getDataDocs(id: string, request: Request) {
|
||||
const shareMetadata = await this.getShareMetadata(id, request);
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: shareMetadata.customerId,
|
||||
customer_name: shareMetadata.customerName,
|
||||
});
|
||||
|
||||
const { documentation } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetDoc({ id }, metadata),
|
||||
);
|
||||
console.log(documentation);
|
||||
const docs = JSON.parse(documentation);
|
||||
return docs;
|
||||
}
|
||||
|
||||
private async getAssetsUsersAndRoles(
|
||||
data_assets: Array<any>,
|
||||
customer_id: string,
|
||||
) {
|
||||
const { users: customer_users } =
|
||||
await this.userService.findAllUsersByCustomerId(customer_id);
|
||||
const { roles: customer_roles } = await this.roleService.roleSearch(
|
||||
{},
|
||||
{ customer_id },
|
||||
);
|
||||
return data_assets.map((data_asset) => {
|
||||
const owner = customer_users.find(
|
||||
(u) => u.id === data_asset.owner,
|
||||
)?.email;
|
||||
|
||||
const roles = [];
|
||||
const users = [];
|
||||
for (const role_id of data_asset.roles) {
|
||||
const role = customer_roles.find((r) => r.id === role_id);
|
||||
if (role) roles.push({ id: role.id, name: role.name });
|
||||
}
|
||||
for (const user_id of data_asset.users) {
|
||||
const user = customer_users.find((r) => r.id === user_id);
|
||||
if (user) users.push({ id: user.id, email: user.email });
|
||||
}
|
||||
return {
|
||||
...data_asset,
|
||||
roles,
|
||||
users,
|
||||
owner,
|
||||
} as typeof data_asset;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private async getShareMetadata(id: string, request: Request) {
|
||||
const metadata = PackTheMetadata({});
|
||||
this.logger.info('GET share metadata')
|
||||
const info = await this.shareMetadataService.get(id, metadata);
|
||||
if (isJWT(id) && info ){
|
||||
return info;
|
||||
}
|
||||
|
||||
const user = await this.getUserFromRequest(request);
|
||||
|
||||
if (info.type === 'private') {
|
||||
if (!user) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this data asset.',
|
||||
);
|
||||
}
|
||||
|
||||
const is_data_manager = user.permissions.includes(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||
);
|
||||
|
||||
const is_get = user.permissions.includes(
|
||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET.seqid,
|
||||
);
|
||||
|
||||
if (is_data_manager || is_get) {
|
||||
return info;
|
||||
}
|
||||
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this data asset.',
|
||||
);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private async getUserFromRequest(request: Request): Promise<RequestUser | null> {
|
||||
const accessToken = request.get('Authorization');
|
||||
if (accessToken) {
|
||||
const accessTokenDecoded: any = jwt.decode(accessToken, {
|
||||
complete: true,
|
||||
});
|
||||
|
||||
const { kid } = accessTokenDecoded.header;
|
||||
|
||||
const { keys } = await this.authClient.getPublicKeys();
|
||||
|
||||
const pemValue = keys.find((key) => key.kid === kid);
|
||||
|
||||
if (!pemValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
jwt.verify(accessToken, pemValue.pem);
|
||||
const accessTokenPayload = accessTokenDecoded.payload;
|
||||
|
||||
return {
|
||||
user_id: accessTokenPayload.user_id,
|
||||
username: accessTokenPayload.username,
|
||||
permissions: accessTokenPayload.permissions,
|
||||
roles: accessTokenPayload.roles,
|
||||
customer_id: accessTokenPayload.customer_id,
|
||||
customer_name: accessTokenPayload.customer_name,
|
||||
customer_tier: accessTokenPayload.customer_tier,
|
||||
customer_modules: accessTokenPayload.customer_modules,
|
||||
access_token: accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
} from '@nestjs/microservices';
|
||||
import { ConnectionTest } from '@dadosfera/protospack-v2';
|
||||
|
||||
const isLocalConnection =
|
||||
process.env.INFACTORY_URL.startsWith('in-factory:') ||
|
||||
process.env.INFACTORY_URL.includes('0.0.0.0');
|
||||
|
||||
export class ConnectionTestClientConfiguration {
|
||||
private config: GrpcOptions = {
|
||||
transport: Transport.GRPC,
|
||||
options: {
|
||||
url: process.env.INFACTORY_URL,
|
||||
package: [ConnectionTest.ProtoPackages.ReadPackage],
|
||||
credentials:
|
||||
process.env.ENV === 'local' ? undefined : credentials.createSsl(),
|
||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||
protoPath: [ConnectionTest.ProtoPaths.ReadFilePath],
|
||||
loader: {
|
||||
keepCase: true,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { User, RequestUser } from 'src/authentication/user.decorator';
|
||||
import { User, RequestUser } from 'src/decorators/user.decorator';
|
||||
import { ConnectionTestService } from './connection-test.service';
|
||||
import {
|
||||
ConnectionTestPingRes,
|
||||
@@ -22,15 +22,24 @@ import {
|
||||
ConnectionTestListTablesRes,
|
||||
GetTableMetadataRes,
|
||||
GetTableMetadataReq,
|
||||
RefreshCatalogReq,
|
||||
RefreshCatalogRes,
|
||||
RefreshCatalogStatusReq,
|
||||
} from './dto/connection-test';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { Authenticated } from 'src/authentication/authentication.decorator';
|
||||
import { Authenticated, RequireModule } from 'src/decorators/authentication.decorator';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
||||
import { DADOSFERA_MODULES_KEYS } from 'src/authentication/permissions.enum';
|
||||
|
||||
@ApiInternalOnlyController()
|
||||
@ApiTags('Connection Test')
|
||||
@Controller('connection-test')
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
@Authenticated()
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
export class ConnectionTestController {
|
||||
logger: any;
|
||||
constructor(
|
||||
@@ -82,7 +91,7 @@ export class ConnectionTestController {
|
||||
});
|
||||
return this.connectionTestService.connectionTestListSchemas(
|
||||
body,
|
||||
user.customer_name,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +108,7 @@ export class ConnectionTestController {
|
||||
});
|
||||
return this.connectionTestService.connectionTestListTables(
|
||||
body,
|
||||
user.customer_name,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +125,38 @@ export class ConnectionTestController {
|
||||
});
|
||||
return this.connectionTestService.getTableMetadata(
|
||||
body,
|
||||
user.customer_name,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('refresh-catalog')
|
||||
@ApiOkResponse({ type: RefreshCatalogRes })
|
||||
@HttpCode(HttpStatus.ACCEPTED)
|
||||
async refreshCatalog(
|
||||
@User() user: RequestUser,
|
||||
@Body(new ValidationPipe()) body: RefreshCatalogReq,
|
||||
) {
|
||||
this.logger.info('/connection-test/refresh-catalog', {
|
||||
user: user.user_id,
|
||||
customer: user.customer_name,
|
||||
connection: body.connection_id,
|
||||
});
|
||||
return this.connectionTestService.refreshCatalog(body, user);
|
||||
}
|
||||
|
||||
@Post('refresh-catalog/status')
|
||||
@ApiOkResponse({ type: RefreshCatalogRes })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async refreshCatalogStatus(
|
||||
@User() user: RequestUser,
|
||||
@Body(new ValidationPipe()) body: RefreshCatalogStatusReq,
|
||||
) {
|
||||
this.logger.info('/connection-test/refresh-catalog/status', {
|
||||
user: user.user_id,
|
||||
customer: user.customer_name,
|
||||
connection: body.connection_id,
|
||||
session: body.session_id,
|
||||
});
|
||||
return this.connectionTestService.refreshCatalogStatus(body, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,17 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { ClientsModule } from '@nestjs/microservices';
|
||||
import { ConnectionTestClientConfiguration } from './connection-test-client.config';
|
||||
import { ConnectionModule } from '../connection/connection.module';
|
||||
import { ConnectionsApiModule } from '../connections-api/connections-api.module';
|
||||
import { PlatformApiModule } from '../platform-api/platform-api.module';
|
||||
const client = new ConnectionTestClientConfiguration();
|
||||
@Module({
|
||||
controllers: [ConnectionTestController],
|
||||
providers: [ConnectionTestService, DadosferaLogger],
|
||||
imports: [ClientsModule.register([client.providerOptions]), ConnectionModule],
|
||||
imports: [
|
||||
ClientsModule.register([client.providerOptions]),
|
||||
ConnectionModule,
|
||||
ConnectionsApiModule,
|
||||
PlatformApiModule,
|
||||
],
|
||||
})
|
||||
export class ConnectionTestModule {}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { ConnectionTestService } from './connection-test.service';
|
||||
import { RequestUser } from 'src/decorators/user.decorator';
|
||||
|
||||
describe('ConnectionTestService catalog cache', () => {
|
||||
const user: RequestUser = {
|
||||
user_id: 'user-id',
|
||||
username: 'user@example.com',
|
||||
permissions: [],
|
||||
customer_id: 'customer-id',
|
||||
customer_name: 'customer-name',
|
||||
customer_tier: 'standard',
|
||||
access_token: 'token',
|
||||
customer_modules: [],
|
||||
roles: [],
|
||||
};
|
||||
const grpcClient = { getService: jest.fn().mockReturnValue({}) };
|
||||
const connectionsService = {};
|
||||
const connectionsApiService = { proxy: jest.fn() };
|
||||
const platformApiService = { proxy: jest.fn() };
|
||||
let service: ConnectionTestService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new ConnectionTestService(
|
||||
grpcClient as any,
|
||||
connectionsService as any,
|
||||
connectionsApiService as any,
|
||||
platformApiService as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the existing schemas response contract', async () => {
|
||||
connectionsApiService.proxy.mockResolvedValue({
|
||||
schemas: [{ schema_name: 'analytics' }, { schema_name: 'public' }],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.connectionTestListSchemas(
|
||||
{ connection_id: 'config-id', plugin: 'postgresql' },
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: true,
|
||||
schema_list: ['analytics', 'public'],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the existing tables response contract', async () => {
|
||||
connectionsApiService.proxy.mockResolvedValue({
|
||||
tables: [{ table_name: 'customers' }, { table_name: 'orders' }],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.connectionTestListTables(
|
||||
{
|
||||
connection_id: 'config-id',
|
||||
plugin: 'postgresql',
|
||||
schema: 'public',
|
||||
},
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: true,
|
||||
table_list: ['customers', 'orders'],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps cached columns to the existing table metadata contract', async () => {
|
||||
connectionsApiService.proxy.mockResolvedValue({
|
||||
columns: [
|
||||
{
|
||||
column_name: 'id',
|
||||
data_type: 'bigint',
|
||||
is_primary_key: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.getTableMetadata(
|
||||
{
|
||||
connection_id: 'config-id',
|
||||
plugin: 'postgresql',
|
||||
schema: 'public',
|
||||
table_list: ['customers'],
|
||||
},
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: true,
|
||||
tables_metadata: [
|
||||
{
|
||||
table_name: 'customers',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigint',
|
||||
is_primary_key: true,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(connectionsApiService.proxy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/connection_catalog/config-id/schemas/public/tables/customers/columns',
|
||||
user,
|
||||
);
|
||||
});
|
||||
|
||||
it('submits a catalog refresh without holding the request open', async () => {
|
||||
platformApiService.proxy.mockResolvedValue({
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.refreshCatalog(
|
||||
{ connection_id: 'config-id', plugin: 'postgresql' },
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: true,
|
||||
status: 'PENDING',
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
});
|
||||
|
||||
expect(platformApiService.proxy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/connection_test',
|
||||
user,
|
||||
{
|
||||
customer_id: user.customer_name,
|
||||
plugin: 'postgresql',
|
||||
task: {
|
||||
task_type: 'refresh_catalog',
|
||||
connection: {
|
||||
provider: 'connection_manager',
|
||||
config_id: 'config-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps polling without changing the catalog pointer while pending', async () => {
|
||||
platformApiService.proxy.mockResolvedValue({ status: 'PENDING' });
|
||||
|
||||
await expect(
|
||||
service.refreshCatalogStatus(
|
||||
{
|
||||
connection_id: 'config-id',
|
||||
plugin: 'postgresql',
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
},
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: false,
|
||||
status: 'PENDING',
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
});
|
||||
|
||||
expect(connectionsApiService.proxy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('publishes the catalog pointer after the refresh finishes', async () => {
|
||||
platformApiService.proxy.mockResolvedValue({ status: 'DONE' });
|
||||
connectionsApiService.proxy.mockResolvedValue({
|
||||
last_catalog_refresh_status: 'SUCCESS',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.refreshCatalogStatus(
|
||||
{
|
||||
connection_id: 'config/id',
|
||||
plugin: 'postgresql',
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
},
|
||||
user,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
operation_result: true,
|
||||
status: 'DONE',
|
||||
session_id: 'session-id',
|
||||
date: '20260731',
|
||||
});
|
||||
|
||||
expect(connectionsApiService.proxy).toHaveBeenCalledWith(
|
||||
'PUT',
|
||||
'/connection_config/config%2Fid/catalog_metadata',
|
||||
user,
|
||||
{
|
||||
last_catalog_refresh_status: 'SUCCESS',
|
||||
last_catalog_connection_test_date: '20260731',
|
||||
last_catalog_connection_test_session_id: 'session-id',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { ConnectionTest } from '@dadosfera/protospack-v2';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
@@ -13,14 +13,19 @@ import {
|
||||
ConnectionTestPingRes,
|
||||
GetTableMetadataReq,
|
||||
GetTableMetadataRes,
|
||||
RefreshCatalogReq,
|
||||
RefreshCatalogRes,
|
||||
RefreshCatalogStatusReq,
|
||||
} from './dto/connection-test';
|
||||
import { ConnectionClientService } from '../connection/client.service';
|
||||
import {
|
||||
CreateConnectionDto,
|
||||
DatabaseConnectionPropertiesDto,
|
||||
} from '../connection/dtos/connection';
|
||||
import { RequestUser } from 'src/authentication/user.decorator';
|
||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
||||
import { RequestUser } from 'src/decorators/user.decorator';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { ConnectionsApiService } from '../connections-api/connections-api.service';
|
||||
import { PlatformApiService } from '../platform-api/platform-api.service';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTestService {
|
||||
@@ -28,6 +33,8 @@ export class ConnectionTestService {
|
||||
constructor(
|
||||
@Inject('ConnectionTestGrpcClient') private readonly grpcClient: ClientGrpc,
|
||||
private connectionsService: ConnectionClientService,
|
||||
private connectionsApiService: ConnectionsApiService,
|
||||
private platformApiService: PlatformApiService,
|
||||
) {
|
||||
this.connectionTestReadClient =
|
||||
grpcClient.getService<ConnectionTest.ReadService.ConnectionTestReadServices>(
|
||||
@@ -147,45 +154,137 @@ export class ConnectionTestService {
|
||||
}
|
||||
async connectionTestListSchemas(
|
||||
body: ConnectionTestListSchemasReq,
|
||||
customer_name: string,
|
||||
user: RequestUser,
|
||||
): Promise<ConnectionTestListSchemasRes> {
|
||||
const { connection_id, plugin } = body;
|
||||
return lastValueFrom(
|
||||
this.connectionTestReadClient.ListSchemas({
|
||||
connection_id,
|
||||
customer_name,
|
||||
plugin,
|
||||
}),
|
||||
const result = await this.connectionsApiService.proxy(
|
||||
'GET',
|
||||
`/connection_catalog/${encodeURIComponent(body.connection_id)}/schemas`,
|
||||
user,
|
||||
);
|
||||
return {
|
||||
operation_result: true,
|
||||
schema_list: result.schemas.map((schema) => schema.schema_name),
|
||||
};
|
||||
}
|
||||
|
||||
async connectionTestListTables(
|
||||
body: ConnectionTestListTablesReq,
|
||||
customer_name: string,
|
||||
user: RequestUser,
|
||||
): Promise<ConnectionTestListTablesRes> {
|
||||
const { connection_id, plugin, schema } = body;
|
||||
return lastValueFrom(
|
||||
this.connectionTestReadClient.ListTables({
|
||||
connection_id,
|
||||
customer_name,
|
||||
plugin,
|
||||
schema,
|
||||
}),
|
||||
const result = await this.connectionsApiService.proxy(
|
||||
'GET',
|
||||
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
|
||||
`/schemas/${encodeURIComponent(body.schema)}/tables`,
|
||||
user,
|
||||
);
|
||||
return {
|
||||
operation_result: true,
|
||||
table_list: result.tables.map((table) => table.table_name),
|
||||
};
|
||||
}
|
||||
|
||||
async getTableMetadata(
|
||||
body: GetTableMetadataReq,
|
||||
customer_name: string,
|
||||
user: RequestUser,
|
||||
): Promise<GetTableMetadataRes> {
|
||||
const { schema, plugin, table_list, connection_id } = body;
|
||||
return lastValueFrom(
|
||||
this.connectionTestReadClient.GetTableMetadata({
|
||||
connection_id,
|
||||
customer_name,
|
||||
plugin,
|
||||
schema,
|
||||
table_list,
|
||||
const tables_metadata = await Promise.all(
|
||||
body.table_list.map(async (table_name) => {
|
||||
const result = await this.connectionsApiService.proxy(
|
||||
'GET',
|
||||
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
|
||||
`/schemas/${encodeURIComponent(body.schema)}` +
|
||||
`/tables/${encodeURIComponent(table_name)}/columns`,
|
||||
user,
|
||||
);
|
||||
return {
|
||||
table_name,
|
||||
columns: result.columns.map((column) => ({
|
||||
name: column.column_name,
|
||||
type: column.data_type,
|
||||
is_primary_key: column.is_primary_key,
|
||||
})),
|
||||
references: [],
|
||||
};
|
||||
}),
|
||||
);
|
||||
return { operation_result: true, tables_metadata };
|
||||
}
|
||||
|
||||
async refreshCatalog(
|
||||
body: RefreshCatalogReq,
|
||||
user: RequestUser,
|
||||
): Promise<RefreshCatalogRes> {
|
||||
const task = await this.platformApiService.proxy(
|
||||
'POST',
|
||||
'/connection_test',
|
||||
user,
|
||||
{
|
||||
customer_id: user.customer_name,
|
||||
plugin: body.plugin,
|
||||
task: {
|
||||
task_type: 'refresh_catalog',
|
||||
connection: {
|
||||
provider: 'connection_manager',
|
||||
config_id: body.connection_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!task.session_id || !task.date) {
|
||||
throw new HttpException(
|
||||
'Platform API did not return a catalog refresh task identifier',
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
operation_result: true,
|
||||
status: 'PENDING',
|
||||
session_id: task.session_id,
|
||||
date: task.date,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshCatalogStatus(
|
||||
body: RefreshCatalogStatusReq,
|
||||
user: RequestUser,
|
||||
): Promise<RefreshCatalogRes> {
|
||||
const result = await this.platformApiService.proxy(
|
||||
'POST',
|
||||
'/connection_test/status',
|
||||
user,
|
||||
{
|
||||
session_id: body.session_id,
|
||||
date: body.date,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'DONE') {
|
||||
await this.connectionsApiService.proxy(
|
||||
'PUT',
|
||||
`/connection_config/${encodeURIComponent(
|
||||
body.connection_id,
|
||||
)}/catalog_metadata`,
|
||||
user,
|
||||
{
|
||||
last_catalog_refresh_status: 'SUCCESS',
|
||||
last_catalog_connection_test_date: body.date,
|
||||
last_catalog_connection_test_session_id: body.session_id,
|
||||
},
|
||||
);
|
||||
} else if (result.status === 'ERROR' || result.status === 'EXPIRED') {
|
||||
throw new HttpException(
|
||||
`Catalog refresh finished with status ${result.status}`,
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
operation_result: result.status === 'DONE',
|
||||
status: result.status,
|
||||
session_id: body.session_id,
|
||||
date: body.date,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional } from 'class-validator';
|
||||
import { IsIn, IsString, IsOptional } from 'class-validator';
|
||||
import { DatabaseConnectionPropertiesDto } from 'src/modules/connection/dtos/connection';
|
||||
import { CreateConnectionDto } from 'src/modules/connection/dtos/connection';
|
||||
export class ColumnDto {
|
||||
@@ -7,6 +7,8 @@ export class ColumnDto {
|
||||
name: string;
|
||||
@ApiProperty()
|
||||
type: string;
|
||||
@ApiProperty()
|
||||
is_primary_key: boolean;
|
||||
}
|
||||
export class TableMetadataDto {
|
||||
@ApiProperty()
|
||||
@@ -131,3 +133,37 @@ export class GetTableMetadataRes {
|
||||
@ApiProperty({ type: [TableMetadataDto] })
|
||||
tables_metadata: TableMetadataDto[];
|
||||
}
|
||||
|
||||
export class RefreshCatalogReq {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
connection_id: string;
|
||||
|
||||
@ApiProperty({ enum: ['oracle', 'mysql', 'postgresql', 'sqlserver'] })
|
||||
@IsIn(['oracle', 'mysql', 'postgresql', 'sqlserver'])
|
||||
plugin: string;
|
||||
}
|
||||
|
||||
export class RefreshCatalogStatusReq extends RefreshCatalogReq {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
session_id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
date: string;
|
||||
}
|
||||
|
||||
export class RefreshCatalogRes {
|
||||
@ApiProperty()
|
||||
operation_result: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
status: string;
|
||||
|
||||
@ApiProperty()
|
||||
session_id: string;
|
||||
|
||||
@ApiProperty()
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
} from '@nestjs/microservices';
|
||||
import { ConnectionManager } from '@dadosfera/protospack-v2';
|
||||
|
||||
const isLocalConnection =
|
||||
process.env.INFACTORY_URL.startsWith('in-factory:') ||
|
||||
process.env.INFACTORY_URL.includes('0.0.0.0');
|
||||
|
||||
export class ConnectionClientConfiguration {
|
||||
public name = 'ConnectionClientConfiguration';
|
||||
private config: GrpcOptions = {
|
||||
@@ -16,10 +20,7 @@ export class ConnectionClientConfiguration {
|
||||
ConnectionManager.ProtoPackages.WritePackage,
|
||||
ConnectionManager.ProtoPackages.ReadPackage,
|
||||
],
|
||||
credentials:
|
||||
process.env.LOCAL_ENV || process.env.ENV === 'local'
|
||||
? undefined
|
||||
: credentials.createSsl(),
|
||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||
protoPath: [
|
||||
ConnectionManager.ProtoPaths.WriteFilePath,
|
||||
ConnectionManager.ProtoPaths.ReadFilePath,
|
||||
|
||||
@@ -7,36 +7,42 @@ import {
|
||||
Param,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
Query,
|
||||
UseFilters,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
|
||||
import { ConnectionClientService } from './client.service';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
} from 'src/authentication/authentication.decorator';
|
||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import { RequestUser, User } from 'src/authentication/user.decorator';
|
||||
RequireModule,
|
||||
} from 'src/decorators/authentication.decorator';
|
||||
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import { ValidationPipe } from '../../pipes/object-validation.pipe';
|
||||
import {
|
||||
ConnectionDetailsRes,
|
||||
ConnectionRes,
|
||||
ConnectionsRes,
|
||||
GetAllConnectionsReq,
|
||||
UpdateConnectionDto,
|
||||
} from './dtos/connection';
|
||||
import { CreateConnectionDto } from './dtos/connection';
|
||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { Language } from 'src/decorators/language.decorator';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
||||
|
||||
const connectionPermissions = PERMISSIONS_GROUPS.CONNECTION.permissions;
|
||||
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||
@ApiTags('connections')
|
||||
@ApiBearerAuth()
|
||||
@Authenticated()
|
||||
@Controller('connections')
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
export class ConnectionController {
|
||||
logger: any;
|
||||
constructor(
|
||||
@@ -47,7 +53,9 @@ export class ConnectionController {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
@ApiInternalOnlyEndpoint()
|
||||
@Get('connections-migration')
|
||||
@ApiExcludeEndpoint()
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.DADOSFERA.permissions.CONNECTIONS_MIGRATION,
|
||||
)
|
||||
@@ -83,12 +91,11 @@ export class ConnectionController {
|
||||
@RequireAllPermissions(connectionPermissions.CREATE)
|
||||
async updateConnection(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-Lang') language: string,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Body(new ValidationPipe()) body: UpdateConnectionDto,
|
||||
): Promise<ConnectionRes> {
|
||||
this.logger.info('/connections - Update Connection');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
const { user_id, customer_id, customer_name, username } = user;
|
||||
const metadata = PackTheMetadata({
|
||||
@@ -135,11 +142,10 @@ export class ConnectionController {
|
||||
@Get()
|
||||
async getAllConnections(
|
||||
@User() user: RequestUser,
|
||||
@Headers('Dadosfera-Lang') language,
|
||||
@Query() queries,
|
||||
@Language() language: LanguageEnum,
|
||||
@Query() queries: GetAllConnectionsReq,
|
||||
): Promise<ConnectionsRes> {
|
||||
this.logger.info('/connections - Get All Connection');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
const { user_id, customer_id, customer_name, username } = user;
|
||||
const { search, size, page, ...filters } = queries;
|
||||
@@ -161,13 +167,12 @@ export class ConnectionController {
|
||||
|
||||
@Get('/:id')
|
||||
async getConnectionDetails(
|
||||
@Headers('Dadosfera-Lang') language,
|
||||
@Language() language: LanguageEnum,
|
||||
@User() user: RequestUser,
|
||||
@Param('id') id,
|
||||
@Param('id') id: string,
|
||||
@Query() queries,
|
||||
): Promise<ConnectionDetailsRes> {
|
||||
this.logger.info('/connections - Get Connection Details');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
console.log(queries);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ConnectionToCatalog,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/ConnectionManager/interfaces/entities';
|
||||
import {
|
||||
GetAllConnectionRequest,
|
||||
GetAllConnectionResponse,
|
||||
GetConnectionDetailsResponse,
|
||||
GetConnectionResponse,
|
||||
@@ -21,7 +22,7 @@ export type ConnectionCredentialsType =
|
||||
| 'oauth'
|
||||
| 'api_key'
|
||||
| 'service_account'
|
||||
| 'headers_authF';
|
||||
| 'headers_auth';
|
||||
export interface ConnectionApiConnection {
|
||||
config_id: string;
|
||||
plugin: string;
|
||||
@@ -94,7 +95,6 @@ export class ConnectionDto implements Connection {
|
||||
|
||||
export class ConnectionToCatalogDto implements ConnectionToCatalog {
|
||||
// ---Automatically generated information - will not be sent by the frontend--- //
|
||||
id: string;
|
||||
customer_id: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
@@ -104,6 +104,9 @@ export class ConnectionToCatalogDto implements ConnectionToCatalog {
|
||||
customer_name: string;
|
||||
|
||||
// ---Information sent by the frontend--- //
|
||||
@ApiPropertyOptional()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@@ -147,6 +150,15 @@ export class UpdateConnectionDto extends PickType(ConnectionDto, [
|
||||
'properties',
|
||||
]) {}
|
||||
|
||||
export class GetAllConnectionsReq implements GetAllConnectionRequest {
|
||||
@ApiPropertyOptional()
|
||||
search?: string;
|
||||
@ApiPropertyOptional()
|
||||
page?: string;
|
||||
@ApiPropertyOptional()
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export class ConnectionsRes implements GetAllConnectionResponse {
|
||||
@ApiProperty({ type: [ConnectionToCatalogDto] })
|
||||
connections: ConnectionToCatalogDto[];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const CONNECTIONS_API_CONFIG = {
|
||||
getUrl: (): string => {
|
||||
const url = process.env.CONNECTIONS_API_URL;
|
||||
if (!url) {
|
||||
throw new Error('CONNECTIONS_API_URL environment variable is not set');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
timeout: parseInt(process.env.CONNECTIONS_API_TIMEOUT || '30000', 10),
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import { ConnectionsApiService } from './connections-api.service';
|
||||
|
||||
@Module({
|
||||
providers: [ConnectionsApiService, DadosferaLogger],
|
||||
exports: [ConnectionsApiService],
|
||||
})
|
||||
export class ConnectionsApiModule {}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Injectable, Inject, HttpException } from '@nestjs/common';
|
||||
import { SignatureV4 } from '@aws-sdk/signature-v4';
|
||||
import { Sha256 } from '@aws-crypto/sha256-js';
|
||||
import { defaultProvider } from '@aws-sdk/credential-provider-node';
|
||||
import axios, { AxiosResponse, Method } from 'axios';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import { RequestUser } from '../../decorators/user.decorator';
|
||||
import { CONNECTIONS_API_CONFIG } from './connections-api.config';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionsApiService {
|
||||
private signer: SignatureV4;
|
||||
private logger: any;
|
||||
|
||||
constructor(@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
this.signer = new SignatureV4({
|
||||
service: 'execute-api',
|
||||
region: CONNECTIONS_API_CONFIG.region,
|
||||
credentials: defaultProvider(),
|
||||
sha256: Sha256,
|
||||
});
|
||||
}
|
||||
|
||||
async proxy(
|
||||
method: string,
|
||||
path: string,
|
||||
user: RequestUser,
|
||||
body?: any,
|
||||
query?: Record<string, string>,
|
||||
): Promise<any> {
|
||||
const baseUrl = CONNECTIONS_API_CONFIG.getUrl();
|
||||
const url = new URL(`${baseUrl}${path}`);
|
||||
|
||||
if (query) {
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
host: url.hostname,
|
||||
'content-type': 'application/json',
|
||||
customer_name: user.customer_name || '',
|
||||
customer_id: user.customer_id || '',
|
||||
'x-user-id': user.user_id || '',
|
||||
'x-username': user.username || '',
|
||||
'x-customer-tier': user.customer_tier || '',
|
||||
'x-customer-id': user.customer_id || '',
|
||||
};
|
||||
const requestToSign = {
|
||||
method: method.toUpperCase(),
|
||||
protocol: url.protocol,
|
||||
hostname: url.hostname,
|
||||
port: url.port ? parseInt(url.port, 10) : undefined,
|
||||
path: url.pathname + url.search,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
const signedRequest = await this.signer.sign(requestToSign);
|
||||
const response: AxiosResponse = await axios({
|
||||
method: method as Method,
|
||||
url: url.href,
|
||||
headers: signedRequest.headers as Record<string, string>,
|
||||
data: body,
|
||||
timeout: CONNECTIONS_API_CONFIG.timeout,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new HttpException(response.data, response.status);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Connections API proxy error', {
|
||||
error: error.message,
|
||||
path,
|
||||
method: method.toUpperCase(),
|
||||
});
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
if (error.response) {
|
||||
throw new HttpException(error.response.data, error.response.status);
|
||||
}
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
throw new HttpException('Connections API service unavailable', 503);
|
||||
}
|
||||
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
||||
throw new HttpException('Connections API request timeout', 504);
|
||||
}
|
||||
throw new HttpException('Internal server error', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
} from '@nestjs/microservices';
|
||||
import { ConnectorManager } from '@dadosfera/protospack-v2';
|
||||
|
||||
const isLocalConnection =
|
||||
process.env.INFACTORY_URL.startsWith('in-factory:') ||
|
||||
process.env.INFACTORY_URL.includes('0.0.0.0');
|
||||
|
||||
export class ConnectorClientConfiguration {
|
||||
public name = 'ConnectorClientConfiguration';
|
||||
private config: GrpcOptions = {
|
||||
@@ -16,10 +20,7 @@ export class ConnectorClientConfiguration {
|
||||
ConnectorManager.ProtoPackages.WritePackage,
|
||||
ConnectorManager.ProtoPackages.ReadPackage,
|
||||
],
|
||||
credentials:
|
||||
process.env.LOCAL_ENV || process.env.ENV === 'local'
|
||||
? undefined
|
||||
: credentials.createSsl(),
|
||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||
protoPath: [
|
||||
ConnectorManager.ProtoPaths.WriteFilePath,
|
||||
ConnectorManager.ProtoPaths.ReadFilePath,
|
||||
|
||||
@@ -11,11 +11,10 @@ import {
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
Inject,
|
||||
Headers,
|
||||
UploadedFiles,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { ConnectorClientService } from './client.service';
|
||||
import { AddTagDto } from './dtos/add-tag';
|
||||
import { CreateConnectorDto } from './dtos/create-connector';
|
||||
@@ -26,12 +25,16 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
RequireModule,
|
||||
RequireSomePermission,
|
||||
} from 'src/authentication/authentication.decorator';
|
||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
} from 'src/decorators/authentication.decorator';
|
||||
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import { Language } from 'src/decorators/language.decorator';
|
||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
||||
|
||||
@ApiInternalOnlyController()
|
||||
@ApiTags('connectors')
|
||||
@ApiBearerAuth()
|
||||
@Authenticated()
|
||||
@Controller('connectors')
|
||||
export class ConnectorController {
|
||||
@@ -97,12 +100,14 @@ export class ConnectorController {
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async getAllConnectors(
|
||||
@Headers('dadosfera-lang') language,
|
||||
@Language() language: LanguageEnum,
|
||||
@Query() queries: GetAllDto,
|
||||
) {
|
||||
this.logger.info('/GET - getAllConnectors Route');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
const { search, size, page, ...filters } = queries;
|
||||
|
||||
@@ -130,6 +135,9 @@ export class ConnectorController {
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async getConnectorsTags() {
|
||||
return await this.connectorClientService.getConnectorsTags();
|
||||
}
|
||||
@@ -142,13 +150,15 @@ export class ConnectorController {
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async getConnector(
|
||||
@Headers('dadosfera-lang') language,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('plugin') plugin: string,
|
||||
@Query('version') version: string,
|
||||
) {
|
||||
this.logger.info('/GET/:plugin - getConnector Route');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
const pluginId = `${plugin}-${version}`;
|
||||
|
||||
@@ -171,14 +181,15 @@ export class ConnectorController {
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
|
||||
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
|
||||
)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async getConnectorDetails(
|
||||
@Headers('dadosfera-lang') language,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('plugin') plugin: string,
|
||||
@Query('version') version: string,
|
||||
) {
|
||||
this.logger.info('/upload - Upload Connector Route');
|
||||
if (!language) language = 'en-us';
|
||||
|
||||
const pluginId = `${plugin}-${version}`;
|
||||
|
||||
const response: any = await this.connectorClientService.getConnectorDetails(
|
||||
@@ -195,6 +206,9 @@ export class ConnectorController {
|
||||
@Put('/:plugin')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async updateConnector(
|
||||
@Param('plugin') plugin: string,
|
||||
@Body() body: UpdateDto,
|
||||
@@ -216,6 +230,9 @@ export class ConnectorController {
|
||||
|
||||
@Put('/:plugin/add-tag')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async addTagOnConnector(
|
||||
@Param('plugin') plugin: string,
|
||||
@Body() body: AddTagDto,
|
||||
@@ -243,6 +260,9 @@ export class ConnectorController {
|
||||
|
||||
@Put('/:plugin/remove-tag')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async removeTagOnConnector(
|
||||
@Param('plugin') plugin: string,
|
||||
@Body() body: RemoveTagDto,
|
||||
@@ -271,20 +291,20 @@ export class ConnectorController {
|
||||
|
||||
@Delete('/:plugin')
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.DELETE)
|
||||
@RequireModule(
|
||||
DADOSFERA_MODULES_KEYS.COLLECT
|
||||
)
|
||||
async deleteConnector(
|
||||
@Param('plugin') plugin: string,
|
||||
@Query('version') version: string,
|
||||
) {
|
||||
this.logger.info('/upload - Upload Connector Route');
|
||||
this.logger.info('DELETE /:plugin - Delete Connector Route');
|
||||
|
||||
const response: any = await this.connectorClientService.deleteConnector(
|
||||
const response = await this.connectorClientService.deleteConnector(
|
||||
plugin,
|
||||
version,
|
||||
);
|
||||
|
||||
return {
|
||||
message: response.message || 'ok',
|
||||
connector: { ...JSON.parse(response.connector) },
|
||||
};
|
||||
return { message: response.message, connector: response.connector };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseFilters,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiProduces, ApiTags } from '@nestjs/swagger';
|
||||
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
RequireModule,
|
||||
RequireSomePermission,
|
||||
} from 'src/decorators/authentication.decorator';
|
||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||
import { CustomersService } from './customers.service';
|
||||
import { CustomerLinkRequest, CustomerLinksResponse } from './dtos/customers';
|
||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||
import type { StringValue } from 'ms';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { EnforceMfa } from './dtos/enforce-mfa';
|
||||
|
||||
@ApiTags('Customers')
|
||||
@Controller('customers')
|
||||
@UseFilters(GrpcToHttpExceptionFilter)
|
||||
export class CustomersController {
|
||||
logger: DadosferaLogger;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
private customersService: CustomersService,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
@Post(':id/mfa')
|
||||
@Authenticated()
|
||||
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@RequireModule(DADOSFERA_MODULES_KEYS.DANGER_ZONE)
|
||||
async enableMfaEnforce(@Param('id') id: string, @Body() data: EnforceMfa) {
|
||||
this.logger.info('enableMfaEnforce', { id });
|
||||
return await this.customersService.enableEnforceMfa(id, data.enabled);
|
||||
}
|
||||
|
||||
@Get(':id/links')
|
||||
@Authenticated()
|
||||
@ApiOkResponse({ type: CustomerLinksResponse })
|
||||
async getCustomerLinks(@Param('id') id: string) {
|
||||
this.logger.info('getCustomerLinks', { id });
|
||||
const links = await this.customersService.getLinks(id);
|
||||
return { links };
|
||||
}
|
||||
|
||||
@Put(':id/links')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@ApiOkResponse()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async setCustomerLinks(
|
||||
@Body() body: CustomerLinkRequest,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const { links } = body;
|
||||
this.logger.info('setCustomerLinks', { id, links });
|
||||
await this.customersService.setLinks(id, links);
|
||||
}
|
||||
|
||||
@Get('token')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.AUTH.permissions.GENERATE_TOKEN)
|
||||
@ApiProduces('text/plain')
|
||||
async getCustomerToken(
|
||||
@Query('exp') exp: StringValue,
|
||||
@User() user: RequestUser,
|
||||
): Promise<string> {
|
||||
this.logger.info('getCustomerToken', { exp, user });
|
||||
const data = {
|
||||
customerId: user.customer_id,
|
||||
userId: user.user_id,
|
||||
customerName: user.customer_name,
|
||||
};
|
||||
return this.customersService.generateToken(exp, data);
|
||||
}
|
||||
|
||||
@Get('monitoring-dashboard')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.CUSTOMER.permissions.MONITORING_DASHBOARD,
|
||||
)
|
||||
async getCustomerMonitoringDashboard(
|
||||
@User() user: RequestUser,
|
||||
): Promise<{ url: string }> {
|
||||
this.logger.info('getCustomerMonitoringDashboard');
|
||||
|
||||
const metadata = PackTheMetadata(user);
|
||||
return this.customersService.getMonitoringDashboardUrl(metadata);
|
||||
}
|
||||
|
||||
@Get('logs-dashboard')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.USERS.permissions.ADMIN,
|
||||
)
|
||||
@RequireModule(DADOSFERA_MODULES_KEYS.LOG_DASHBOARD)
|
||||
async getCustomerMixPanelLogsDashboard(
|
||||
@User() user: RequestUser,
|
||||
): Promise<{ url: string }> {
|
||||
this.logger.info('getLogsDashboardUrl');
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
const result = await this.customersService.getLogsDashboardUrl(metadata);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('access-dashboard')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(
|
||||
PERMISSIONS_GROUPS.USERS.permissions.ADMIN,
|
||||
)
|
||||
@RequireModule(DADOSFERA_MODULES_KEYS.ACCESS_DASHBOARD)
|
||||
async getAccessDashboard(
|
||||
@User() user: RequestUser,
|
||||
): Promise<{ url: string }> {
|
||||
this.logger.info('getAccessDashboard');
|
||||
const metadata = PackTheMetadata(user);
|
||||
|
||||
const result = await this.customersService.getAccessDashboardUrl(user.customer_name, metadata);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get(':id/organization-info')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@ApiOkResponse({ description: 'Organization information' })
|
||||
async getOrganizationInfo(@Param('id') id: string) {
|
||||
this.logger.info('getOrganizationInfo', { id });
|
||||
return this.customersService.getOrganizationInfo(id);
|
||||
}
|
||||
|
||||
@Put(':id/organization-info')
|
||||
@Authenticated()
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOkResponse({ description: 'Organization information updated' })
|
||||
async updateOrganizationInfo(
|
||||
@Param('id') id: string,
|
||||
@Body() body: {
|
||||
companyName: string;
|
||||
companySite: string;
|
||||
domain: string;
|
||||
cnpj: string;
|
||||
description: string;
|
||||
},
|
||||
) {
|
||||
return this.customersService.updateOrganizationInfo(id, body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { ClientsModule } from '@nestjs/microservices';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { CustomersController } from './customers.controller';
|
||||
import { CustomersService } from './customers.service';
|
||||
import { PipelinesClientConfiguration } from '../pipelinesV2/pipelines-client';
|
||||
|
||||
const ducClient = new DucClient();
|
||||
const piFactoryClient = new PipelinesClientConfiguration();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClientsModule.register([
|
||||
ducClient.providerOptions,
|
||||
piFactoryClient.providerOptions,
|
||||
]),
|
||||
],
|
||||
controllers: [CustomersController],
|
||||
providers: [CustomersService, DadosferaLogger],
|
||||
exports: [CustomersService],
|
||||
})
|
||||
export class CustomersModule {}
|
||||
@@ -0,0 +1,279 @@
|
||||
import {
|
||||
OnModuleInit,
|
||||
Inject,
|
||||
Injectable,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
InternalServerErrorException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
import { CustomerSetLinksRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||
import { CustomersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import { CustomerLinksConfig } from './dtos/customers';
|
||||
import ErrorCodes from 'src/utils/errorCodes';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import {
|
||||
GetSecretValueCommand,
|
||||
SecretsManagerClient,
|
||||
} from '@aws-sdk/client-secrets-manager';
|
||||
import getEnv from 'src/utils/getEnv';
|
||||
import { logger } from 'elastic-apm-node';
|
||||
import {
|
||||
ReadService,
|
||||
ProtoServices as PipelineProtoServices,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { PipelinesClientConfiguration } from '../pipelinesV2/pipelines-client';
|
||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||
|
||||
// This function will accept any string, which may result in a bug.
|
||||
@Injectable()
|
||||
export class CustomersService implements OnModuleInit {
|
||||
|
||||
private customerService: CustomersProtoService;
|
||||
private logger: DadosferaLogger;
|
||||
private pipelineReadService: ReadService.PipelineV2ReadService;
|
||||
|
||||
constructor(
|
||||
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||
@Inject(PipelinesClientConfiguration.name)
|
||||
private readonly pipelinesGrpcClient: ClientGrpc,
|
||||
@Inject(DadosferaLogger)
|
||||
dadosferaLogger: DadosferaLogger,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.customerService = this.grpcClient.getService<CustomersProtoService>(
|
||||
ProtoServices.CustomersProtoService,
|
||||
);
|
||||
this.pipelineReadService =
|
||||
this.pipelinesGrpcClient.getService<ReadService.PipelineV2ReadService>(
|
||||
PipelineProtoServices.PipelineV2ReadService,
|
||||
);
|
||||
}
|
||||
|
||||
async getCustomer(customerId: string) {
|
||||
return await lastValueFrom(
|
||||
this.customerService.CustomerFindOneById({
|
||||
id: customerId
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async getLinks(customerId: string): Promise<CustomerLinksConfig | null> {
|
||||
try {
|
||||
const result = await lastValueFrom(
|
||||
this.customerService.CustomerGetLinks({ customerId }),
|
||||
);
|
||||
return (result.links as CustomerLinksConfig) || null;
|
||||
} catch (err) {
|
||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
|
||||
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async setLinks(customerId: string, links: CustomerLinksConfig) {
|
||||
if (!customerId || !links) {
|
||||
throw new HttpException(null, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.customerService.CustomerSetLinks({
|
||||
customerId,
|
||||
links: links as CustomerSetLinksRequest['links'],
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
|
||||
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
|
||||
else throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async generateToken(
|
||||
expiresIn = '30m',
|
||||
data: { customerId: string; userId: string; customerName: string },
|
||||
) {
|
||||
const { customerId, userId: generatedById, customerName } = data;
|
||||
|
||||
const secretsManagerClient = new SecretsManagerClient({});
|
||||
|
||||
const getSecretComand = new GetSecretValueCommand({
|
||||
SecretId: `${getEnv()}/${customerName}/jwt-signing-key`,
|
||||
});
|
||||
let private_key_pem;
|
||||
await secretsManagerClient
|
||||
.send(getSecretComand)
|
||||
.then((res) => {
|
||||
const secret = JSON.parse(res.SecretString);
|
||||
|
||||
private_key_pem = secret.private_key_pem;
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err.stack);
|
||||
throw new InternalServerErrorException(
|
||||
'Error finding keys for customer',
|
||||
);
|
||||
});
|
||||
let jwt_token;
|
||||
try {
|
||||
jwt_token = jwt.sign(
|
||||
{
|
||||
customerId,
|
||||
generatedById,
|
||||
},
|
||||
private_key_pem,
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
expiresIn,
|
||||
issuer: 'maestro',
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err.stack);
|
||||
throw new InternalServerErrorException('Error generating token');
|
||||
}
|
||||
// const verify = jwt.verify(jwt_token, public_key_pem, {
|
||||
// algorithms: ['RS256'],
|
||||
// issuer: 'maestro',
|
||||
// });
|
||||
// const decoded = jwt.decode(jwt_token, { complete: true });
|
||||
return jwt_token;
|
||||
}
|
||||
|
||||
async getMonitoringDashboardUrl(metadata: Metadata) {
|
||||
logger.info('CustomersService - getMonitoringDashboardUrl');
|
||||
|
||||
const res = await lastValueFrom(
|
||||
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||
{
|
||||
dashboard_id: '98',
|
||||
exp: '15m',
|
||||
metabase_customer_name: 'dadosferatech',
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
logger.info('Done');
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getLogsDashboardUrl(metadata: Metadata) {
|
||||
logger.info('CustomersService - getMixPanelLogsDashboardUrl');
|
||||
|
||||
const res = await lastValueFrom(
|
||||
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||
{
|
||||
dashboard_id: '103',
|
||||
exp: '15m',
|
||||
metabase_customer_name: 'dadosferatech',
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
logger.info('Done');
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getAccessDashboardUrl(customerName: string, metadata: Metadata) {
|
||||
/*
|
||||
* TODO(Refactor): dar um jeito de exibir o dash da sbm diferente dos outros customer
|
||||
* pois o signicado de department para sbm significa as instituições do usuários
|
||||
*/
|
||||
if (customerName !== 'sbmoffshorecom') {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
logger.info('CustomersService - getAccessDashboardUrl');
|
||||
|
||||
const res = await this.getDashboardUrl('105', metadata);
|
||||
logger.info('Done');
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private async getDashboardUrl(dashboardId: string, metadata: Metadata) {
|
||||
return await lastValueFrom(
|
||||
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||
{
|
||||
dashboard_id: dashboardId,
|
||||
exp: '15m',
|
||||
metabase_customer_name: 'dadosferatech',
|
||||
},
|
||||
metadata
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async enableEnforceMfa(id: string, enabled: boolean) {
|
||||
return await lastValueFrom(
|
||||
this.customerService.CustomerUpdateEnforceMfa({
|
||||
customerId: id,
|
||||
enforceMfa: enabled
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async updateOrganizationInfo(
|
||||
customerId: string,
|
||||
data: {
|
||||
companyName: string;
|
||||
companySite: string;
|
||||
domain: string;
|
||||
cnpj: string;
|
||||
description: string;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const result = await lastValueFrom(
|
||||
this.customerService.OrganizationUpdate({
|
||||
customerId,
|
||||
companyName: data.companyName || '',
|
||||
companySite: data.companySite || '',
|
||||
domain: data.domain || '',
|
||||
cnpj: data.cnpj || '',
|
||||
description: data.description || '',
|
||||
}),
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
|
||||
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
|
||||
else throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrganizationInfo(customerId: string) {
|
||||
try {
|
||||
const customerResponse = await lastValueFrom(
|
||||
this.customerService.CustomerFindOneById({ id: customerId })
|
||||
);
|
||||
|
||||
const customer = customerResponse.customer;
|
||||
|
||||
return {
|
||||
companyName: customer.companyName || '',
|
||||
companySite: customer.companySite || '',
|
||||
domain: customer.domain || '',
|
||||
cnpj: customer.cnpj || '',
|
||||
description: customer.description || ''
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
|
||||
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
|
||||
else throw err;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CustomerLinkItem {
|
||||
@ApiProperty()
|
||||
href: string;
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
@ApiPropertyOptional()
|
||||
iconSrc?: string;
|
||||
}
|
||||
|
||||
export class CustomerSidebarLinkItem {
|
||||
@ApiProperty()
|
||||
type: 'link';
|
||||
@ApiProperty({ type: Object })
|
||||
title: Record<string, string>;
|
||||
@ApiProperty()
|
||||
link: string;
|
||||
@ApiPropertyOptional()
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export class CustomerSidebarMenuItem {
|
||||
@ApiProperty()
|
||||
type: 'menu';
|
||||
@ApiProperty({ type: Object })
|
||||
title: Record<string, string>;
|
||||
@ApiPropertyOptional()
|
||||
icon?: string;
|
||||
@ApiProperty({ type: [CustomerSidebarLinkItem] })
|
||||
items: CustomerSidebarLinkItem[];
|
||||
}
|
||||
|
||||
export class CustomerSidebarSection {
|
||||
@ApiProperty({ type: Object })
|
||||
title: Record<string, string>;
|
||||
@ApiProperty({
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [
|
||||
{ $ref: '#/components/schemas/CustomerSidebarMenuItem' },
|
||||
{ $ref: '#/components/schemas/CustomerSidebarLinkItem' },
|
||||
],
|
||||
},
|
||||
})
|
||||
items: (CustomerSidebarMenuItem | CustomerSidebarLinkItem)[];
|
||||
}
|
||||
|
||||
export class CustomerLinksConfig {
|
||||
@ApiPropertyOptional({ type: [CustomerLinkItem] })
|
||||
home?: CustomerLinkItem[];
|
||||
@ApiPropertyOptional({ type: [CustomerSidebarSection] })
|
||||
sidebar?: CustomerSidebarSection[];
|
||||
}
|
||||
|
||||
export class CustomerLinkRequest {
|
||||
@ApiProperty({ type: CustomerLinksConfig })
|
||||
links: CustomerLinksConfig;
|
||||
}
|
||||
|
||||
export class CustomerLinksResponse {
|
||||
@ApiPropertyOptional({ type: CustomerLinksConfig })
|
||||
links?: CustomerLinksConfig;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class EnforceMfa {
|
||||
@ApiProperty()
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class OrganizationUpdateRequest {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
@ApiPropertyOptional()
|
||||
companySite: string;
|
||||
@ApiProperty()
|
||||
domain: string;
|
||||
@ApiPropertyOptional()
|
||||
info: string;
|
||||
@ApiPropertyOptional()
|
||||
cnpj: string;
|
||||
}
|
||||
|
||||
export class OrganizationResponse {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
@ApiPropertyOptional()
|
||||
companySite: string;
|
||||
@ApiProperty()
|
||||
domain: string;
|
||||
@ApiPropertyOptional()
|
||||
info: string;
|
||||
@ApiPropertyOptional()
|
||||
cnpj: string;
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
ProtoPaths,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
|
||||
const isLocalConnection =
|
||||
process.env.DUC_URL.startsWith('duc:') ||
|
||||
process.env.DUC_URL.includes('0.0.0.0');
|
||||
|
||||
export class DucClient {
|
||||
public name = 'DucClient';
|
||||
|
||||
@@ -17,10 +21,7 @@ export class DucClient {
|
||||
options: {
|
||||
url: process.env.DUC_URL,
|
||||
package: [ProtoPackages.WritePackage, ProtoPackages.ReadPackage],
|
||||
credentials:
|
||||
process.env.LOCAL_ENV || process.env.ENV === 'local'
|
||||
? undefined
|
||||
: credentials.createSsl(),
|
||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||
protoPath: [ProtoPaths.WriteFilePath, ProtoPaths.ReadFilePath],
|
||||
loader: {
|
||||
keepCase: true,
|
||||
@@ -28,6 +29,8 @@ export class DucClient {
|
||||
objects: true,
|
||||
arrays: true,
|
||||
},
|
||||
maxSendMessageLength: 15 * 1024 * 1024, // 15 MB por mensagem
|
||||
maxReceiveMessageLength: 15 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CreateIdentityProvider {
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
clientId: string;
|
||||
|
||||
@ApiProperty()
|
||||
clientSecret: string;
|
||||
|
||||
@ApiProperty()
|
||||
issuerUrl: string;
|
||||
|
||||
@ApiProperty()
|
||||
permissions: number[];
|
||||
}
|
||||
|
||||
|
||||
export class IdentityProviderResponse {
|
||||
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
clientId: string;
|
||||
|
||||
@ApiProperty()
|
||||
issueUrl: string;
|
||||
|
||||
@ApiProperty()
|
||||
permissions: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class IdentityProviderListResponse {
|
||||
|
||||
@ApiProperty()
|
||||
providers: IdentityProviderResponse[]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export class SsoSignInDto {
|
||||
readonly nonce: string;
|
||||
readonly codeVerifier: string;
|
||||
readonly state: string;
|
||||
readonly id: string;
|
||||
readonly clientId: string;
|
||||
readonly clientSecret: string;
|
||||
readonly issuerUrl: string;
|
||||
readonly redirectUrls: string[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user