mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
43 Commits
server-adm
...
log-github
Author | SHA1 | Date | |
---|---|---|---|
|
5a3aa3d608 | ||
|
95b327de50 | ||
|
a3c36f82f3 | ||
|
42612da57d | ||
|
f63c07d538 | ||
|
98a08d136e | ||
|
6c74b875f3 | ||
|
793cd4c144 | ||
|
ec0be1166f | ||
|
899d01237c | ||
|
ff5dbe74fd | ||
|
24004084f2 | ||
|
0e401ece73 | ||
|
c4e1651df7 | ||
|
514c7596db | ||
|
9fbdede82c | ||
|
e519637e89 | ||
|
ba393b0498 | ||
|
4150f81d83 | ||
|
a45bba8537 | ||
|
fe7e8e7240 | ||
|
cf54365022 | ||
|
4f26365c21 | ||
|
c974df104e | ||
|
e88fdc957e | ||
|
55e5360dd4 | ||
|
de2c1c5560 | ||
|
52f773c647 | ||
|
79de7c5f08 | ||
|
3877fe524d | ||
|
4c5df70790 | ||
|
5645dd2b8d | ||
|
0d55195561 | ||
|
1c0caab469 | ||
|
ed9dfd2974 | ||
|
7f72037d77 | ||
|
9928ca17ea | ||
|
2cbd66e804 | ||
|
149cecd805 | ||
|
c80fd55a74 | ||
|
93e7723b48 | ||
|
4a55ecbe12 | ||
|
1e29d550be |
189
backend/package-lock.json
generated
189
backend/package-lock.json
generated
@@ -38,6 +38,7 @@
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
@@ -9777,18 +9778,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9835,11 +9824,6 @@
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
|
||||
@@ -9855,18 +9839,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9905,11 +9877,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
|
||||
@@ -9924,18 +9891,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9974,11 +9929,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz",
|
||||
@@ -9994,18 +9944,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -10044,11 +9982,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
|
||||
@@ -10102,32 +10035,38 @@
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
|
||||
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
|
||||
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"@octokit/types": "^14.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
@@ -10159,6 +10098,12 @@
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/oauth-authorization-url": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
|
||||
@@ -10181,18 +10126,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -10231,11 +10164,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
|
||||
@@ -10376,31 +10304,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "13.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
|
||||
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
|
||||
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "13.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
|
||||
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "20.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
|
||||
@@ -18288,7 +18239,8 @@
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
|
||||
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ=="
|
||||
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "3.0.1",
|
||||
@@ -24776,6 +24728,12 @@
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/octokit-auth-probot/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/odbc": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
|
||||
@@ -30705,9 +30663,10 @@
|
||||
"integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
|
@@ -158,6 +158,7 @@
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -12,6 +12,8 @@ import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certifi
|
||||
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
|
||||
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { TServerSentEventsService } from "@app/ee/services/event/event-sse-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
@@ -296,6 +298,8 @@ declare module "fastify" {
|
||||
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
||||
pkiTemplate: TPkiTemplatesServiceFactory;
|
||||
reminder: TReminderServiceFactory;
|
||||
bus: TEventBusService;
|
||||
sse: TServerSentEventsService;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Reminder, "fromDate"))) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.timestamp("fromDate", { useTz: true }).nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.dropColumn("fromDate");
|
||||
});
|
||||
}
|
||||
}
|
@@ -14,7 +14,8 @@ export const RemindersSchema = z.object({
|
||||
repeatDays: z.number().nullable().optional(),
|
||||
nextReminderDate: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
fromDate: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TReminders = z.infer<typeof RemindersSchema>;
|
||||
|
@@ -1,8 +1,10 @@
|
||||
// weird commonjs-related error in the CI requires us to do the import like this
|
||||
import knex from "knex";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAuditLogs } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -150,43 +152,70 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
|
||||
// delete all audit log that have expired
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
const runPrune = async (dbClient: knex.Knex) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
.select("id")
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
.select("id")
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await dbClient(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
await runPrune(tx);
|
||||
} else {
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
await runPrune(trx);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, pruneAuditLog, find };
|
||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||
const config = getConfig();
|
||||
|
||||
if (config.DISABLE_AUDIT_LOG_STORAGE) {
|
||||
return {
|
||||
...tx,
|
||||
id: uuidv4(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
return auditLogOrm.create(tx);
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, create, pruneAuditLog, find };
|
||||
};
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { TopicName, toPublishableEvent } from "@app/ee/services/event/types";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -21,6 +22,7 @@ type TAuditLogQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
eventBusService: TEventBusService;
|
||||
};
|
||||
|
||||
export type TAuditLogQueueServiceFactory = {
|
||||
@@ -36,133 +38,17 @@ export const auditLogQueueServiceFactory = async ({
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
auditLogStreamDAL,
|
||||
eventBusService
|
||||
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) {
|
||||
await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, {
|
||||
retryLimit: 10,
|
||||
retryBackoff: true
|
||||
});
|
||||
} else {
|
||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (appCfg.SHOULD_INIT_PG_QUEUE) {
|
||||
await queueService.startPg<QueueName.AuditLog>(
|
||||
QueueJobs.AuditLog,
|
||||
async ([job]) => {
|
||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||
let { orgId } = job.data;
|
||||
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
let project;
|
||||
|
||||
if (!orgId) {
|
||||
// it will never be undefined for both org and project id
|
||||
// TODO(akhilmhdh): use caching here in dal to avoid db calls
|
||||
project = await projectDAL.findById(projectId as string);
|
||||
orgId = project.orgId;
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan.auditLogsRetentionDays === 0) {
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
return;
|
||||
}
|
||||
|
||||
// For project actions, set TTL to project-level audit log retention config
|
||||
// This condition ensures that the plan's audit log retention days cannot be bypassed
|
||||
const ttlInDays =
|
||||
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
|
||||
? project.auditLogsRetentionDays
|
||||
: plan.auditLogsRetentionDays;
|
||||
|
||||
const ttl = ttlInDays * MS_IN_DAY;
|
||||
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
projectId,
|
||||
projectName: project?.name,
|
||||
ipAddress,
|
||||
orgId,
|
||||
eventType: event.type,
|
||||
expiresAt: new Date(Date.now() + ttl),
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 30,
|
||||
pollingIntervalSeconds: 0.5
|
||||
}
|
||||
);
|
||||
}
|
||||
removeOnComplete: true
|
||||
});
|
||||
};
|
||||
|
||||
queueService.start(QueueName.AuditLog, async (job) => {
|
||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||
@@ -178,88 +64,97 @@ export const auditLogQueueServiceFactory = async ({
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan.auditLogsRetentionDays === 0) {
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
return;
|
||||
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
if (plan.auditLogsRetentionDays !== 0) {
|
||||
// For project actions, set TTL to project-level audit log retention config
|
||||
// This condition ensures that the plan's audit log retention days cannot be bypassed
|
||||
const ttlInDays =
|
||||
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
|
||||
? project.auditLogsRetentionDays
|
||||
: plan.auditLogsRetentionDays;
|
||||
|
||||
const ttl = ttlInDays * MS_IN_DAY;
|
||||
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
projectId,
|
||||
projectName: project?.name,
|
||||
ipAddress,
|
||||
orgId,
|
||||
eventType: event.type,
|
||||
expiresAt: new Date(Date.now() + ttl),
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// For project actions, set TTL to project-level audit log retention config
|
||||
// This condition ensures that the plan's audit log retention days cannot be bypassed
|
||||
const ttlInDays =
|
||||
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
|
||||
? project.auditLogsRetentionDays
|
||||
: plan.auditLogsRetentionDays;
|
||||
const publishable = toPublishableEvent(event);
|
||||
|
||||
const ttl = ttlInDays * MS_IN_DAY;
|
||||
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
projectId,
|
||||
projectName: project?.name,
|
||||
ipAddress,
|
||||
orgId,
|
||||
eventType: event.type,
|
||||
expiresAt: new Date(Date.now() + ttl),
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
if (publishable) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: publishable.data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
83
backend/src/ee/services/event/event-bus-service.ts
Normal file
83
backend/src/ee/services/event/event-bus-service.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import Redis from "ioredis";
|
||||
import { z } from "zod";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventSchema, TopicName } from "./types";
|
||||
|
||||
export const eventBusFactory = (redis: Redis) => {
|
||||
const publisher = redis.duplicate();
|
||||
// Duplicate the publisher to create a subscriber.
|
||||
// This is necessary because Redis does not allow a single connection to both publish and subscribe.
|
||||
const subscriber = publisher.duplicate();
|
||||
|
||||
const init = async (topics: TopicName[] = Object.values(TopicName)) => {
|
||||
subscriber.on("error", (e) => {
|
||||
logger.error(e, "Event Bus subscriber error");
|
||||
});
|
||||
|
||||
publisher.on("error", (e) => {
|
||||
logger.error(e, "Event Bus publisher error");
|
||||
});
|
||||
|
||||
await subscriber.subscribe(...topics);
|
||||
};
|
||||
|
||||
/**
|
||||
* Publishes an event to the specified topic.
|
||||
* @param topic - The topic to publish the event to.
|
||||
* @param event - The event data to publish.
|
||||
*/
|
||||
const publish = async <T extends z.input<typeof EventSchema>>(topic: TopicName, event: T) => {
|
||||
const json = JSON.stringify(event);
|
||||
|
||||
return publisher.publish(topic, json, (err) => {
|
||||
if (err) {
|
||||
return logger.error(err, `Error publishing to channel ${topic}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param fn - The function to call when a message is received.
|
||||
* It should accept the parsed event data as an argument.
|
||||
* @template T - The type of the event data, which should match the schema defined in EventSchema.
|
||||
* @returns A function that can be called to unsubscribe from the event bus.
|
||||
*/
|
||||
const subscribe = <T extends z.infer<typeof EventSchema>>(fn: (data: T) => Promise<void> | void) => {
|
||||
// Not using async await cause redis client's `on` method does not expect async listeners.
|
||||
const listener = (channel: string, message: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(message) as T;
|
||||
const thenable = fn(parsed);
|
||||
|
||||
// If the function returns a Promise, catch any errors that occur during processing.
|
||||
if (thenable instanceof Promise) {
|
||||
thenable.catch((error) => {
|
||||
logger.error(error, `Error processing message from channel ${channel}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, `Error parsing message data from channel ${channel}`);
|
||||
}
|
||||
};
|
||||
subscriber.on("message", listener);
|
||||
|
||||
return () => {
|
||||
subscriber.off("message", listener);
|
||||
};
|
||||
};
|
||||
|
||||
const close = async () => {
|
||||
try {
|
||||
await publisher.quit();
|
||||
await subscriber.quit();
|
||||
} catch (error) {
|
||||
logger.error(error, "Error closing event bus connections");
|
||||
}
|
||||
};
|
||||
|
||||
return { init, publish, subscribe, close };
|
||||
};
|
||||
|
||||
export type TEventBusService = ReturnType<typeof eventBusFactory>;
|
164
backend/src/ee/services/event/event-sse-service.ts
Normal file
164
backend/src/ee/services/event/event-sse-service.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/* eslint-disable no-continue */
|
||||
import { subject } from "@casl/ability";
|
||||
import Redis from "ioredis";
|
||||
|
||||
import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TEventBusService } from "./event-bus-service";
|
||||
import { createEventStreamClient, EventStreamClient, IEventStreamClientOpts } from "./event-sse-stream";
|
||||
import { EventData, RegisteredEvent, toBusEventName } from "./types";
|
||||
|
||||
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||
|
||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||
let heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const clients = new Set<EventStreamClient>();
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
for (const client of clients) {
|
||||
if (client.stream.closed) continue;
|
||||
void client.ping();
|
||||
}
|
||||
}, HEART_BEAT_INTERVAL);
|
||||
|
||||
const refreshInterval = setInterval(() => {
|
||||
for (const client of clients) {
|
||||
if (client.stream.closed) continue;
|
||||
void client.refresh();
|
||||
}
|
||||
}, AUTH_REFRESH_INTERVAL);
|
||||
|
||||
const removeActiveConnection = async (projectId: string, identityId: string, connectionId: string) => {
|
||||
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, identityId);
|
||||
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, identityId, connectionId);
|
||||
|
||||
await Promise.all([redis.lrem(set, 0, connectionId), redis.del(key)]);
|
||||
};
|
||||
|
||||
const getActiveConnectionsCount = async (projectId: string, identityId: string) => {
|
||||
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, identityId);
|
||||
const connections = await redis.lrange(set, 0, -1);
|
||||
|
||||
if (connections.length === 0) {
|
||||
return 0; // No active connections
|
||||
}
|
||||
|
||||
const keys = connections.map((c) => KeyStorePrefixes.ActiveSSEConnections(projectId, identityId, c));
|
||||
|
||||
const values = await redis.mget(...keys);
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] === null) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await removeActiveConnection(projectId, identityId, connections[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return redis.llen(set);
|
||||
};
|
||||
|
||||
const onDisconnect = async (client: EventStreamClient) => {
|
||||
try {
|
||||
client.close();
|
||||
clients.delete(client);
|
||||
await removeActiveConnection(client.auth.projectId, client.auth.actorId, client.id);
|
||||
} catch (error) {
|
||||
logger.error(error, "Error during SSE stream disconnection");
|
||||
}
|
||||
};
|
||||
|
||||
function filterEventsForClient(client: EventStreamClient, event: EventData, registered: RegisteredEvent[]) {
|
||||
const eventType = toBusEventName(event.data.eventType);
|
||||
const match = registered.find((r) => r.event === eventType);
|
||||
if (!match) return;
|
||||
|
||||
const item = event.data.payload;
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
if (item.length === 0) return;
|
||||
|
||||
const baseSubject = {
|
||||
eventType,
|
||||
environment: undefined as string | undefined,
|
||||
secretPath: undefined as string | undefined
|
||||
};
|
||||
|
||||
const filtered = item.filter((ev) => {
|
||||
baseSubject.secretPath = ev.secretPath ?? "/";
|
||||
baseSubject.environment = ev.environment;
|
||||
|
||||
return client.matcher.can("subscribe", subject(event.type, baseSubject));
|
||||
});
|
||||
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
return client.send({
|
||||
...event,
|
||||
data: {
|
||||
...event.data,
|
||||
payload: filtered
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For single item
|
||||
const baseSubject = {
|
||||
eventType,
|
||||
secretPath: item.secretPath ?? "/",
|
||||
environment: item.environment
|
||||
};
|
||||
|
||||
if (client.matcher.can("subscribe", subject(event.type, baseSubject))) {
|
||||
client.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
const subscribe = async (
|
||||
opts: IEventStreamClientOpts & {
|
||||
onClose?: () => void;
|
||||
}
|
||||
) => {
|
||||
const client = createEventStreamClient(redis, opts);
|
||||
|
||||
// Set up event listener on event bus
|
||||
const unsubscribe = bus.subscribe((event) => {
|
||||
if (event.type !== opts.type) return;
|
||||
filterEventsForClient(client, event, opts.registered);
|
||||
});
|
||||
|
||||
client.stream.on("close", () => {
|
||||
unsubscribe();
|
||||
void onDisconnect(client); // This will never throw
|
||||
});
|
||||
|
||||
await client.open();
|
||||
|
||||
clients.add(client);
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
|
||||
for (const client of clients) {
|
||||
client.close();
|
||||
}
|
||||
|
||||
clients.clear();
|
||||
};
|
||||
|
||||
return { subscribe, close, getActiveConnectionsCount };
|
||||
};
|
||||
|
||||
export type TServerSentEventsService = ReturnType<typeof sseServiceFactory>;
|
178
backend/src/ee/services/event/event-sse-stream.ts
Normal file
178
backend/src/ee/services/event/event-sse-stream.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { MongoAbility, PureAbility } from "@casl/ability";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import Redis from "ioredis";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
|
||||
import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventData, RegisteredEvent } from "./types";
|
||||
|
||||
export const getServerSentEventsHeaders = () =>
|
||||
({
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/event-stream",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
}) as const;
|
||||
|
||||
type TAuthInfo = {
|
||||
actorId: string;
|
||||
projectId: string;
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
};
|
||||
|
||||
export interface IEventStreamClientOpts {
|
||||
type: ProjectType;
|
||||
registered: RegisteredEvent[];
|
||||
onAuthRefresh: (info: TAuthInfo) => Promise<void> | void;
|
||||
getAuthInfo: () => Promise<TAuthInfo> | TAuthInfo;
|
||||
}
|
||||
|
||||
interface EventMessage {
|
||||
time?: string | number;
|
||||
type: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
function serializeSseEvent(chunk: EventMessage): string {
|
||||
let payload = "";
|
||||
|
||||
if (chunk.time) payload += `id: ${chunk.time}\n`;
|
||||
if (chunk.type) payload += `event: ${chunk.type}\n`;
|
||||
if (chunk.data) payload += `data: ${JSON.stringify(chunk)}\n`;
|
||||
|
||||
return `${payload}\n`;
|
||||
}
|
||||
|
||||
export type EventStreamClient = {
|
||||
id: string;
|
||||
stream: Readable;
|
||||
open: () => Promise<void>;
|
||||
send: (data: EventMessage | EventData) => void;
|
||||
ping: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
close: () => void;
|
||||
get auth(): TAuthInfo;
|
||||
signal: AbortSignal;
|
||||
abort: () => void;
|
||||
matcher: PureAbility;
|
||||
};
|
||||
|
||||
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
||||
const rules = options.registered.map((r) => ({
|
||||
subject: options.type,
|
||||
action: "subscribe",
|
||||
conditions: {
|
||||
eventType: r.event,
|
||||
secretPath: r.conditions?.secretPath ?? "/",
|
||||
environment: r.conditions?.environmentSlug
|
||||
}
|
||||
}));
|
||||
|
||||
const id = `sse-${nanoid()}`;
|
||||
const control = new AbortController();
|
||||
const matcher = new PureAbility(rules, { conditionsMatcher });
|
||||
|
||||
let auth: TAuthInfo | undefined;
|
||||
|
||||
const stream = new Readable({
|
||||
objectMode: true
|
||||
});
|
||||
|
||||
// We will manually push data to the stream
|
||||
stream._read = () => {};
|
||||
|
||||
const send = (data: EventMessage | EventData) => {
|
||||
const chunk = serializeSseEvent(data);
|
||||
if (!stream.push(chunk)) {
|
||||
logger.debug("Backpressure detected: dropped manual event");
|
||||
}
|
||||
};
|
||||
|
||||
stream.on("error", (error: Error) => stream.destroy(error));
|
||||
|
||||
const open = async () => {
|
||||
auth = await options.getAuthInfo();
|
||||
await options.onAuthRefresh(auth);
|
||||
|
||||
const { actorId, projectId } = auth;
|
||||
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, actorId);
|
||||
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, actorId, id);
|
||||
|
||||
await Promise.all([redis.rpush(set, id), redis.set(key, "1", "EX", 60)]);
|
||||
};
|
||||
|
||||
const ping = async () => {
|
||||
if (!auth) return; // Avoid race condition if ping is called before open
|
||||
|
||||
const { actorId, projectId } = auth;
|
||||
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, actorId, id);
|
||||
|
||||
await redis.set(key, "1", "EX", 60);
|
||||
|
||||
stream.push("1");
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (stream.closed) return;
|
||||
stream.push(null);
|
||||
stream.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes the connection's auth permissions
|
||||
* Must be called atleast once when connection is opened
|
||||
*/
|
||||
const refresh = async () => {
|
||||
try {
|
||||
auth = await options.getAuthInfo();
|
||||
await options.onAuthRefresh(auth);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
send({
|
||||
type: "error",
|
||||
data: {
|
||||
...error
|
||||
}
|
||||
});
|
||||
return close();
|
||||
}
|
||||
stream.emit("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
const abort = () => {
|
||||
try {
|
||||
control.abort();
|
||||
} catch (error) {
|
||||
logger.debug(error, "Error aborting SSE stream");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
stream,
|
||||
open,
|
||||
send,
|
||||
ping,
|
||||
refresh,
|
||||
close,
|
||||
signal: control.signal,
|
||||
abort,
|
||||
matcher,
|
||||
get auth() {
|
||||
if (!auth) {
|
||||
throw new Error("Auth info not set");
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
};
|
||||
}
|
125
backend/src/ee/services/event/types.ts
Normal file
125
backend/src/ee/services/event/types.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
|
||||
export enum TopicName {
|
||||
CoreServers = "infisical::core-servers"
|
||||
}
|
||||
|
||||
export enum BusEventName {
|
||||
CreateSecret = "secret:create",
|
||||
UpdateSecret = "secret:update",
|
||||
DeleteSecret = "secret:delete"
|
||||
}
|
||||
|
||||
type PublisableEventTypes =
|
||||
| EventType.CREATE_SECRET
|
||||
| EventType.CREATE_SECRETS
|
||||
| EventType.DELETE_SECRET
|
||||
| EventType.DELETE_SECRETS
|
||||
| EventType.UPDATE_SECRETS
|
||||
| EventType.UPDATE_SECRET;
|
||||
|
||||
export function toBusEventName(input: EventType) {
|
||||
switch (input) {
|
||||
case EventType.CREATE_SECRET:
|
||||
case EventType.CREATE_SECRETS:
|
||||
return BusEventName.CreateSecret;
|
||||
case EventType.UPDATE_SECRET:
|
||||
case EventType.UPDATE_SECRETS:
|
||||
return BusEventName.UpdateSecret;
|
||||
case EventType.DELETE_SECRET:
|
||||
case EventType.DELETE_SECRETS:
|
||||
return BusEventName.DeleteSecret;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const isBulkEvent = (event: Event): event is Extract<Event, { metadata: { secrets: Array<unknown> } }> => {
|
||||
return event.type.endsWith("-secrets"); // Feels so wrong
|
||||
};
|
||||
|
||||
export const toPublishableEvent = (event: Event) => {
|
||||
const name = toBusEventName(event.type);
|
||||
|
||||
if (!name) return null;
|
||||
|
||||
const e = event as Extract<Event, { type: PublisableEventTypes }>;
|
||||
|
||||
if (isBulkEvent(e)) {
|
||||
return {
|
||||
name,
|
||||
isBulk: true,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: e.metadata.secrets.map((s) => ({
|
||||
environment: e.metadata.environment,
|
||||
secretPath: e.metadata.secretPath,
|
||||
...s
|
||||
}))
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
isBulk: false,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: {
|
||||
...e.metadata,
|
||||
environment: e.metadata.environment
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const EventName = z.nativeEnum(BusEventName);
|
||||
|
||||
const EventSecretPayload = z.object({
|
||||
secretPath: z.string().optional(),
|
||||
secretId: z.string(),
|
||||
secretKey: z.string(),
|
||||
environment: z.string()
|
||||
});
|
||||
|
||||
export type EventSecret = z.infer<typeof EventSecretPayload>;
|
||||
|
||||
export const EventSchema = z.object({
|
||||
datacontenttype: z.literal("application/json").optional().default("application/json"),
|
||||
type: z.nativeEnum(ProjectType),
|
||||
source: z.string(),
|
||||
time: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(() => new Date().toISOString()),
|
||||
data: z.discriminatedUnion("eventType", [
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRET, EventType.UPDATE_SECRET, EventType.DELETE_SECRET]),
|
||||
payload: EventSecretPayload
|
||||
}),
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRETS, EventType.UPDATE_SECRETS, EventType.DELETE_SECRETS]),
|
||||
payload: EventSecretPayload.array()
|
||||
})
|
||||
// Add more event types as needed
|
||||
])
|
||||
});
|
||||
|
||||
export type EventData = z.infer<typeof EventSchema>;
|
||||
|
||||
export const EventRegisterSchema = z.object({
|
||||
event: EventName,
|
||||
conditions: z
|
||||
.object({
|
||||
secretPath: z.string().optional().default("/"),
|
||||
environmentSlug: z.string()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type RegisteredEvent = z.infer<typeof EventRegisterSchema>;
|
@@ -59,7 +59,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false,
|
||||
fips: false
|
||||
fips: false,
|
||||
eventSubscriptions: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
@@ -76,6 +76,7 @@ export type TFeatureSet = {
|
||||
enterpriseSecretSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@@ -161,7 +161,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
ProjectPermissionSecretActions.Subscribe
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
@@ -265,7 +266,8 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
ProjectPermissionSecretActions.Subscribe
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
@@ -36,7 +36,8 @@ export enum ProjectPermissionSecretActions {
|
||||
ReadValue = "readValue",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
Delete = "delete",
|
||||
Subscribe = "subscribe"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCmekActions {
|
||||
@@ -204,6 +205,7 @@ export type SecretSubjectFields = {
|
||||
secretPath: string;
|
||||
secretName?: string;
|
||||
secretTags?: string[];
|
||||
eventType?: string;
|
||||
};
|
||||
|
||||
export type SecretFolderSubjectFields = {
|
||||
@@ -483,7 +485,17 @@ const SecretConditionV2Schema = z
|
||||
.object({
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
.partial(),
|
||||
eventType: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
|
@@ -49,6 +49,7 @@ const baseSecretScanningDataSourceQuery = ({
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
@@ -82,6 +83,7 @@ const expandSecretScanningDataSource = <
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
connectionGatewayId,
|
||||
...el
|
||||
} = dataSource;
|
||||
|
||||
@@ -100,7 +102,8 @@ const expandSecretScanningDataSource = <
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
|
||||
gatewayId: connectionGatewayId
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
@@ -46,7 +46,11 @@ export const KeyStorePrefixes = {
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`,
|
||||
ActiveSSEConnectionsSet: (projectId: string, identityId: string) =>
|
||||
`sse-connections:${projectId}:${identityId}` as const,
|
||||
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
|
||||
`sse-connections:${projectId}:${identityId}:${connectionId}` as const
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
|
@@ -2302,6 +2302,9 @@ export const AppConnections = {
|
||||
DIGITAL_OCEAN_APP_PLATFORM: {
|
||||
apiToken: "The API token used to authenticate with Digital Ocean App Platform."
|
||||
},
|
||||
NETLIFY: {
|
||||
accessToken: "The Access token used to authenticate with Netlify."
|
||||
},
|
||||
OKTA: {
|
||||
instanceUrl: "The URL used to access your Okta organization.",
|
||||
apiToken: "The API token used to authenticate with Okta."
|
||||
@@ -2533,6 +2536,13 @@ export const SecretSyncs = {
|
||||
workspaceSlug: "The Bitbucket Workspace slug to sync secrets to.",
|
||||
repositorySlug: "The Bitbucket Repository slug to sync secrets to.",
|
||||
environmentId: "The Bitbucket Deployment Environment uuid to sync secrets to."
|
||||
},
|
||||
NETLIFY: {
|
||||
accountId: "The ID of the Netlify account to sync secrets to.",
|
||||
accountName: "The name of the Netlify account to sync secrets to.",
|
||||
siteName: "The name of the Netlify site to sync secrets to.",
|
||||
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||
context: "The Netlify context to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -59,6 +59,7 @@ const envSchema = z
|
||||
AUDIT_LOGS_DB_ROOT_CERT: zpStr(
|
||||
z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional()
|
||||
),
|
||||
DISABLE_AUDIT_LOG_STORAGE: zodStrBool.default("false").optional().describe("Disable audit log storage"),
|
||||
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
|
||||
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
|
||||
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
|
||||
@@ -482,6 +483,15 @@ export const overwriteSchema: {
|
||||
fields: { key: keyof TEnvConfig; description?: string }[];
|
||||
};
|
||||
} = {
|
||||
auditLogs: {
|
||||
name: "Audit Logs",
|
||||
fields: [
|
||||
{
|
||||
key: "DISABLE_AUDIT_LOG_STORAGE",
|
||||
description: "Disable audit log storage"
|
||||
}
|
||||
]
|
||||
},
|
||||
aws: {
|
||||
name: "AWS",
|
||||
fields: [
|
||||
|
@@ -22,6 +22,7 @@ import { crypto } from "@app/lib/crypto";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueWorkerProfile } from "@app/lib/types";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types";
|
||||
import {
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
TIntegrationSyncPayload,
|
||||
@@ -228,6 +229,7 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||
payload: {
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
iv: string;
|
||||
tag: string;
|
||||
|
@@ -22,6 +22,7 @@ export type TAuthMode =
|
||||
orgId: string;
|
||||
authMethod: AuthMethod;
|
||||
isMfaVerified?: boolean;
|
||||
token: AuthModeJwtTokenPayload;
|
||||
}
|
||||
| {
|
||||
authMode: AuthMode.API_KEY;
|
||||
@@ -30,6 +31,7 @@ export type TAuthMode =
|
||||
userId: string;
|
||||
user: TUsers;
|
||||
orgId: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
authMode: AuthMode.SERVICE_TOKEN;
|
||||
@@ -38,6 +40,7 @@ export type TAuthMode =
|
||||
serviceTokenId: string;
|
||||
orgId: string;
|
||||
authMethod: null;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
authMode: AuthMode.IDENTITY_ACCESS_TOKEN;
|
||||
@@ -47,6 +50,7 @@ export type TAuthMode =
|
||||
orgId: string;
|
||||
authMethod: null;
|
||||
isInstanceAdmin?: boolean;
|
||||
token: TIdentityAccessTokenJwtPayload;
|
||||
}
|
||||
| {
|
||||
authMode: AuthMode.SCIM_TOKEN;
|
||||
@@ -56,7 +60,7 @@ export type TAuthMode =
|
||||
authMethod: null;
|
||||
};
|
||||
|
||||
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
export const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
const apiKey = req.headers?.["x-api-key"];
|
||||
if (apiKey) {
|
||||
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||
@@ -133,7 +137,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
actor,
|
||||
orgId: orgId as string,
|
||||
authMethod: token.authMethod,
|
||||
isMfaVerified: token.isMfaVerified
|
||||
isMfaVerified: token.isMfaVerified,
|
||||
token
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -148,7 +153,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
identityId: identity.identityId,
|
||||
identityName: identity.name,
|
||||
authMethod: null,
|
||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
|
||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
||||
token
|
||||
};
|
||||
if (token?.identityAuth?.oidc) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
@@ -179,7 +185,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
serviceToken,
|
||||
serviceTokenId: serviceToken.id,
|
||||
actor,
|
||||
authMethod: null
|
||||
authMethod: null,
|
||||
token
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -191,7 +198,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
actor,
|
||||
user,
|
||||
orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon!
|
||||
authMethod: null
|
||||
authMethod: null,
|
||||
token: token as string
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
@@ -31,6 +31,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro
|
||||
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { eventBusFactory } from "@app/ee/services/event/event-bus-service";
|
||||
import { sseServiceFactory } from "@app/ee/services/event/event-sse-service";
|
||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
@@ -495,6 +497,9 @@ export const registerRoutes = async (
|
||||
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||
const secretScanningV2DAL = secretScanningV2DALFactory(db);
|
||||
|
||||
const eventBusService = eventBusFactory(server.redis);
|
||||
const sseService = sseServiceFactory(eventBusService, server.redis);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@@ -552,7 +557,8 @@ export const registerRoutes = async (
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
auditLogStreamDAL,
|
||||
eventBusService
|
||||
});
|
||||
|
||||
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||
@@ -1968,6 +1974,7 @@ export const registerRoutes = async (
|
||||
await kmsService.startService();
|
||||
await microsoftTeamsService.start();
|
||||
await dynamicSecretQueueService.init();
|
||||
await eventBusService.init();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -2074,7 +2081,9 @@ export const registerRoutes = async (
|
||||
githubOrgSync: githubOrgSyncConfigService,
|
||||
folderCommit: folderCommitService,
|
||||
secretScanningV2: secretScanningV2Service,
|
||||
reminder: reminderService
|
||||
reminder: reminderService,
|
||||
bus: eventBusService,
|
||||
sse: sseService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@@ -2135,7 +2144,8 @@ export const registerRoutes = async (
|
||||
inviteOnlySignup: z.boolean().optional(),
|
||||
redisConfigured: z.boolean().optional(),
|
||||
secretScanningConfigured: z.boolean().optional(),
|
||||
samlDefaultOrgSlug: z.string().optional()
|
||||
samlDefaultOrgSlug: z.string().optional(),
|
||||
auditLogStorageDisabled: z.boolean().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -2162,7 +2172,8 @@ export const registerRoutes = async (
|
||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||
redisConfigured: cfg.isRedisConfigured,
|
||||
secretScanningConfigured: cfg.isSecretScanningConfigured,
|
||||
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
|
||||
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug,
|
||||
auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE)
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -2190,5 +2201,7 @@ export const registerRoutes = async (
|
||||
server.addHook("onClose", async () => {
|
||||
cronJobs.forEach((job) => job.stop());
|
||||
await telemetryService.flushAll();
|
||||
await eventBusService.close();
|
||||
sseService.close();
|
||||
});
|
||||
};
|
||||
|
@@ -75,6 +75,10 @@ import {
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
|
||||
import {
|
||||
NetlifyConnectionListItemSchema,
|
||||
SanitizedNetlifyConnectionSchema
|
||||
} from "@app/services/app-connection/netlify";
|
||||
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
@@ -145,6 +149,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedChecklyConnectionSchema.options,
|
||||
...SanitizedSupabaseConnectionSchema.options,
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options
|
||||
]);
|
||||
|
||||
@@ -184,6 +189,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
ChecklyConnectionListItemSchema,
|
||||
SupabaseConnectionListItemSchema,
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema
|
||||
]);
|
||||
|
||||
|
@@ -46,7 +46,6 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
@@ -73,9 +72,36 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const projects = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
|
||||
const scripts = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
|
||||
return scripts;
|
||||
}
|
||||
});
|
||||
|
||||
return projects;
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/cloudflare-zones`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const zones = await server.services.appConnection.cloudflare.listZones(connectionId, req.permission);
|
||||
return zones;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -26,6 +26,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
||||
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
||||
@@ -76,5 +77,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Checkly]: registerChecklyConnectionRouter,
|
||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter
|
||||
};
|
||||
|
@@ -0,0 +1,87 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateNetlifyConnectionSchema,
|
||||
SanitizedNetlifyConnectionSchema,
|
||||
UpdateNetlifyConnectionSchema
|
||||
} from "@app/services/app-connection/netlify";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerNetlifyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Netlify,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedNetlifyConnectionSchema,
|
||||
createSchema: CreateNetlifyConnectionSchema,
|
||||
updateSchema: UpdateNetlifyConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accounts: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const accounts = await server.services.appConnection.netlify.listAccounts(connectionId, req.permission);
|
||||
|
||||
return { accounts };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts/:accountId/sites`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid(),
|
||||
accountId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
sites: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId, accountId } = req.params;
|
||||
|
||||
const sites = await server.services.appConnection.netlify.listSites(connectionId, req.permission, accountId);
|
||||
|
||||
return { sites };
|
||||
}
|
||||
});
|
||||
};
|
118
backend/src/server/routes/v1/event-router.ts
Normal file
118
backend/src/server/routes/v1/event-router.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { subject } from "@casl/ability";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
||||
import { EventRegisterSchema } from "@app/ee/services/event/types";
|
||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerEventRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/subscribe/project-events",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectId: z.string().trim(),
|
||||
register: z.array(EventRegisterSchema).max(10)
|
||||
})
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req, reply) => {
|
||||
try {
|
||||
const { sse, permission, identityAccessToken, authToken, license } = req.server.services;
|
||||
|
||||
const plan = await license.getPlan(req.auth.orgId);
|
||||
|
||||
if (!plan.eventSubscriptions) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to use event subscriptions due to plan restriction. Upgrade plan to access enterprise event subscriptions."
|
||||
});
|
||||
}
|
||||
|
||||
const count = await sse.getActiveConnectionsCount(req.body.projectId, req.permission.id);
|
||||
|
||||
if (count >= 5) {
|
||||
throw new RateLimitError({
|
||||
message: `Too many active connections for project ${req.body.projectId}. Please close some connections before opening a new one.`
|
||||
});
|
||||
}
|
||||
|
||||
const client = await sse.subscribe({
|
||||
type: ProjectType.SecretManager,
|
||||
registered: req.body.register,
|
||||
async getAuthInfo() {
|
||||
const ability = await permission.getProjectPermission({
|
||||
actor: req.auth.actor,
|
||||
projectId: req.body.projectId,
|
||||
actionProjectType: ActionProjectType.Any,
|
||||
actorAuthMethod: req.auth.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return { permission: ability.permission, actorId: req.permission.id, projectId: req.body.projectId };
|
||||
},
|
||||
async onAuthRefresh(info) {
|
||||
switch (req.auth.authMode) {
|
||||
case AuthMode.JWT:
|
||||
await authToken.fnValidateJwtIdentity(req.auth.token);
|
||||
break;
|
||||
case AuthMode.IDENTITY_ACCESS_TOKEN:
|
||||
await identityAccessToken.fnValidateIdentityAccessToken(req.auth.token, req.realIp);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported authentication method");
|
||||
}
|
||||
|
||||
req.body.register.forEach((r) => {
|
||||
const allowed = info.permission.can(
|
||||
ProjectPermissionSecretActions.Subscribe,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: r.conditions?.environmentSlug ?? "",
|
||||
secretPath: r.conditions?.secretPath ?? "/",
|
||||
eventType: r.event
|
||||
})
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionDenied",
|
||||
message: `You are not allowed to subscribe on secrets`,
|
||||
details: {
|
||||
event: r.event,
|
||||
environmentSlug: r.conditions?.environmentSlug,
|
||||
secretPath: r.conditions?.secretPath ?? "/"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Switches to manual response and enable SSE streaming
|
||||
reply.hijack();
|
||||
reply.raw.writeHead(200, getServerSentEventsHeaders()).flushHeaders();
|
||||
reply.raw.on("close", client.abort);
|
||||
|
||||
await pipeline(client.stream, reply.raw, { signal: client.signal });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
// If the stream is aborted, we don't need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@@ -13,6 +13,7 @@ import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerEventRouter } from "./event-router";
|
||||
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router";
|
||||
@@ -183,4 +184,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/reminders" }
|
||||
);
|
||||
|
||||
await server.register(registerEventRouter, { prefix: "/events" });
|
||||
};
|
||||
|
@@ -22,6 +22,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
|
||||
message: z.string().trim().max(1024).optional(),
|
||||
repeatDays: z.number().min(1).nullable().optional(),
|
||||
nextReminderDate: z.string().datetime().nullable().optional(),
|
||||
fromDate: z.string().datetime().nullable().optional(),
|
||||
recipients: z.string().array().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
@@ -45,6 +46,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
|
||||
message: req.body.message,
|
||||
repeatDays: req.body.repeatDays,
|
||||
nextReminderDate: req.body.nextReminderDate,
|
||||
fromDate: req.body.fromDate,
|
||||
recipients: req.body.recipients
|
||||
}
|
||||
});
|
||||
|
@@ -21,6 +21,7 @@ import { registerGitLabSyncRouter } from "./gitlab-sync-router";
|
||||
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
|
||||
import { registerHerokuSyncRouter } from "./heroku-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerNetlifySyncRouter } from "./netlify-sync-router";
|
||||
import { registerRailwaySyncRouter } from "./railway-sync-router";
|
||||
import { registerRenderSyncRouter } from "./render-sync-router";
|
||||
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
|
||||
@@ -61,5 +62,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Railway]: registerRailwaySyncRouter,
|
||||
[SecretSync.Checkly]: registerChecklySyncRouter,
|
||||
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
|
||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter
|
||||
};
|
||||
|
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateNetlifySyncSchema,
|
||||
NetlifySyncSchema,
|
||||
UpdateNetlifySyncSchema
|
||||
} from "@app/services/secret-sync/netlify/netlify-sync-schemas";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerNetlifySyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Netlify,
|
||||
server,
|
||||
responseSchema: NetlifySyncSchema,
|
||||
createSchema: CreateNetlifySyncSchema,
|
||||
updateSchema: UpdateNetlifySyncSchema
|
||||
});
|
@@ -44,6 +44,7 @@ import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
|
||||
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
|
||||
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
|
||||
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
|
||||
@@ -82,6 +83,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
RailwaySyncSchema,
|
||||
ChecklySyncSchema,
|
||||
DigitalOceanAppPlatformSyncSchema,
|
||||
NetlifySyncSchema,
|
||||
BitbucketSyncSchema
|
||||
]);
|
||||
|
||||
@@ -114,6 +116,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
RailwaySyncListItemSchema,
|
||||
ChecklySyncListItemSchema,
|
||||
SupabaseSyncListItemSchema,
|
||||
NetlifySyncListItemSchema,
|
||||
BitbucketSyncListItemSchema
|
||||
]);
|
||||
|
||||
|
@@ -34,6 +34,7 @@ export enum AppConnection {
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Netlify = "netlify",
|
||||
Okta = "okta"
|
||||
}
|
||||
|
||||
|
@@ -97,6 +97,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
||||
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
||||
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
|
||||
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
||||
@@ -163,6 +164,7 @@ export const listAppConnectionOptions = () => {
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem(),
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getNetlifyConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@@ -251,7 +253,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
|
||||
@@ -381,6 +384,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
|
@@ -36,6 +36,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Checkly]: "Checkly",
|
||||
[AppConnection.Supabase]: "Supabase",
|
||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta"
|
||||
};
|
||||
|
||||
@@ -75,5 +76,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
@@ -81,6 +81,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
||||
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
|
||||
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
|
||||
import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
@@ -148,6 +150,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@@ -611,6 +614,7 @@ export const appConnectionServiceFactory = ({
|
||||
checkly: checklyConnectionService(connectAppConnectionById),
|
||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@@ -149,6 +149,12 @@ import {
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import {
|
||||
TNetlifyConnection,
|
||||
TNetlifyConnectionConfig,
|
||||
TNetlifyConnectionInput,
|
||||
TValidateNetlifyConnectionCredentialsSchema
|
||||
} from "./netlify";
|
||||
import {
|
||||
TOktaConnection,
|
||||
TOktaConnectionConfig,
|
||||
@@ -245,6 +251,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
| TDigitalOceanConnection
|
||||
| TNetlifyConnection
|
||||
| TOktaConnection
|
||||
);
|
||||
|
||||
@@ -288,6 +295,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
| TDigitalOceanConnectionInput
|
||||
| TNetlifyConnectionInput
|
||||
| TOktaConnectionInput
|
||||
);
|
||||
|
||||
@@ -339,6 +347,7 @@ export type TAppConnectionConfig =
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig
|
||||
| TDigitalOceanConnectionConfig
|
||||
| TNetlifyConnectionConfig
|
||||
| TOktaConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@@ -377,6 +386,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateDigitalOceanCredentialsSchema
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
@@ -10,7 +10,8 @@ import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflarePagesProject,
|
||||
TCloudflareWorkersScript
|
||||
TCloudflareWorkersScript,
|
||||
TCloudflareZone
|
||||
} from "./cloudflare-connection-types";
|
||||
|
||||
export const getCloudflareConnectionListItem = () => {
|
||||
@@ -66,6 +67,27 @@ export const listCloudflareWorkersScripts = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export const listCloudflareZones = async (appConnection: TCloudflareConnection): Promise<TCloudflareZone[]> => {
|
||||
const {
|
||||
credentials: { apiToken }
|
||||
} = appConnection;
|
||||
|
||||
const { data } = await request.get<{ result: { name: string; id: string }[] }>(
|
||||
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.result.map((a) => ({
|
||||
name: a.name,
|
||||
id: a.id
|
||||
}));
|
||||
};
|
||||
|
||||
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
|
||||
const { apiToken, accountId } = config.credentials;
|
||||
|
||||
|
@@ -2,7 +2,11 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listCloudflarePagesProjects, listCloudflareWorkersScripts } from "./cloudflare-connection-fns";
|
||||
import {
|
||||
listCloudflarePagesProjects,
|
||||
listCloudflareWorkersScripts,
|
||||
listCloudflareZones
|
||||
} from "./cloudflare-connection-fns";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
@@ -16,7 +20,6 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||
try {
|
||||
const projects = await listCloudflarePagesProjects(appConnection);
|
||||
|
||||
return projects;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -30,9 +33,8 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
||||
const listWorkersScripts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||
try {
|
||||
const projects = await listCloudflareWorkersScripts(appConnection);
|
||||
|
||||
return projects;
|
||||
const scripts = await listCloudflareWorkersScripts(appConnection);
|
||||
return scripts;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
@@ -42,8 +44,20 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
|
||||
}
|
||||
};
|
||||
|
||||
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
|
||||
try {
|
||||
const zones = await listCloudflareZones(appConnection);
|
||||
return zones;
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to list Cloudflare Zones for Cloudflare connection [connectionId=${connectionId}]`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listPagesProjects,
|
||||
listWorkersScripts
|
||||
listWorkersScripts,
|
||||
listZones
|
||||
};
|
||||
};
|
||||
|
@@ -32,3 +32,8 @@ export type TCloudflarePagesProject = {
|
||||
export type TCloudflareWorkersScript = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TCloudflareZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { request } from "@octokit/request";
|
||||
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import https from "https";
|
||||
import RE2 from "re2";
|
||||
@@ -12,7 +13,6 @@ import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
@@ -30,6 +30,23 @@ export const getGitHubConnectionListItem = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getGitHubInstanceApiUrl = async (config: {
|
||||
credentials: Pick<TGitHubConnectionConfig["credentials"], "host" | "instanceType">;
|
||||
}) => {
|
||||
const host = config.credentials.host || "github.com";
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(host);
|
||||
|
||||
let apiBase: string;
|
||||
if (config.credentials.instanceType === "server") {
|
||||
apiBase = `${host}/api/v3`;
|
||||
} else {
|
||||
apiBase = `api.${host}`;
|
||||
}
|
||||
|
||||
return apiBase;
|
||||
};
|
||||
|
||||
export const requestWithGitHubGateway = async <T>(
|
||||
appConnection: { gatewayId?: string | null },
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||
@@ -73,7 +90,10 @@ export const requestWithGitHubGateway = async <T>(
|
||||
return await httpRequest.request(finalRequestConfig);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data);
|
||||
logger.error(
|
||||
{ message: axiosError.message, data: axiosError.response?.data },
|
||||
"Error during GitHub gateway request:"
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -112,7 +132,10 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) =>
|
||||
const appAuth = createAppAuth({
|
||||
appId,
|
||||
privateKey: appPrivateKey,
|
||||
installationId: appConnection.credentials.installationId
|
||||
installationId: appConnection.credentials.installationId,
|
||||
request: request.defaults({
|
||||
baseUrl: `https://${await getGitHubInstanceApiUrl(appConnection)}`
|
||||
})
|
||||
});
|
||||
|
||||
const { token } = await appAuth({ type: "installation" });
|
||||
@@ -141,7 +164,7 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||
|
||||
const token =
|
||||
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
|
||||
let url: string | null = `https://api.${credentials.host || "github.com"}${path}`;
|
||||
let url: string | null = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||
let results: T[] = [];
|
||||
let i = 0;
|
||||
|
||||
@@ -325,6 +348,8 @@ export const validateGitHubConnectionCredentials = async (
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
logger.error(e, "Unable to verify GitHub connection");
|
||||
|
||||
if (e instanceof BadRequestError) {
|
||||
throw e;
|
||||
}
|
||||
@@ -355,7 +380,7 @@ export const validateGitHubConnectionCredentials = async (
|
||||
};
|
||||
}[];
|
||||
}>(config, gatewayService, {
|
||||
url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`),
|
||||
url: `https://${await getGitHubInstanceApiUrl(config)}/user/installations`,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${tokenResp.data.access_token}`,
|
||||
@@ -377,11 +402,15 @@ export const validateGitHubConnectionCredentials = async (
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return {
|
||||
installationId: credentials.installationId
|
||||
installationId: credentials.installationId,
|
||||
instanceType: credentials.instanceType,
|
||||
host: credentials.host
|
||||
};
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return {
|
||||
accessToken: tokenResp.data.access_token
|
||||
accessToken: tokenResp.data.access_token,
|
||||
instanceType: credentials.instanceType,
|
||||
host: credentials.host
|
||||
};
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
|
@@ -10,26 +10,59 @@ import {
|
||||
|
||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||
|
||||
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required"),
|
||||
host: z.string().trim().optional()
|
||||
});
|
||||
export const GitHubConnectionOAuthInputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required"),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1, "Host is required for server instance type")
|
||||
}),
|
||||
z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required"),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "GitHub App code required"),
|
||||
installationId: z.string().min(1, "GitHub App Installation ID required"),
|
||||
host: z.string().trim().optional()
|
||||
});
|
||||
export const GitHubConnectionAppInputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
code: z.string().trim().min(1, "GitHub App code required"),
|
||||
installationId: z.string().min(1, "GitHub App Installation ID required"),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1, "Host is required for server instance type")
|
||||
}),
|
||||
z.object({
|
||||
code: z.string().trim().min(1, "GitHub App code required"),
|
||||
installationId: z.string().min(1, "GitHub App Installation ID required"),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
host: z.string().trim().optional()
|
||||
});
|
||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
accessToken: z.string(),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1)
|
||||
}),
|
||||
z.object({
|
||||
accessToken: z.string(),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
||||
installationId: z.string(),
|
||||
host: z.string().trim().optional()
|
||||
});
|
||||
export const GitHubConnectionAppOutputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
installationId: z.string(),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1)
|
||||
}),
|
||||
z.object({
|
||||
installationId: z.string(),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
@@ -84,11 +117,17 @@ export const GitHubConnectionSchema = z.intersection(
|
||||
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseGitHubConnectionSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.App),
|
||||
credentials: GitHubConnectionAppOutputCredentialsSchema.pick({})
|
||||
credentials: z.object({
|
||||
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
}),
|
||||
BaseGitHubConnectionSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema.pick({})
|
||||
credentials: z.object({
|
||||
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
4
backend/src/services/app-connection/netlify/index.ts
Normal file
4
backend/src/services/app-connection/netlify/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./netlify-connection-constants";
|
||||
export * from "./netlify-connection-fns";
|
||||
export * from "./netlify-connection-schemas";
|
||||
export * from "./netlify-connection-types";
|
@@ -0,0 +1,3 @@
|
||||
export enum NetlifyConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
import { NetlifyPublicAPI } from "./netlify-connection-public-client";
|
||||
import { TNetlifyConnectionConfig } from "./netlify-connection-types";
|
||||
|
||||
export const getNetlifyConnectionListItem = () => {
|
||||
return {
|
||||
name: "Netlify" as const,
|
||||
app: AppConnection.Netlify as const,
|
||||
methods: Object.values(NetlifyConnectionMethod)
|
||||
};
|
||||
};
|
||||
|
||||
export const validateNetlifyConnectionCredentials = async (config: TNetlifyConnectionConfig) => {
|
||||
try {
|
||||
await NetlifyPublicAPI.healthcheck(config);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
@@ -0,0 +1,261 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxiosError } from "axios";
|
||||
|
||||
import { createRequestClient } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
import { TNetlifyAccount, TNetlifyConnectionConfig, TNetlifySite, TNetlifyVariable } from "./netlify-connection-types";
|
||||
|
||||
export function getNetlifyAuthHeaders(connection: TNetlifyConnectionConfig): Record<string, string> {
|
||||
switch (connection.method) {
|
||||
case NetlifyConnectionMethod.AccessToken:
|
||||
return {
|
||||
Authorization: `Bearer ${connection.credentials.accessToken}`
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported Netlify connection method`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getNetlifyRatelimiter(response: AxiosResponse): {
|
||||
maxAttempts: number;
|
||||
isRatelimited: boolean;
|
||||
wait: () => Promise<void>;
|
||||
} {
|
||||
const wait = (seconds: number = 60) => {
|
||||
return new Promise<void>((res) => {
|
||||
setTimeout(res, seconds * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
let remaining = parseInt(response.headers["X-RateLimit-Remaining"] as string, 10);
|
||||
let isRatelimited = response.status === HttpStatusCode.TooManyRequests;
|
||||
|
||||
if (isRatelimited) {
|
||||
if (Math.round(remaining) > 0) {
|
||||
isRatelimited = true;
|
||||
remaining += 1; // Jitter to ensure we wait at least 1 second
|
||||
} else {
|
||||
remaining = 60;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isRatelimited,
|
||||
wait: () => wait(remaining),
|
||||
maxAttempts: 3
|
||||
};
|
||||
}
|
||||
|
||||
type NetlifyParams = {
|
||||
account_id: string;
|
||||
context_name?: string;
|
||||
site_id?: string;
|
||||
};
|
||||
|
||||
class NetlifyPublicClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = createRequestClient({
|
||||
baseURL: `${IntegrationUrls.NETLIFY_API_URL}/api/v1`,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async send<T>(connection: TNetlifyConnectionConfig, config: AxiosRequestConfig, retryAttempt = 0): Promise<T> {
|
||||
const response = await this.client.request<T>({
|
||||
...config,
|
||||
timeout: 1000 * 60, // 60 seconds timeout
|
||||
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
|
||||
headers: getNetlifyAuthHeaders(connection)
|
||||
});
|
||||
const limiter = getNetlifyRatelimiter(response);
|
||||
|
||||
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
|
||||
await limiter.wait();
|
||||
return this.send(connection, config, retryAttempt + 1);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
healthcheck(connection: TNetlifyConnectionConfig) {
|
||||
switch (connection.method) {
|
||||
case NetlifyConnectionMethod.AccessToken:
|
||||
return this.getNetlifyAccounts(connection);
|
||||
default:
|
||||
throw new Error(`Unsupported Netlify connection method`);
|
||||
}
|
||||
}
|
||||
|
||||
async getVariables(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
limit: number = 50,
|
||||
page: number = 1
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts/${account_id}/env`,
|
||||
params: {
|
||||
...params,
|
||||
limit,
|
||||
page
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async createVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
...variables: TNetlifyVariable[]
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "POST",
|
||||
url: `/accounts/${account_id}/env`,
|
||||
data: variables,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateVariableValue(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: TNetlifyVariable
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "PATCH",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
data: variable,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: TNetlifyVariable
|
||||
) {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "PUT",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
data: variable,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: Pick<TNetlifyVariable, "key">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async upsertVariable(connection: TNetlifyConnectionConfig, params: NetlifyParams, variable: TNetlifyVariable) {
|
||||
const res = await this.getVariable(connection, params, variable);
|
||||
|
||||
if (!res) {
|
||||
return this.createVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
if (res.is_secret) {
|
||||
await this.deleteVariable(connection, params, variable);
|
||||
return this.createVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
return this.updateVariable(connection, params, variable);
|
||||
}
|
||||
|
||||
async deleteVariable(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, ...params }: NetlifyParams,
|
||||
variable: Pick<TNetlifyVariable, "key">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "DELETE",
|
||||
url: `/accounts/${account_id}/env/${variable.key}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteVariableValue(
|
||||
connection: TNetlifyConnectionConfig,
|
||||
{ account_id, value_id, ...params }: NetlifyParams & { value_id: string },
|
||||
variable: Pick<TNetlifyVariable, "key" | "id">
|
||||
) {
|
||||
try {
|
||||
const res = await this.send<TNetlifyVariable>(connection, {
|
||||
method: "DELETE",
|
||||
url: `/accounts/${account_id}/${variable.key}/value/${value_id}`,
|
||||
params
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSites(connection: TNetlifyConnectionConfig, accountId: string) {
|
||||
const res = await this.send<TNetlifySite[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/${accountId}/sites`
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getNetlifyAccounts(connection: TNetlifyConnectionConfig) {
|
||||
const res = await this.send<TNetlifyAccount[]>(connection, {
|
||||
method: "GET",
|
||||
url: `/accounts`
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const NetlifyPublicAPI = new NetlifyPublicClient();
|
@@ -0,0 +1,67 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { NetlifyConnectionMethod } from "./netlify-connection-constants";
|
||||
|
||||
export const NetlifyConnectionMethodSchema = z
|
||||
.nativeEnum(NetlifyConnectionMethod)
|
||||
.describe(AppConnections.CREATE(AppConnection.Netlify).method);
|
||||
|
||||
export const NetlifyConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.NETLIFY.accessToken)
|
||||
});
|
||||
|
||||
const BaseNetlifyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Netlify)
|
||||
});
|
||||
|
||||
export const NetlifyConnectionSchema = BaseNetlifyConnectionSchema.extend({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedNetlifyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseNetlifyConnectionSchema.extend({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateNetlifyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: NetlifyConnectionMethodSchema,
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Netlify).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateNetlifyConnectionSchema = ValidateNetlifyConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Netlify)
|
||||
);
|
||||
|
||||
export const UpdateNetlifyConnectionSchema = z
|
||||
.object({
|
||||
credentials: NetlifyConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Netlify).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Netlify));
|
||||
|
||||
export const NetlifyConnectionListItemSchema = z.object({
|
||||
name: z.literal("Netlify"),
|
||||
app: z.literal(AppConnection.Netlify),
|
||||
methods: z.nativeEnum(NetlifyConnectionMethod).array()
|
||||
});
|
@@ -0,0 +1,42 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { NetlifyPublicAPI } from "./netlify-connection-public-client";
|
||||
import { TNetlifyConnection } from "./netlify-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TNetlifyConnection>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const netlifyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listAccounts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Netlify, connectionId, actor);
|
||||
try {
|
||||
const accounts = await NetlifyPublicAPI.getNetlifyAccounts(appConnection);
|
||||
return accounts;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list accounts on Netlify");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listSites = async (connectionId: string, actor: OrgServiceActor, accountId: string) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Netlify, connectionId, actor);
|
||||
try {
|
||||
const sites = await NetlifyPublicAPI.getSites(appConnection, accountId);
|
||||
return sites;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list sites on Netlify");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listAccounts,
|
||||
listSites
|
||||
};
|
||||
};
|
@@ -0,0 +1,51 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateNetlifyConnectionSchema,
|
||||
NetlifyConnectionSchema,
|
||||
ValidateNetlifyConnectionCredentialsSchema
|
||||
} from "./netlify-connection-schemas";
|
||||
|
||||
export type TNetlifyConnection = z.infer<typeof NetlifyConnectionSchema>;
|
||||
|
||||
export type TNetlifyConnectionInput = z.infer<typeof CreateNetlifyConnectionSchema> & {
|
||||
app: AppConnection.Netlify;
|
||||
};
|
||||
|
||||
export type TValidateNetlifyConnectionCredentialsSchema = typeof ValidateNetlifyConnectionCredentialsSchema;
|
||||
|
||||
export type TNetlifyConnectionConfig = DiscriminativePick<TNetlifyConnection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TNetlifyVariable = {
|
||||
key: string;
|
||||
id?: string; // ID of the variable (present in responses)
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
is_secret?: boolean;
|
||||
scopes?: ("builds" | "functions" | "runtime" | "post_processing")[];
|
||||
values: TNetlifyVariableValue[];
|
||||
};
|
||||
|
||||
export type TNetlifyVariableValue = {
|
||||
id?: string;
|
||||
context?: string; // "all", "dev", "branch-deploy", etc.
|
||||
value?: string; // Omitted in response if `is_secret` is true
|
||||
site_id?: string; // Optional: overrides at site-level
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type TNetlifyAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TNetlifySite = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53"
|
||||
Route53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
}
|
||||
|
@@ -1,19 +1,17 @@
|
||||
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import acme from "acme-client";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
@@ -39,6 +37,8 @@ import {
|
||||
TCreateAcmeCertificateAuthorityDTO,
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme-certificate-authority-types";
|
||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||
|
||||
type TAcmeCertificateAuthorityFnsDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||
@@ -95,74 +95,6 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
||||
};
|
||||
};
|
||||
|
||||
export const route53InsertTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
sha256: CustomAWSHasher,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Set ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "UPSERT",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
||||
|
||||
export const route53DeleteTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Delete ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "DELETE",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
||||
|
||||
export const AcmeCertificateAuthorityFns = ({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
@@ -209,6 +141,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.Cloudflare && appConnection.app !== AppConnection.Cloudflare) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a Cloudflare connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
|
||||
|
||||
@@ -289,6 +227,15 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
dnsProviderConfig.provider === AcmeDnsProvider.Cloudflare &&
|
||||
appConnection.app !== AppConnection.Cloudflare
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a Cloudflare connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(
|
||||
appConnection.app as AppConnection,
|
||||
@@ -443,26 +390,56 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
|
||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
||||
await route53InsertTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
await route53InsertTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.Cloudflare: {
|
||||
await cloudflareInsertTxtRecord(
|
||||
connection as TCloudflareConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
|
||||
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
|
||||
await route53DeleteTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
await route53DeleteTxtRecord(
|
||||
connection as TAwsConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.Cloudflare: {
|
||||
await cloudflareDeleteTxtRecord(
|
||||
connection as TCloudflareConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -0,0 +1,109 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { TCloudflareConnectionConfig } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
export const cloudflareInsertTxtRecord = async (
|
||||
connection: TCloudflareConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiToken }
|
||||
} = connection;
|
||||
|
||||
try {
|
||||
await request.post(
|
||||
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records`,
|
||||
{
|
||||
type: "TXT",
|
||||
name: domain,
|
||||
content: value,
|
||||
ttl: 60,
|
||||
proxied: false
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const firstErrorMessage = (
|
||||
error.response?.data as {
|
||||
errors?: { message: string }[];
|
||||
}
|
||||
)?.errors?.[0]?.message;
|
||||
if (firstErrorMessage) {
|
||||
throw new Error(firstErrorMessage);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloudflareDeleteTxtRecord = async (
|
||||
connection: TCloudflareConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiToken }
|
||||
} = connection;
|
||||
|
||||
try {
|
||||
const listRecordsResponse = await request.get<{
|
||||
result: { id: string; type: string; name: string; content: string }[];
|
||||
}>(`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
type: "TXT",
|
||||
name: domain,
|
||||
content: value
|
||||
}
|
||||
});
|
||||
|
||||
const dnsRecords = listRecordsResponse.data?.result;
|
||||
|
||||
if (Array.isArray(dnsRecords) && dnsRecords.length > 0) {
|
||||
const recordToDelete = dnsRecords.find(
|
||||
(record) => record.type === "TXT" && record.name === domain && record.content === value
|
||||
);
|
||||
|
||||
if (recordToDelete) {
|
||||
await request.delete(
|
||||
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/zones/${encodeURIComponent(hostedZoneId)}/dns_records/${recordToDelete.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const firstErrorMessage = (
|
||||
error.response?.data as {
|
||||
errors?: { message: string }[];
|
||||
}
|
||||
)?.errors?.[0]?.message;
|
||||
if (firstErrorMessage) {
|
||||
throw new Error(firstErrorMessage);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
@@ -0,0 +1,75 @@
|
||||
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
||||
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
|
||||
export const route53InsertTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
sha256: CustomAWSHasher,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Set ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "UPSERT",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
||||
|
||||
export const route53DeleteTxtRecord = async (
|
||||
connection: TAwsConnectionConfig,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||
const route53Client = new Route53Client({
|
||||
credentials: config.credentials!,
|
||||
region: config.region
|
||||
});
|
||||
|
||||
const command = new ChangeResourceRecordSetsCommand({
|
||||
HostedZoneId: hostedZoneId,
|
||||
ChangeBatch: {
|
||||
Comment: "Delete ACME challenge TXT record",
|
||||
Changes: [
|
||||
{
|
||||
Action: "DELETE",
|
||||
ResourceRecordSet: {
|
||||
Name: domain,
|
||||
Type: "TXT",
|
||||
TTL: 30,
|
||||
ResourceRecords: [{ Value: value }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await route53Client.send(command);
|
||||
};
|
@@ -15,10 +15,15 @@ export const validateAltNameField = z
|
||||
.trim()
|
||||
.refine(
|
||||
(name) => {
|
||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
return (
|
||||
isFQDN(name, { allow_wildcard: true, require_tld: false }) ||
|
||||
z.string().url().safeParse(name).success ||
|
||||
z.string().email().safeParse(name).success ||
|
||||
isValidIp(name)
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "SAN must be a valid hostname, email address, or IP address"
|
||||
message: "SAN must be a valid hostname, email address, IP address or URL"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -39,10 +44,15 @@ export const validateAltNamesField = z
|
||||
if (data === "") return true;
|
||||
// Split and validate each alt name
|
||||
return data.split(", ").every((name) => {
|
||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
return (
|
||||
isFQDN(name, { allow_wildcard: true, require_tld: false }) ||
|
||||
z.string().url().safeParse(name).success ||
|
||||
z.string().email().safeParse(name).success ||
|
||||
isValidIp(name)
|
||||
);
|
||||
});
|
||||
},
|
||||
{
|
||||
message: "Each alt name must be a valid hostname or email address"
|
||||
message: "Each alt name must be a valid hostname, email address, IP address or URL"
|
||||
}
|
||||
);
|
||||
|
@@ -152,7 +152,7 @@ export const InternalCertificateAuthorityFns = ({
|
||||
extensions.push(extendedKeyUsagesExtension);
|
||||
}
|
||||
|
||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
|
||||
|
||||
if (subscriber.subjectAlternativeNames?.length) {
|
||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||
@@ -160,10 +160,18 @@ export const InternalCertificateAuthorityFns = ({
|
||||
return { type: "email", value: altName };
|
||||
}
|
||||
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
|
||||
return { type: "dns", value: altName };
|
||||
}
|
||||
|
||||
if (z.string().url().safeParse(altName).success) {
|
||||
return { type: "url", value: altName };
|
||||
}
|
||||
|
||||
if (z.string().ip().safeParse(altName).success) {
|
||||
return { type: "ip", value: altName };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||
});
|
||||
|
||||
@@ -418,7 +426,7 @@ export const InternalCertificateAuthorityFns = ({
|
||||
);
|
||||
}
|
||||
|
||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
|
||||
|
||||
if (altNames) {
|
||||
altNamesArray = altNames.split(",").map((altName) => {
|
||||
@@ -426,10 +434,18 @@ export const InternalCertificateAuthorityFns = ({
|
||||
return { type: "email", value: altName };
|
||||
}
|
||||
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
|
||||
return { type: "dns", value: altName };
|
||||
}
|
||||
|
||||
if (z.string().url().safeParse(altName).success) {
|
||||
return { type: "url", value: altName };
|
||||
}
|
||||
|
||||
if (z.string().ip().safeParse(altName).success) {
|
||||
return { type: "ip", value: altName };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||
});
|
||||
|
||||
|
@@ -17,25 +17,16 @@ type VaultData = {
|
||||
const vaultFactory = () => {
|
||||
const getMounts = async (request: AxiosInstance) => {
|
||||
const response = await request
|
||||
.get<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
accessor: string;
|
||||
options: {
|
||||
version?: string;
|
||||
} | null;
|
||||
type: string;
|
||||
}
|
||||
>
|
||||
>("/v1/sys/mounts")
|
||||
.get<{
|
||||
data: Record<string, { accessor: string; options: { version?: string } | null; type: string }>;
|
||||
}>("/v1/sys/mounts")
|
||||
.catch((err) => {
|
||||
if (axios.isAxiosError(err)) {
|
||||
logger.error(err.response?.data, "External migration: Failed to get Vault mounts");
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return response.data;
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
const getPaths = async (
|
||||
|
@@ -19,7 +19,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
|
||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { importDataIntoInfisicalFn } from "./external-migration-fns";
|
||||
import { ExternalPlatforms, ImportType, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||
import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||
|
||||
export type TExternalMigrationQueueFactoryDep = {
|
||||
smtpService: TSmtpService;
|
||||
@@ -66,8 +66,8 @@ export const externalMigrationQueueFactory = ({
|
||||
}: TExternalMigrationQueueFactoryDep) => {
|
||||
const startImport = async (dto: {
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
importType: ImportType;
|
||||
iv: string;
|
||||
tag: string;
|
||||
ciphertext: string;
|
||||
@@ -87,14 +87,14 @@ export const externalMigrationQueueFactory = ({
|
||||
};
|
||||
|
||||
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
||||
try {
|
||||
const { data, actorEmail } = job.data;
|
||||
const { data, actorEmail, importType } = job.data;
|
||||
|
||||
try {
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import started",
|
||||
substitutions: {
|
||||
provider: ExternalPlatforms.EnvKey
|
||||
provider: importType
|
||||
},
|
||||
template: SmtpTemplates.ExternalImportStarted
|
||||
});
|
||||
@@ -141,7 +141,7 @@ export const externalMigrationQueueFactory = ({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import successful",
|
||||
substitutions: {
|
||||
provider: ExternalPlatforms.EnvKey
|
||||
provider: importType
|
||||
},
|
||||
template: SmtpTemplates.ExternalImportSuccessful
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export const externalMigrationQueueFactory = ({
|
||||
recipients: [job.data.actorEmail],
|
||||
subjectLine: "Infisical import failed",
|
||||
substitutions: {
|
||||
provider: ExternalPlatforms.EnvKey,
|
||||
provider: importType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
||||
error: (err as any)?.message || "Unknown error"
|
||||
},
|
||||
|
@@ -6,7 +6,7 @@ import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
|
||||
import { ImportType, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types";
|
||||
import { ExternalPlatforms, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types";
|
||||
|
||||
type TExternalMigrationServiceFactoryDep = {
|
||||
permissionService: TPermissionServiceFactory;
|
||||
@@ -60,8 +60,8 @@ export const externalMigrationServiceFactory = ({
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.EnvKey,
|
||||
data: {
|
||||
importType: ImportType.EnvKey,
|
||||
...encrypted
|
||||
}
|
||||
});
|
||||
@@ -110,8 +110,8 @@ export const externalMigrationServiceFactory = ({
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.Vault,
|
||||
data: {
|
||||
importType: ImportType.Vault,
|
||||
...encrypted
|
||||
}
|
||||
});
|
||||
|
@@ -2,11 +2,6 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
export enum ImportType {
|
||||
EnvKey = "envkey",
|
||||
Vault = "vault"
|
||||
}
|
||||
|
||||
export enum VaultMappingType {
|
||||
Namespace = "namespace",
|
||||
KeyVault = "key-vault"
|
||||
@@ -112,5 +107,6 @@ export type TEnvKeyExportJSON = {
|
||||
};
|
||||
|
||||
export enum ExternalPlatforms {
|
||||
EnvKey = "EnvKey"
|
||||
EnvKey = "EnvKey",
|
||||
Vault = "Vault"
|
||||
}
|
||||
|
@@ -79,25 +79,33 @@ export const reminderServiceFactory = ({
|
||||
repeatDays,
|
||||
nextReminderDate: nextReminderDateInput,
|
||||
recipients,
|
||||
projectId
|
||||
projectId,
|
||||
fromDate: fromDateInput
|
||||
}: {
|
||||
secretId?: string;
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
fromDate?: string | null;
|
||||
projectId: string;
|
||||
}) => {
|
||||
if (!secretId) {
|
||||
throw new BadRequestError({ message: "secretId is required" });
|
||||
}
|
||||
let nextReminderDate;
|
||||
let fromDate;
|
||||
if (nextReminderDateInput) {
|
||||
nextReminderDate = new Date(nextReminderDateInput);
|
||||
}
|
||||
|
||||
if (repeatDays && repeatDays > 0) {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
if (repeatDays) {
|
||||
if (fromDateInput) {
|
||||
fromDate = new Date(fromDateInput);
|
||||
nextReminderDate = fromDate;
|
||||
} else {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextReminderDate) {
|
||||
@@ -112,7 +120,8 @@ export const reminderServiceFactory = ({
|
||||
await reminderDAL.updateById(existingReminder.id, {
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
});
|
||||
reminderId = existingReminder.id;
|
||||
} else {
|
||||
@@ -121,7 +130,8 @@ export const reminderServiceFactory = ({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
});
|
||||
reminderId = newReminder.id;
|
||||
}
|
||||
@@ -280,14 +290,28 @@ export const reminderServiceFactory = ({
|
||||
}
|
||||
|
||||
const processedReminders = remindersData.map(
|
||||
({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => {
|
||||
({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate: nextReminderDateInput,
|
||||
recipients,
|
||||
projectId,
|
||||
fromDate: fromDateInput
|
||||
}) => {
|
||||
let nextReminderDate;
|
||||
let fromDate;
|
||||
if (nextReminderDateInput) {
|
||||
nextReminderDate = new Date(nextReminderDateInput);
|
||||
}
|
||||
|
||||
if (repeatDays && repeatDays > 0 && !nextReminderDate) {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
if (repeatDays && !nextReminderDate) {
|
||||
if (fromDateInput) {
|
||||
fromDate = new Date(fromDateInput);
|
||||
nextReminderDate = fromDate;
|
||||
} else {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextReminderDate) {
|
||||
@@ -302,17 +326,19 @@ export const reminderServiceFactory = ({
|
||||
repeatDays,
|
||||
nextReminderDate,
|
||||
recipients: recipients ? [...new Set(recipients)] : [],
|
||||
projectId
|
||||
projectId,
|
||||
fromDate
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const newReminders = await reminderDAL.insertMany(
|
||||
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({
|
||||
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, fromDate }) => ({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
@@ -8,6 +8,7 @@ export type TReminder = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate: Date;
|
||||
fromDate?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -21,6 +22,7 @@ export type TCreateReminderDTO = {
|
||||
secretId?: string;
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
fromDate?: string | null;
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
};
|
||||
@@ -31,6 +33,7 @@ export type TBatchCreateReminderDTO = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: string | Date | null;
|
||||
fromDate?: Date | null;
|
||||
recipients?: string[] | null;
|
||||
projectId?: string;
|
||||
}[];
|
||||
@@ -95,6 +98,7 @@ export interface TReminderServiceFactory {
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
projectId: string;
|
||||
fromDate?: string | null;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
created: boolean;
|
||||
|
@@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import {
|
||||
getGitHubAppAuthToken,
|
||||
getGitHubInstanceApiUrl,
|
||||
GitHubConnectionMethod,
|
||||
makePaginatedGitHubRequest,
|
||||
requestWithGitHubGateway
|
||||
@@ -73,7 +74,7 @@ const getPublicKey = async (
|
||||
}
|
||||
|
||||
const response = await requestWithGitHubGateway<TGitHubPublicKey>(connection, gatewayService, {
|
||||
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||
url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
@@ -111,7 +112,7 @@ const deleteSecret = async (
|
||||
}
|
||||
|
||||
await requestWithGitHubGateway(connection, gatewayService, {
|
||||
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||
url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
@@ -157,7 +158,7 @@ const putSecret = async (
|
||||
}
|
||||
|
||||
await requestWithGitHubGateway(connection, gatewayService, {
|
||||
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||
url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
|
4
backend/src/services/secret-sync/netlify/index.ts
Normal file
4
backend/src/services/secret-sync/netlify/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./netlify-sync-constants";
|
||||
export * from "./netlify-sync-fns";
|
||||
export * from "./netlify-sync-schemas";
|
||||
export * from "./netlify-sync-types";
|
@@ -0,0 +1,19 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const NETLIFY_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Netlify",
|
||||
destination: SecretSync.Netlify,
|
||||
connection: AppConnection.Netlify,
|
||||
canImportSecrets: true
|
||||
};
|
||||
|
||||
export enum NetlifySyncContext {
|
||||
All = "all",
|
||||
DeployPreview = "deploy-preview",
|
||||
Production = "production",
|
||||
BranchDeploy = "branch-deploy",
|
||||
Dev = "dev",
|
||||
Branch = "branch"
|
||||
}
|
125
backend/src/services/secret-sync/netlify/netlify-sync-fns.ts
Normal file
125
backend/src/services/secret-sync/netlify/netlify-sync-fns.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-continue */
|
||||
import { NetlifyPublicAPI } from "@app/services/app-connection/netlify/netlify-connection-public-client";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
import type { TNetlifySyncWithCredentials } from "./netlify-sync-types";
|
||||
|
||||
export const NetlifySyncFns = {
|
||||
async getSecrets(secretSync: TNetlifySyncWithCredentials): Promise<TSecretMap> {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context, // Only used in the case of getVariables
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const map = {} as TSecretMap;
|
||||
|
||||
for (const variable of variables) {
|
||||
if (variable.is_secret) continue;
|
||||
|
||||
const latest = variable.values.sort((a, b) => Number(b.created_at) - Number(a.created_at))[0];
|
||||
|
||||
map[variable.key] = {
|
||||
value: latest?.value ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
|
||||
async syncSecrets(secretSync: TNetlifySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const {
|
||||
environment,
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context,
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const existing = Object.fromEntries(variables.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
const entry = secretMap[key];
|
||||
|
||||
await NetlifyPublicAPI.upsertVariable(secretSync.connection, params, {
|
||||
key,
|
||||
is_secret: false,
|
||||
values: [
|
||||
{
|
||||
value: entry.value,
|
||||
context: config.context
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const key of Object.keys(existing)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue, @typescript-eslint/no-unsafe-argument
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await NetlifyPublicAPI.deleteVariable(secretSync.connection, params, {
|
||||
key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TNetlifySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const params = {
|
||||
account_id: config.accountId,
|
||||
context_name: config.context,
|
||||
site_id: config.siteId
|
||||
};
|
||||
|
||||
const variables = await NetlifyPublicAPI.getVariables(secretSync.connection, params);
|
||||
|
||||
const existingSecrets = Object.fromEntries(variables.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const secret of Object.keys(existingSecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await NetlifyPublicAPI.deleteVariable(secretSync.connection, params, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { NetlifySyncContext } from "./netlify-sync-constants";
|
||||
|
||||
const NetlifySyncDestinationConfigSchema = z.object({
|
||||
accountId: z
|
||||
.string()
|
||||
.min(1, "Account ID is required")
|
||||
.max(255, "Account ID must be less than 255 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.accountId),
|
||||
accountName: z
|
||||
.string()
|
||||
.min(1, "Account Name is required")
|
||||
.max(255, "Account Name must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.accountName),
|
||||
siteId: z
|
||||
.string()
|
||||
.min(1, "Site ID is required")
|
||||
.max(255, "Site ID must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.siteId),
|
||||
siteName: z
|
||||
.string()
|
||||
.min(1, "Site Name is required")
|
||||
.max(255, "Site Name must be less than 255 characters")
|
||||
.optional()
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.siteName),
|
||||
context: z.nativeEnum(NetlifySyncContext).optional().describe(SecretSyncs.DESTINATION_CONFIG.NETLIFY.context)
|
||||
});
|
||||
|
||||
const NetlifySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const NetlifySyncSchema = BaseSecretSyncSchema(SecretSync.Netlify, NetlifySyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Netlify),
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateNetlifySyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Netlify,
|
||||
NetlifySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateNetlifySyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Netlify,
|
||||
NetlifySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: NetlifySyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const NetlifySyncListItemSchema = z.object({
|
||||
name: z.literal("Netlify"),
|
||||
connection: z.literal(AppConnection.Netlify),
|
||||
destination: z.literal(SecretSync.Netlify),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TNetlifyConnection } from "@app/services/app-connection/netlify";
|
||||
|
||||
import { CreateNetlifySyncSchema, NetlifySyncListItemSchema, NetlifySyncSchema } from "./netlify-sync-schemas";
|
||||
|
||||
export type TNetlifySyncListItem = z.infer<typeof NetlifySyncListItemSchema>;
|
||||
|
||||
export type TNetlifySync = z.infer<typeof NetlifySyncSchema>;
|
||||
|
||||
export type TNetlifySyncInput = z.infer<typeof CreateNetlifySyncSchema>;
|
||||
|
||||
export type TNetlifySyncWithCredentials = TNetlifySync & {
|
||||
connection: TNetlifyConnection;
|
||||
};
|
@@ -30,6 +30,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
@@ -65,6 +66,7 @@ const expandSecretSync = (
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
connectionGatewayId,
|
||||
...el
|
||||
} = secretSync;
|
||||
|
||||
@@ -83,7 +85,8 @@ const expandSecretSync = (
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
|
||||
gatewayId: connectionGatewayId
|
||||
},
|
||||
folder: folder
|
||||
? {
|
||||
|
@@ -27,6 +27,7 @@ export enum SecretSync {
|
||||
Railway = "railway",
|
||||
Checkly = "checkly",
|
||||
DigitalOceanAppPlatform = "digital-ocean-app-platform",
|
||||
Netlify = "netlify",
|
||||
Bitbucket = "bitbucket"
|
||||
}
|
||||
|
||||
|
@@ -48,6 +48,7 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
||||
import { RailwaySyncFns } from "./railway/railway-sync-fns";
|
||||
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
|
||||
@@ -88,6 +89,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION,
|
||||
[SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
|
||||
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
@@ -269,6 +271,8 @@ export const SecretSyncFns = {
|
||||
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Netlify:
|
||||
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
@@ -381,6 +385,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
secretMap = await DigitalOceanAppPlatformSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Netlify:
|
||||
secretMap = await NetlifySyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Bitbucket:
|
||||
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
@@ -473,6 +480,8 @@ export const SecretSyncFns = {
|
||||
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Netlify:
|
||||
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
|
@@ -30,6 +30,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Railway]: "Railway",
|
||||
[SecretSync.Checkly]: "Checkly",
|
||||
[SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform",
|
||||
[SecretSync.Netlify]: "Netlify",
|
||||
[SecretSync.Bitbucket]: "Bitbucket"
|
||||
};
|
||||
|
||||
@@ -62,6 +63,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Railway]: AppConnection.Railway,
|
||||
[SecretSync.Checkly]: AppConnection.Checkly,
|
||||
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
|
||||
[SecretSync.Netlify]: AppConnection.Netlify,
|
||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket
|
||||
};
|
||||
|
||||
@@ -94,5 +96,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Railway]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Checkly]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Netlify]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
@@ -117,6 +117,7 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import { TNetlifySync, TNetlifySyncInput, TNetlifySyncListItem, TNetlifySyncWithCredentials } from "./netlify";
|
||||
import {
|
||||
TRailwaySync,
|
||||
TRailwaySyncInput,
|
||||
@@ -178,6 +179,7 @@ export type TSecretSync =
|
||||
| TRailwaySync
|
||||
| TChecklySync
|
||||
| TSupabaseSync
|
||||
| TNetlifySync
|
||||
| TBitbucketSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
@@ -209,6 +211,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TChecklySyncWithCredentials
|
||||
| TSupabaseSyncWithCredentials
|
||||
| TDigitalOceanAppPlatformSyncWithCredentials
|
||||
| TNetlifySyncWithCredentials
|
||||
| TBitbucketSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
@@ -240,6 +243,7 @@ export type TSecretSyncInput =
|
||||
| TChecklySyncInput
|
||||
| TSupabaseSyncInput
|
||||
| TDigitalOceanAppPlatformSyncInput
|
||||
| TNetlifySyncInput
|
||||
| TBitbucketSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
@@ -271,6 +275,7 @@ export type TSecretSyncListItem =
|
||||
| TChecklySyncListItem
|
||||
| TSupabaseSyncListItem
|
||||
| TDigitalOceanAppPlatformSyncListItem
|
||||
| TNetlifySyncListItem
|
||||
| TBitbucketSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
projectName: string;
|
||||
@@ -38,18 +40,15 @@ export const AccessApprovalRequestTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
You have a new access approval request pending review for the project <strong>{projectName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
<strong>{requesterFullName}</strong> (
|
||||
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||
{requesterEmail}
|
||||
</Link>
|
||||
) has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
|
||||
<strong>{requesterFullName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>)
|
||||
has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
|
||||
<strong>{environment}</strong> environment.
|
||||
</Text>
|
||||
|
||||
{isTemporary && (
|
||||
<Text className="text-[14px] text-red-500 leading-[24px]">
|
||||
<Text className="text-[14px] text-red-600 leading-[24px]">
|
||||
<strong>This access will expire {expiresIn} after approval.</strong>
|
||||
</Text>
|
||||
)}
|
||||
@@ -67,13 +66,8 @@ export const AccessApprovalRequestTemplate = ({
|
||||
</Text>
|
||||
)}
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={approvalUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Review Request
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={approvalUrl}>Review Request</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
18
backend/src/services/smtp/emails/BaseButton.tsx
Normal file
18
backend/src/services/smtp/emails/BaseButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Button } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
children: string;
|
||||
};
|
||||
|
||||
export const BaseButton = ({ href, children }: Props) => {
|
||||
return (
|
||||
<Button
|
||||
href={href}
|
||||
className="rounded-[8px] py-[12px] px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -16,23 +16,21 @@ export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmai
|
||||
<Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px] py-[4px]">
|
||||
<Preview>{preview}</Preview>
|
||||
<Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]">
|
||||
<Section className="border-0 border-b border-[#d1e309] border-solid bg-[#EBF852] mb-[44px] h-[10px] rounded-t-xl" />
|
||||
<Section className="px-[32px] mb-[18px]">
|
||||
<Section className="w-[48px] h-[48px] border border-solid border-gray-300 rounded-full bg-gray-100 mx-auto">
|
||||
<Img
|
||||
src={`https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75`}
|
||||
width="32"
|
||||
alt="Infisical Logo"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Section className="mb-[24px] px-[24px] mt-[24px]">
|
||||
<Img
|
||||
src="https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75"
|
||||
width="36"
|
||||
alt="Infisical Logo"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Hr className=" mb-[32px] mt-[0px] h-[1px]" />
|
||||
<Section className="px-[28px]">{children}</Section>
|
||||
<Hr className=" mt-[32px] mb-[0px] h-[1px]" />
|
||||
<Section className="px-[24px] text-center">
|
||||
<Text className="text-gray-500 text-[12px]">
|
||||
Email sent via{" "}
|
||||
<Link href={siteUrl} className="text-slate-700 no-underline">
|
||||
<Link href={siteUrl} className="text-slate-700 underline decoration-slate-700">
|
||||
Infisical
|
||||
</Link>
|
||||
</Text>
|
||||
|
15
backend/src/services/smtp/emails/BaseLink.tsx
Normal file
15
backend/src/services/smtp/emails/BaseLink.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Link } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
children: string;
|
||||
};
|
||||
|
||||
export const BaseLink = ({ href, children }: Props) => {
|
||||
return (
|
||||
<Link href={href} className="text-slate-700 underline decoration-slate-700">
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
code: string;
|
||||
@@ -25,11 +26,7 @@ export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplatePro
|
||||
<strong>Not you?</strong>{" "}
|
||||
{isCloud ? (
|
||||
<>
|
||||
Contact us at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>{" "}
|
||||
immediately
|
||||
Contact us at <BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink> immediately
|
||||
</>
|
||||
) : (
|
||||
"Contact your administrator immediately"
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface EmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
code: string;
|
||||
@@ -29,10 +30,7 @@ export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerif
|
||||
<strong>Questions about Infisical?</strong>{" "}
|
||||
{isCloud ? (
|
||||
<>
|
||||
Email us at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
Email us at <BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>
|
||||
</>
|
||||
) : (
|
||||
"Contact your administrator"
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface ExternalImportFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
error: string;
|
||||
@@ -21,12 +22,9 @@ export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: Exter
|
||||
</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
If your issue persists, you can contact the Infisical team at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
.
|
||||
<BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>.
|
||||
</Text>
|
||||
<Text className="text-[14px] text-red-500 leading-[24px]">
|
||||
<Text className="text-[14px] text-red-600 leading-[24px]">
|
||||
<strong>Error:</strong> "{error}"
|
||||
</Text>
|
||||
</Section>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface IntegrationSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -30,7 +31,7 @@ export const IntegrationSyncFailedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>{count}</strong> integration(s) failed to sync
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<strong>Project</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
|
||||
<strong>Environment</strong>
|
||||
@@ -38,15 +39,10 @@ export const IntegrationSyncFailedTemplate = ({
|
||||
<strong>Secret Path</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
|
||||
<strong className="text-black">Failure Reason:</strong>
|
||||
<Text className="text-[14px] mt-[4px] text-red-500 leading-[24px]">"{syncMessage}"</Text>
|
||||
<Text className="text-[14px] mt-[4px] text-red-600 leading-[24px]">"{syncMessage}"</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={integrationUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View Integrations
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={integrationUrl}>View Integrations</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface NewDeviceLoginTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
email: string;
|
||||
@@ -42,9 +43,7 @@ export const NewDeviceLoginTemplate = ({
|
||||
<Text className="mb-[0px]">
|
||||
If you believe that this login is suspicious, please contact{" "}
|
||||
{isCloud ? (
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
<BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>
|
||||
) : (
|
||||
"your administrator"
|
||||
)}{" "}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
email: string;
|
||||
@@ -35,10 +36,7 @@ export const OrgAdminBreakglassAccessTemplate = ({
|
||||
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
|
||||
<Text className="text-[14px]">
|
||||
If you'd like to disable Admin SSO Bypass, please visit{" "}
|
||||
<Link href={`${siteUrl}/organization/settings`} className="text-slate-700 no-underline">
|
||||
Organization Security Settings
|
||||
</Link>
|
||||
.
|
||||
<BaseLink href={`${siteUrl}/organization/settings`}>Organization Security Settings</BaseLink>.
|
||||
</Text>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
|
@@ -2,6 +2,7 @@ import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface OrgAdminProjectGrantAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview"> {
|
||||
email: string;
|
||||
@@ -24,8 +25,8 @@ export const OrgAdminProjectGrantAccessTemplate = ({
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px] mt-[4px]">
|
||||
The organization admin <strong>{email}</strong> has self-issued direct access to the project{" "}
|
||||
<strong>{projectName}</strong>.
|
||||
The organization admin <BaseLink href={`mailto:${email}`}>{email}</BaseLink> has self-issued direct access to
|
||||
the project <strong>{projectName}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||
metadata?: string;
|
||||
@@ -36,15 +38,13 @@ export const OrganizationInvitationTemplate = ({
|
||||
<br />
|
||||
<strong>{organizationName}</strong> on <strong>Infisical</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
{inviterFirstName && inviterUsername ? (
|
||||
<>
|
||||
<strong>{inviterFirstName}</strong> (
|
||||
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
|
||||
{inviterUsername}
|
||||
</Link>
|
||||
) has invited you to collaborate on <strong>{organizationName}</strong>.
|
||||
<BaseLink href={`mailto:${inviterUsername}`}>{inviterUsername}</BaseLink>) has invited you to collaborate
|
||||
on <strong>{organizationName}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -53,13 +53,12 @@ export const OrganizationInvitationTemplate = ({
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
<Section className="text-center">
|
||||
<BaseButton
|
||||
href={`${callback_url}?token=${token}${metadata ? `&metadata=${metadata}` : ""}&to=${encodeURIComponent(email)}&organization_id=${organizationId}`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Accept Invite
|
||||
</Button>
|
||||
</BaseButton>
|
||||
</Section>
|
||||
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||
<Text className="mb-[0px]">
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface PasswordResetTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
email: string;
|
||||
@@ -20,16 +22,13 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>Account Recovery</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">A password reset was requested for your Infisical account.</Text>
|
||||
<Text className="text-[14px]">
|
||||
If you did not initiate this request, please contact{" "}
|
||||
{isCloud ? (
|
||||
<>
|
||||
us immediately at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
us immediately at <BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>
|
||||
</>
|
||||
) : (
|
||||
"your administrator immediately"
|
||||
@@ -37,13 +36,8 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}>Reset Password</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseLink } from "@app/services/smtp/emails/BaseLink";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface PasswordSetupTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -16,19 +18,16 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>Password Setup</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">Someone requested to set up a password for your Infisical account.</Text>
|
||||
<Text className="text-[14px] text-red-500">
|
||||
<Text className="text-[14px] text-red-600">
|
||||
Make sure you are already logged in to Infisical in the current browser before clicking the link below.
|
||||
</Text>
|
||||
<Text className="text-[14px]">
|
||||
If you did not initiate this request, please contact{" "}
|
||||
{isCloud ? (
|
||||
<>
|
||||
us immediately at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
us immediately at <BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>
|
||||
</>
|
||||
) : (
|
||||
"your administrator immediately"
|
||||
@@ -36,7 +35,7 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Section className="text-center">
|
||||
<Button
|
||||
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface ProjectAccessRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
projectName: string;
|
||||
@@ -30,26 +32,17 @@ export const ProjectAccessRequestTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
A user has requested access to the project <strong>{projectName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
<strong>{requesterName}</strong> (
|
||||
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||
{requesterEmail}
|
||||
</Link>
|
||||
) has requested access to the project <strong>{projectName}</strong> in the organization{" "}
|
||||
<strong>{orgName}</strong>.
|
||||
<strong>{requesterName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>) has
|
||||
requested access to the project <strong>{projectName}</strong> in the organization <strong>{orgName}</strong>.
|
||||
</Text>
|
||||
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||
<strong className="text-black">User note:</strong> "{note}"
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={callback_url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Grant Access
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={callback_url}>Grant Access</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface ProjectInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||
@@ -18,18 +19,13 @@ export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
You've been invited to join a project on Infisical
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
You've been invited to join the project <strong>{workspaceName}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={callback_url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Join Project
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={callback_url}>Join Project</BaseButton>
|
||||
</Section>
|
||||
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||
<Text className="mb-[0px]">
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface ScimUserProvisionedTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||
@@ -24,18 +25,13 @@ export const ScimUserProvisionedTemplate = ({
|
||||
<br />
|
||||
<strong>{organizationName}</strong> on <strong>Infisical</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
You've been invited to collaborate on <strong>{organizationName}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={callback_url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Accept Invite
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={callback_url}>Accept Invite</BaseButton>
|
||||
</Section>
|
||||
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||
<Text className="mb-[0px]">
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface SecretApprovalRequestBypassedTemplateProps
|
||||
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -35,13 +37,10 @@ export const SecretApprovalRequestBypassedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
A secret approval request has been bypassed in the project <strong>{projectName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
<strong>{requesterFullName}</strong> (
|
||||
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||
{requesterEmail}
|
||||
</Link>
|
||||
) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
|
||||
<strong>{requesterFullName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>)
|
||||
has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
|
||||
<strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required
|
||||
approval.
|
||||
</Text>
|
||||
@@ -50,13 +49,8 @@ export const SecretApprovalRequestBypassedTemplate = ({
|
||||
{bypassReason}"
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={approvalUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Review Bypass
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={approvalUrl}>Review Bypass</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretApprovalRequestNeedsReviewTemplateProps
|
||||
@@ -27,20 +28,15 @@ export const SecretApprovalRequestNeedsReviewTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
A secret approval request for the project <strong>{projectName}</strong> requires review
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">Hello {firstName},</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
You have a new secret change request pending your review for the project <strong>{projectName}</strong> in the
|
||||
organization <strong>{organizationName}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={approvalUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Review Changes
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={approvalUrl}>Review Changes</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface SecretLeakIncidentTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
numberOfSecrets: number;
|
||||
@@ -24,7 +26,7 @@ export const SecretLeakIncidentTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Infisical has uncovered <strong>{numberOfSecrets}</strong> secret(s) from a recent commit
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
You are receiving this notification because one or more leaked secrets have been detected in a recent commit
|
||||
{(pusher_email || pusher_name) && (
|
||||
@@ -33,11 +35,7 @@ export const SecretLeakIncidentTemplate = ({
|
||||
pushed by <strong>{pusher_name ?? "Unknown Pusher"}</strong>{" "}
|
||||
{pusher_email && (
|
||||
<>
|
||||
(
|
||||
<Link href={`mailto:${pusher_email}`} className="text-slate-700 no-underline">
|
||||
{pusher_email}
|
||||
</Link>
|
||||
)
|
||||
(<BaseLink href={`mailto:${pusher_email}`}>{pusher_email}</BaseLink>)
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -49,24 +47,16 @@ export const SecretLeakIncidentTemplate = ({
|
||||
a comment in the given programming language. This will prevent future notifications from being sent out for
|
||||
these secrets.
|
||||
</Text>
|
||||
<Text className="text-[14px] text-red-500">
|
||||
<Text className="text-[14px] text-red-600">
|
||||
If these are production secrets, please rotate them immediately.
|
||||
</Text>
|
||||
<Text className="text-[14px]">
|
||||
Once you have taken action, be sure to update the status of the risk in the{" "}
|
||||
<Link href={`${siteUrl}/organization/secret-scanning`} className="text-slate-700 no-underline">
|
||||
Infisical Dashboard
|
||||
</Link>
|
||||
.
|
||||
<BaseLink href={`${siteUrl}/organization/secret-scanning`}>Infisical Dashboard</BaseLink>.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={`${siteUrl}/organization/secret-scanning`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View Leaked Secrets
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={`${siteUrl}/organization/secret-scanning`}>View Leaked Secrets</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretRequestCompletedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -20,7 +21,7 @@ export const SecretRequestCompletedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>A secret has been shared with you</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] text-center pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] text-center pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
{respondentUsername ? <strong>{respondentUsername}</strong> : "Someone"} shared a secret{" "}
|
||||
{name && (
|
||||
@@ -31,13 +32,8 @@ export const SecretRequestCompletedTemplate = ({
|
||||
with you.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={secretRequestUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View Secret
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={secretRequestUrl}>View Secret</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretRotationFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -28,7 +29,7 @@ export const SecretRotationFailedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Your <strong>{rotationType}</strong> rotation <strong>{rotationName}</strong> failed to rotate
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<strong>Name</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{rotationName}</Text>
|
||||
<strong>Type</strong>
|
||||
@@ -40,15 +41,12 @@ export const SecretRotationFailedTemplate = ({
|
||||
<strong>Secret Path</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{secretPath}</Text>
|
||||
<strong>Reason:</strong>
|
||||
<Text className="text-[14px] text-red-500 mt-[4px]">{content}</Text>
|
||||
<Text className="text-[14px] text-red-600 mt-[4px]">{content}</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={`${rotationUrl}?search=${rotationName}&secretPath=${secretPath}`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={`${rotationUrl}?search=${rotationName}&secretPath=${secretPath}`}>
|
||||
View in Infisical
|
||||
</Button>
|
||||
</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretScanningScanFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -30,7 +31,7 @@ export const SecretScanningScanFailedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Infisical encountered an error while attempting to scan the resource <strong>{resourceName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<strong>Resource</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{resourceName}</Text>
|
||||
<strong>Data Source</strong>
|
||||
@@ -40,15 +41,10 @@ export const SecretScanningScanFailedTemplate = ({
|
||||
<strong>Timestamp</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
|
||||
<strong>Error</strong>
|
||||
<Text className="text-[14px] text-red-500 mt-[4px]">{errorMessage}</Text>
|
||||
<Text className="text-[14px] text-red-600 mt-[4px]">{errorMessage}</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View in Infisical
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={url}>View in Infisical</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface SecretScanningSecretsDetectedTemplateProps
|
||||
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -32,7 +34,7 @@ export const SecretScanningSecretsDetectedTemplate = ({
|
||||
Infisical has uncovered <strong>{numberOfSecrets}</strong> secret(s)
|
||||
{isDiffScan ? " from a recent commit to" : " in"} <strong>{resourceName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
You are receiving this notification because one or more leaked secrets have been detected
|
||||
{isDiffScan && " in a recent commit"}
|
||||
@@ -43,11 +45,7 @@ export const SecretScanningSecretsDetectedTemplate = ({
|
||||
pushed by <strong>{authorName ?? "Unknown Pusher"}</strong>{" "}
|
||||
{authorEmail && (
|
||||
<>
|
||||
(
|
||||
<Link href={`mailto:${authorEmail}`} className="text-slate-700 no-underline">
|
||||
{authorEmail}
|
||||
</Link>
|
||||
)
|
||||
(<BaseLink href={`mailto:${authorEmail}`}>{authorEmail}</BaseLink>)
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -65,24 +63,16 @@ export const SecretScanningSecretsDetectedTemplate = ({
|
||||
a comment in the given programming language. This will prevent future notifications from being sent out for
|
||||
these secrets.
|
||||
</Text>
|
||||
<Text className="text-[14px] text-red-500">
|
||||
<Text className="text-[14px] text-red-600">
|
||||
If these are production secrets, please rotate them immediately.
|
||||
</Text>
|
||||
<Text className="text-[14px]">
|
||||
Once you have taken action, be sure to update the finding status in the{" "}
|
||||
<Link href={url} className="text-slate-700 no-underline">
|
||||
Infisical Dashboard
|
||||
</Link>
|
||||
.
|
||||
<BaseLink href={url}>Infisical Dashboard</BaseLink>.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View Leaked Secrets
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={url}>View Leaked Secrets</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -28,7 +29,7 @@ export const SecretSyncFailedTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Your <strong>{syncDestination}</strong> sync <strong>{syncName}</strong> failed to complete
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<strong>Name</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{syncName}</Text>
|
||||
<strong>Destination</strong>
|
||||
@@ -50,17 +51,12 @@ export const SecretSyncFailedTemplate = ({
|
||||
{failureMessage && (
|
||||
<>
|
||||
<strong>Reason:</strong>
|
||||
<Text className="text-[14px] text-red-500 mt-[4px]">{failureMessage}</Text>
|
||||
<Text className="text-[14px] text-red-600 mt-[4px]">{failureMessage}</Text>
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={syncUrl}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View in Infisical
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={syncUrl}>View in Infisical</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface ServiceTokenExpiryNoticeTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -24,20 +25,15 @@ export const ServiceTokenExpiryNoticeTemplate = ({
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>Service token expiry notice</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
Your service token <strong>{tokenName}</strong> for the project <strong>{projectName}</strong> will expire
|
||||
within 24 hours.
|
||||
</Text>
|
||||
<Text>If this token is still needed for your workflow, please create a new one before it expires.</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Create New Token
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={url}>Create New Token</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface SignupEmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
code: string;
|
||||
@@ -29,10 +30,7 @@ export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: Sign
|
||||
<strong>Questions about setting up Infisical?</strong>{" "}
|
||||
{isCloud ? (
|
||||
<>
|
||||
Email us at{" "}
|
||||
<Link href="mailto:support@infisical.com" className="text-slate-700 no-underline">
|
||||
support@infisical.com
|
||||
</Link>
|
||||
Email us at <BaseLink href="mailto:support@infisical.com">support@infisical.com</BaseLink>
|
||||
</>
|
||||
) : (
|
||||
"Contact your administrator"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface UnlockAccountTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
@@ -18,19 +19,14 @@ export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAc
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
<strong>Unlock your Infisical account</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
Your account has been temporarily locked due to multiple failed login attempts.
|
||||
</Text>
|
||||
<Text>If these attempts were not made by you, reset your password immediately.</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={`${callback_url}?token=${token}`}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
Unlock Account
|
||||
</Button>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={`${callback_url}?token=${token}`}>Unlock Account</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user