mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): test first milestone one flow
This commit is contained in:
2
backend-pg/.eslintignore
Normal file
2
backend-pg/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.eslintrc.js
|
||||
./scripts
|
||||
@@ -13,12 +13,13 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
rules: {
|
||||
"import/prefer-default-export": "off",
|
||||
"simple-import-sort/exports": "error",
|
||||
"consistent-return": "off", // my style
|
||||
'import/order': 'off', // for simple-import-order
|
||||
"import/prefer-default-export": "off", // why
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"consistent-return": "off",
|
||||
"simple-import-sort/exports": "error",
|
||||
"simple-import-sort/imports": [
|
||||
"warn",
|
||||
{
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
||||
}
|
||||
215
backend-pg/package-lock.json
generated
215
backend-pg/package-lock.json
generated
@@ -9,12 +9,14 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@fastify/cookie": "^9.2.0",
|
||||
"@fastify/cors": "^8.4.1",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
@@ -27,6 +29,7 @@
|
||||
"nodemailer": "^6.9.7",
|
||||
"ora": "^7.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
@@ -37,12 +40,13 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
@@ -63,6 +67,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@casl/ability": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
|
||||
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
|
||||
"dependencies": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -450,9 +465,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
|
||||
"integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
@@ -513,9 +528,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
|
||||
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
@@ -1092,6 +1107,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/picomatch": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz",
|
||||
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prompt-sync": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
|
||||
@@ -1104,15 +1125,15 @@
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
|
||||
"integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
|
||||
"integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.12.0",
|
||||
"@typescript-eslint/type-utils": "6.12.0",
|
||||
"@typescript-eslint/utils": "6.12.0",
|
||||
"@typescript-eslint/visitor-keys": "6.12.0",
|
||||
"@typescript-eslint/scope-manager": "6.13.2",
|
||||
"@typescript-eslint/type-utils": "6.13.2",
|
||||
"@typescript-eslint/utils": "6.13.2",
|
||||
"@typescript-eslint/visitor-keys": "6.13.2",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -1159,14 +1180,14 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
|
||||
"integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz",
|
||||
"integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.12.0",
|
||||
"@typescript-eslint/types": "6.12.0",
|
||||
"@typescript-eslint/typescript-estree": "6.12.0",
|
||||
"@typescript-eslint/visitor-keys": "6.12.0",
|
||||
"@typescript-eslint/scope-manager": "6.13.2",
|
||||
"@typescript-eslint/types": "6.13.2",
|
||||
"@typescript-eslint/typescript-estree": "6.13.2",
|
||||
"@typescript-eslint/visitor-keys": "6.13.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1207,12 +1228,12 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
|
||||
"integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
|
||||
"integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.12.0",
|
||||
"@typescript-eslint/visitor-keys": "6.12.0"
|
||||
"@typescript-eslint/types": "6.13.2",
|
||||
"@typescript-eslint/visitor-keys": "6.13.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1223,12 +1244,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
|
||||
"integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz",
|
||||
"integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.12.0",
|
||||
"@typescript-eslint/utils": "6.12.0",
|
||||
"@typescript-eslint/typescript-estree": "6.13.2",
|
||||
"@typescript-eslint/utils": "6.13.2",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1270,9 +1291,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
|
||||
"integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
|
||||
"integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
@@ -1282,12 +1303,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
|
||||
"integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
|
||||
"integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.12.0",
|
||||
"@typescript-eslint/visitor-keys": "6.12.0",
|
||||
"@typescript-eslint/types": "6.13.2",
|
||||
"@typescript-eslint/visitor-keys": "6.13.2",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1329,16 +1350,16 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
|
||||
"integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz",
|
||||
"integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.12.0",
|
||||
"@typescript-eslint/types": "6.12.0",
|
||||
"@typescript-eslint/typescript-estree": "6.12.0",
|
||||
"@typescript-eslint/scope-manager": "6.13.2",
|
||||
"@typescript-eslint/types": "6.13.2",
|
||||
"@typescript-eslint/typescript-estree": "6.13.2",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1353,11 +1374,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
|
||||
"integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
|
||||
"integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.12.0",
|
||||
"@typescript-eslint/types": "6.13.2",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1368,6 +1389,37 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/core": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
|
||||
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
|
||||
},
|
||||
"node_modules/@ucast/js": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
|
||||
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
|
||||
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo2js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
|
||||
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
@@ -1523,6 +1575,18 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
@@ -2453,14 +2517,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
|
||||
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.3",
|
||||
"@eslint/js": "8.54.0",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.55.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
@@ -2547,9 +2611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
|
||||
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
@@ -4576,6 +4640,17 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
@@ -5192,11 +5267,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
|
||||
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
@@ -5471,6 +5546,18 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||
"dev:docker": "nodemon",
|
||||
"type:check": "tsc --noEmit",
|
||||
"lint:fix": "eslint --fix 'src/**/*.ts'",
|
||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||
@@ -28,12 +28,13 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
@@ -46,12 +47,14 @@
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@fastify/cookie": "^9.2.0",
|
||||
"@fastify/cors": "^8.4.1",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
@@ -64,6 +67,7 @@
|
||||
"nodemailer": "^6.9.7",
|
||||
"ora": "^7.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
|
||||
@@ -16,11 +16,18 @@ const componentType = parseInt(prompt("Select a component: "), 10);
|
||||
if (componentType === 1) {
|
||||
const componentName = prompt("Enter service name: ");
|
||||
const dir = path.join(__dirname, `../src/services/${componentName}`);
|
||||
const capitalizedComponentName = componentName.at(0)?.toUpperCase() + componentName.slice(1);
|
||||
const dalTypeName = `T${capitalizedComponentName}DalFactory`;
|
||||
const dalName = `${componentName}DalFactory`;
|
||||
const serviceTypeName = `T${capitalizedComponentName}ServiceFactory`;
|
||||
const serviceName = `${componentName}ServiceFactory`;
|
||||
const pascalCase = componentName
|
||||
.split("-")
|
||||
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
|
||||
.join("");
|
||||
const camelCase = componentName
|
||||
.split("-")
|
||||
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
|
||||
.join("");
|
||||
const dalTypeName = `T${pascalCase}DalFactory`;
|
||||
const dalName = `${camelCase}DalFactory`;
|
||||
const serviceTypeName = `T${pascalCase}ServiceFactory`;
|
||||
const serviceName = `${camelCase}ServiceFactory`;
|
||||
|
||||
mkdirSync(dir);
|
||||
|
||||
@@ -29,9 +36,9 @@ if (componentType === 1) {
|
||||
`import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export type ${dalTypeName} = {};
|
||||
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
|
||||
|
||||
export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
|
||||
export const ${dalName} = (db: TDbClient) => {
|
||||
|
||||
return { };
|
||||
};
|
||||
@@ -43,12 +50,12 @@ export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
|
||||
`import { ${dalTypeName} } from "./${componentName}-dal";
|
||||
|
||||
type ${serviceTypeName}Dep = {
|
||||
${componentName}Dal: ${dalTypeName};
|
||||
${camelCase}Dal: ${dalTypeName};
|
||||
};
|
||||
|
||||
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
|
||||
|
||||
export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) => {
|
||||
export const ${serviceName} = ({ ${camelCase}Dal }: ${serviceTypeName}Dep) => {
|
||||
return {};
|
||||
};
|
||||
`
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "path";
|
||||
import knex from "knex";
|
||||
import { appendFileSync, readFileSync, writeFileSync } from "fs";
|
||||
import promptSync from "prompt-sync";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
const prompt = promptSync();
|
||||
|
||||
@@ -27,11 +28,11 @@ const getZodPrimitiveType = (type: string) => {
|
||||
case "boolean":
|
||||
return "z.boolean()";
|
||||
case "jsonb":
|
||||
return "z.string()";
|
||||
return "z.unknown()";
|
||||
case "json":
|
||||
return "z.string()";
|
||||
return "z.unknown()";
|
||||
case "timestamp with time zone":
|
||||
return "z.string().datetime()";
|
||||
return "z.date()";
|
||||
case "integer":
|
||||
return "z.number()";
|
||||
case "text":
|
||||
|
||||
23
backend-pg/src/@types/fastify.d.ts
vendored
23
backend-pg/src/@types/fastify.d.ts
vendored
@@ -1,10 +1,16 @@
|
||||
import { TUser } from "@app/db/schemas";
|
||||
import { TAuthDalFactory } from "@app/services/auth/auth-dal";
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { AuthMode } from "@app/services/auth/auth-signup-type";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TServerCfgServiceFactory } from "@app/services/server-cfg/server-cfg-service";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
|
||||
import { TUserDalFactory } from "@app/services/user/user-dal";
|
||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
|
||||
import "fastify";
|
||||
|
||||
@@ -14,13 +20,14 @@ declare module "fastify" {
|
||||
// used for mfa session authentication
|
||||
mfa: {
|
||||
userId: string;
|
||||
user: TUser;
|
||||
user: TUsers;
|
||||
};
|
||||
// identity injection. depending on which kinda of token the information is filled in auth
|
||||
auth: {
|
||||
authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY;
|
||||
userId: string;
|
||||
user: TUser;
|
||||
tokenVersionId: string; // the session id of token used
|
||||
user: TUsers;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,12 +37,18 @@ declare module "fastify" {
|
||||
password: TAuthPasswordFactory;
|
||||
signup: TAuthSignupFactory;
|
||||
authToken: TAuthTokenServiceFactory;
|
||||
permission: TPermissionServiceFactory;
|
||||
org: TOrgServiceFactory;
|
||||
orgRole: TOrgRoleServiceFactory;
|
||||
serverCfg: TServerCfgServiceFactory;
|
||||
user: TUserServiceFactory;
|
||||
apiKey: TApiKeyServiceFactory;
|
||||
};
|
||||
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
store: {
|
||||
user: Pick<TAuthDalFactory, "getUserById">;
|
||||
user: Pick<TUserDalFactory, "findById">;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
40
backend-pg/src/@types/knex.d.ts
vendored
40
backend-pg/src/@types/knex.d.ts
vendored
@@ -10,10 +10,24 @@ import {
|
||||
TBackupPrivateKey,
|
||||
TBackupPrivateKeyInsert,
|
||||
TBackupPrivateKeyUpdate,
|
||||
TOrganizationMemberships,
|
||||
TIncidentContacts,
|
||||
TIncidentContactsInsert,
|
||||
TIncidentContactsUpdate,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
TOrgRoles,
|
||||
TOrgRolesInsert,
|
||||
TOrgRolesUpdate,
|
||||
TServerConfig,
|
||||
TServerConfigInsert,
|
||||
TServerConfigUpdate,
|
||||
TUserActions,
|
||||
TUserActionsInsert,
|
||||
TUserActionsUpdate,
|
||||
TUserEncryptionKeys,
|
||||
TUserEncryptionKeysInsert,
|
||||
TUserEncryptionKeysUpdate,
|
||||
@@ -21,6 +35,7 @@ import {
|
||||
TUsersInsert,
|
||||
TUsersUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { TApiKeys, TApiKeysInsert, TApiKeysUpdate } from "@app/db/schemas/api-keys";
|
||||
|
||||
declare module "knex/types/tables" {
|
||||
interface Tables extends { [key in TableName]: Knex.CompositeTableType<any> } {
|
||||
@@ -51,9 +66,26 @@ declare module "knex/types/tables" {
|
||||
TOrganizationsUpdate
|
||||
>;
|
||||
[TableName.OrgMembership]: Knex.CompositeTableType<
|
||||
TOrganizationMemberships,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate
|
||||
>;
|
||||
[TableName.OrgRoles]: Knex.CompositeTableType<TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate>;
|
||||
[TableName.IncidentContact]: Knex.CompositeTableType<
|
||||
TIncidentContacts,
|
||||
TIncidentContactsInsert,
|
||||
TIncidentContactsUpdate
|
||||
>;
|
||||
[TableName.UserAction]: Knex.CompositeTableType<
|
||||
TUserActions,
|
||||
TUserActionsInsert,
|
||||
TUserActionsUpdate
|
||||
>;
|
||||
[TableName.ServerConfig]: Knex.CompositeTableType<
|
||||
TServerConfig,
|
||||
TServerConfigInsert,
|
||||
TServerConfigUpdate
|
||||
>;
|
||||
[TableName.ApiKey]: Knex.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.text("clientPublicKey");
|
||||
t.text("serverPrivateKey");
|
||||
t.integer("encryptionVersion").defaultTo(1);
|
||||
t.integer("encryptionVersion").defaultTo(2);
|
||||
t.text("protectedKey").notNullable();
|
||||
t.text("protectedKeyIV").notNullable();
|
||||
t.text("protectedKeyTag").notNullable();
|
||||
|
||||
@@ -13,12 +13,21 @@ export async function up(knex: Knex): Promise<void> {
|
||||
// does not need update trigger we will do it manually
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
await createOnUpdateTrigger(knex, TableName.Organization);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.AuthTokens, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.dropColumn("orgId");
|
||||
});
|
||||
}
|
||||
await knex.schema.dropTableIfExists(TableName.Organization);
|
||||
await dropOnUpdateTrigger(knex, TableName.Organization);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,23 @@ import { OrgMembershipStatus } from "../schemas/models";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
|
||||
if (!isTablePresent) {
|
||||
const isOrgRolePresent = await knex.schema.hasTable(TableName.OrgRoles);
|
||||
if (!isOrgRolePresent) {
|
||||
await knex.schema.createTable(TableName.OrgRoles, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.string("description");
|
||||
t.string("slug").notNullable();
|
||||
t.json("permissions").notNullable();
|
||||
// does not need update trigger we will do it manually
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
const isOrgTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
|
||||
if (!isOrgTablePresent) {
|
||||
await knex.schema.createTable(TableName.OrgMembership, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("role").notNullable();
|
||||
@@ -14,10 +29,13 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.string("inviteEmail");
|
||||
// does not need update trigger we will do it manually
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("userId").notNullable();
|
||||
t.uuid("userId");
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
// until role is changed/removed the role should not deleted
|
||||
t.uuid("roleId");
|
||||
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
@@ -26,5 +44,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.OrgMembership);
|
||||
await knex.schema.dropTableIfExists(TableName.OrgRoles);
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgMembership);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.IncidentContact);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.IncidentContact, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("email").notNullable();
|
||||
// does not need update trigger we will do it manually
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
await createOnUpdateTrigger(knex, TableName.IncidentContact);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IncidentContact);
|
||||
await dropOnUpdateTrigger(knex, TableName.IncidentContact);
|
||||
}
|
||||
20
backend-pg/src/db/migrations/20231207055643_user-action.ts
Normal file
20
backend-pg/src/db/migrations/20231207055643_user-action.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.UserAction);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.UserAction, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("action").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("userId").notNullable();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.UserAction);
|
||||
}
|
||||
23
backend-pg/src/db/migrations/20231207055701_server-config.ts
Normal file
23
backend-pg/src/db/migrations/20231207055701_server-config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.ServerConfig);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.ServerConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.boolean("initialized").defaultTo(false);
|
||||
t.boolean("allowSignUp").defaultTo(true);
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
await createOnUpdateTrigger(knex, TableName.ServerConfig);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ServerConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.ServerConfig);
|
||||
}
|
||||
26
backend-pg/src/db/migrations/20231207105059_api-key.ts
Normal file
26
backend-pg/src/db/migrations/20231207105059_api-key.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.ApiKey);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.ApiKey, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.datetime("lastUsed");
|
||||
t.datetime("expiresAt");
|
||||
t.string("secretHash").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("userId").notNullable();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.ApiKey);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ApiKey);
|
||||
await dropOnUpdateTrigger(knex, TableName.ApiKey);
|
||||
}
|
||||
23
backend-pg/src/db/schemas/api-keys.ts
Normal file
23
backend-pg/src/db/schemas/api-keys.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ApiKeysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
secretHash: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TApiKeys = z.infer<typeof ApiKeysSchema>;
|
||||
export type TApiKeysInsert = Omit<TApiKeys, TImmutableDBKeys>;
|
||||
export type TApiKeysUpdate = Partial<Omit<TApiKeys, TImmutableDBKeys>>;
|
||||
@@ -13,9 +13,9 @@ export const AuthTokenSessionsSchema = z.object({
|
||||
userAgent: z.string().nullable().optional(),
|
||||
refreshVersion: z.number().default(1),
|
||||
accessVersion: z.number().default(1),
|
||||
lastUsed: z.string().datetime(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
lastUsed: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@ export const AuthTokensSchema = z.object({
|
||||
phoneNumber: z.string().nullable().optional(),
|
||||
tokenHash: z.string(),
|
||||
triesLeft: z.number().nullable().optional(),
|
||||
expiresAt: z.string().datetime(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
expiresAt: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
||||
@@ -16,8 +16,8 @@ export const BackupPrivateKeySchema = z.object({
|
||||
keyEncoding: z.string(),
|
||||
salt: z.string(),
|
||||
verifier: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
|
||||
20
backend-pg/src/db/schemas/incident-contacts.ts
Normal file
20
backend-pg/src/db/schemas/incident-contacts.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IncidentContactsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TIncidentContacts = z.infer<typeof IncidentContactsSchema>;
|
||||
export type TIncidentContactsInsert = Omit<TIncidentContacts, TImmutableDBKeys>;
|
||||
export type TIncidentContactsUpdate = Partial<Omit<TIncidentContacts, TImmutableDBKeys>>;
|
||||
@@ -1,8 +1,12 @@
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
export * from "./backup-private-key";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./models";
|
||||
export * from "./organization-memberships";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
export * from "./server-config";
|
||||
export * from "./user-actions";
|
||||
export * from "./user-encryption-keys";
|
||||
export * from "./users";
|
||||
|
||||
@@ -7,7 +7,12 @@ export enum TableName {
|
||||
AuthTokenSession = "auth_token_sessions",
|
||||
BackupPrivateKey = "backup_private_key",
|
||||
Organization = "organizations",
|
||||
OrgMembership = "organization_memberships"
|
||||
OrgMembership = "org_memberships",
|
||||
OrgRoles = "org_roles",
|
||||
IncidentContact = "incident_contacts",
|
||||
UserAction = "user_actions",
|
||||
ServerConfig = "server_config",
|
||||
ApiKey = "api_keys"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
24
backend-pg/src/db/schemas/org-memberships.ts
Normal file
24
backend-pg/src/db/schemas/org-memberships.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
status: z.string().default('invited'),
|
||||
inviteEmail: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||
export type TOrgMembershipsInsert = Omit<TOrgMemberships, TImmutableDBKeys>;
|
||||
export type TOrgMembershipsUpdate = Partial<Omit<TOrgMemberships, TImmutableDBKeys>>;
|
||||
23
backend-pg/src/db/schemas/org-roles.ts
Normal file
23
backend-pg/src/db/schemas/org-roles.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgRolesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
slug: z.string(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TOrgRoles = z.infer<typeof OrgRolesSchema>;
|
||||
export type TOrgRolesInsert = Omit<TOrgRoles, TImmutableDBKeys>;
|
||||
export type TOrgRolesUpdate = Partial<Omit<TOrgRoles, TImmutableDBKeys>>;
|
||||
@@ -1,23 +0,0 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrganizationMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
status: z.string().default('invited'),
|
||||
inviteEmail: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TOrganizationMemberships = z.infer<typeof OrganizationMembershipsSchema>;
|
||||
export type TOrganizationMembershipsInsert = Omit<TOrganizationMemberships, TImmutableDBKeys>;
|
||||
export type TOrganizationMembershipsUpdate = Partial<Omit<TOrganizationMemberships, TImmutableDBKeys>>;
|
||||
@@ -11,8 +11,8 @@ export const OrganizationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
customerId: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
20
backend-pg/src/db/schemas/server-config.ts
Normal file
20
backend-pg/src/db/schemas/server-config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ServerConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
initialized: z.boolean().default(false).nullable().optional(),
|
||||
allowSignUp: z.boolean().default(true).nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TServerConfig = z.infer<typeof ServerConfigSchema>;
|
||||
export type TServerConfigInsert = Omit<TServerConfig, TImmutableDBKeys>;
|
||||
export type TServerConfigUpdate = Partial<Omit<TServerConfig, TImmutableDBKeys>>;
|
||||
20
backend-pg/src/db/schemas/user-actions.ts
Normal file
20
backend-pg/src/db/schemas/user-actions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UserActionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
action: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TUserActions = z.infer<typeof UserActionsSchema>;
|
||||
export type TUserActionsInsert = Omit<TUserActions, TImmutableDBKeys>;
|
||||
export type TUserActionsUpdate = Partial<Omit<TUserActions, TImmutableDBKeys>>;
|
||||
@@ -17,9 +17,9 @@ export const UsersSchema = z.object({
|
||||
isAccepted: z.boolean().default(false).nullable().optional(),
|
||||
isMfaEnabled: z.boolean().default(false).nullable().optional(),
|
||||
mfaMethods: z.string().array().nullable().optional(),
|
||||
devices: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
devices: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
||||
36
backend-pg/src/ee/LICENSE
Normal file
36
backend-pg/src/ee/LICENSE
Normal file
@@ -0,0 +1,36 @@
|
||||
The Infisical Enterprise license (the “Enterprise License”)
|
||||
Copyright (c) 2022 Infisical Inc
|
||||
|
||||
With regard to the Infisical Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Infisical Subscription Terms of Service, available
|
||||
at https://infisical.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Infisical,
|
||||
and otherwise have a valid Infisical Enterprise License for the
|
||||
correct number of user seats. Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Infisical
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Infiscial Enterprise subscription for the correct
|
||||
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Infisical and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Infisical Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
6
backend-pg/src/ee/routes/v1/index.ts
Normal file
6
backend-pg/src/ee/routes/v1/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { registerOrgRoleRouter } from "./org-role";
|
||||
|
||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||
};
|
||||
150
backend-pg/src/ee/routes/v1/org-role.ts
Normal file
150
backend-pg/src/ee/routes/v1/org-role.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:organizationId/roles",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
workspaceId: z.string().trim().optional(),
|
||||
orgId: z.string().trim(),
|
||||
permissions: z.any().array()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: OrgRolesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.orgRole.createRole(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
{ ...req.body, permissions: JSON.stringify(req.body.permissions) }
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
roleId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().trim().optional(),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
workspaceId: z.string().trim().optional(),
|
||||
orgId: z.string().trim(),
|
||||
permissions: z.any().array()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: OrgRolesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.orgRole.updateRole(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
req.params.roleId,
|
||||
{
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
|
||||
}
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
roleId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: OrgRolesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.orgRole.deleteRole(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
req.params.roleId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/roles",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: OrgRolesSchema.omit({ permissions: true })
|
||||
.merge(z.object({ permissions: z.unknown() }))
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.orgRole.listRoles(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/permissions",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: OrgMembershipsSchema,
|
||||
permissions: z.any().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { permissions, membership } = await server.services.orgRole.getUserPermission(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { permissions, membership };
|
||||
}
|
||||
});
|
||||
};
|
||||
124
backend-pg/src/ee/services/permission/org-permission.ts
Normal file
124
backend-pg/src/ee/services/permission/org-permission.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import {
|
||||
AbilityBuilder,
|
||||
buildMongoQueryMatcher,
|
||||
createMongoAbility,
|
||||
MongoAbility
|
||||
} from "@casl/ability";
|
||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||
|
||||
const $glob: FieldInstruction<string> = {
|
||||
type: "field",
|
||||
validate(instruction, value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`"${instruction.name}" expects value to be a string`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||
const secretPath = context.get(object, node.field);
|
||||
const permissionSecretGlobPath = node.value;
|
||||
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
||||
};
|
||||
|
||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||
|
||||
export enum OrgPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
Settings = "settings",
|
||||
IncidentAccount = "incident-contact",
|
||||
Sso = "sso",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
// ws permissions
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
// role permission
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
export const orgAdminPermissions = buildAdminPermission();
|
||||
|
||||
const buildMemberPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
export const orgMemberPermissions = buildMemberPermission();
|
||||
23
backend-pg/src/ee/services/permission/permission-dal.ts
Normal file
23
backend-pg/src/ee/services/permission/permission-dal.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TOrgMemberships } from "@app/db/schemas";
|
||||
|
||||
export type TPermissionDalFactory = ReturnType<typeof permissionDalFactory>;
|
||||
|
||||
export const permissionDalFactory = (db: TDbClient) => {
|
||||
const getOrgPermission = async (
|
||||
userId: string,
|
||||
orgId: string
|
||||
): Promise<(TOrgMemberships & { permissions: string }) | undefined> => {
|
||||
const membership = await db(TableName.OrgMembership)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.select(`${TableName.OrgMembership}.*`, `${TableName.OrgRoles}.permissions`)
|
||||
.where({ userId, [`${TableName.OrgMembership}.orgId`]: orgId })
|
||||
.first();
|
||||
|
||||
return membership;
|
||||
};
|
||||
|
||||
return {
|
||||
getOrgPermission
|
||||
};
|
||||
};
|
||||
50
backend-pg/src/ee/services/permission/permission-service.ts
Normal file
50
backend-pg/src/ee/services/permission/permission-service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import {
|
||||
conditionsMatcher,
|
||||
orgAdminPermissions,
|
||||
orgMemberPermissions,
|
||||
OrgPermissionSet
|
||||
} from "./org-permission";
|
||||
import { TPermissionDalFactory } from "./permission-dal";
|
||||
|
||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { unpackRules } from "@casl/ability/extra";
|
||||
|
||||
type TPermissionServiceFactoryDep = {
|
||||
permissionDal: TPermissionDalFactory;
|
||||
};
|
||||
|
||||
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
|
||||
|
||||
export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFactoryDep) => {
|
||||
/*
|
||||
* Get user permission in an organization
|
||||
* */
|
||||
const getUserOrgPermission = async (userId: string, orgId: string) => {
|
||||
const membership = await permissionDal.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (membership.role === "custom" && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
if (membership.role === "admin") return { permission: orgAdminPermissions, membership };
|
||||
if (membership.role === "member") return { permission: orgMemberPermissions, membership };
|
||||
if (membership.role === "custom") {
|
||||
const permission = createMongoAbility<OrgPermissionSet>(
|
||||
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
|
||||
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
|
||||
{
|
||||
conditionsMatcher
|
||||
}
|
||||
);
|
||||
return { permission, membership };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ name: "Role missing", message: "User role not found" });
|
||||
};
|
||||
|
||||
return {
|
||||
getUserOrgPermission
|
||||
};
|
||||
};
|
||||
@@ -8,33 +8,35 @@ const zodStrBool = z
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.coerce.number().default(4000),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
SALT_ROUNDS: z.coerce.number().default(10),
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_SECURE: zodStrBool,
|
||||
SMTP_PORT: z.coerce.number().default(587),
|
||||
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
|
||||
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
|
||||
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
|
||||
SITE_URL: zpStr(z.string().optional()),
|
||||
// jwt options
|
||||
JWT_AUTH_SECRET: zpStr(z.string()),
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
|
||||
});
|
||||
const envSchema = z
|
||||
.object({
|
||||
PORT: z.coerce.number().default(4000),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
SALT_ROUNDS: z.coerce.number().default(10),
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_SECURE: zodStrBool,
|
||||
SMTP_PORT: z.coerce.number().default(587),
|
||||
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
|
||||
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
|
||||
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
|
||||
SITE_URL: zpStr(z.string().optional()),
|
||||
// jwt options
|
||||
JWT_AUTH_SECRET: zpStr(z.string()),
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
|
||||
})
|
||||
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
|
||||
|
||||
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class BadRequestError extends Error {
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message?: string; name: string; error: unknown }) {
|
||||
constructor({ name, error, message }: { message?: string; name: string; error?: unknown }) {
|
||||
super(message ?? "The request is invalid");
|
||||
this.name = name;
|
||||
this.error = error;
|
||||
|
||||
@@ -1,10 +1,108 @@
|
||||
import { Knex } from "knex";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
import { DatabaseError } from "../errors";
|
||||
|
||||
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
transaction: async <T>(cb: (tx: Knex) => T) =>
|
||||
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
|
||||
db.transaction(async (trx) => {
|
||||
const res = await cb(trx);
|
||||
return res;
|
||||
}),
|
||||
...dal
|
||||
});
|
||||
|
||||
// What is ormify
|
||||
// It is to inject typical operations like find, findOne, update, delete, create
|
||||
// This will avoid writing most common ones each time
|
||||
export const ormify = <DbOps extends object, Tname extends TableName>(
|
||||
db: Knex,
|
||||
tableName: Tname,
|
||||
dal?: DbOps
|
||||
) => ({
|
||||
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
|
||||
db.transaction(async (trx) => {
|
||||
const res = await cb(trx);
|
||||
return res;
|
||||
}),
|
||||
findById: (id: string, tx?: Knex) => {
|
||||
try {
|
||||
return (tx || db)(tableName)
|
||||
.where({ id } as any)
|
||||
.first("*");
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id" });
|
||||
}
|
||||
},
|
||||
findOne: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
|
||||
try {
|
||||
const res = await (tx || db)(tableName).where(filter).first("*");
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
find: (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
|
||||
try {
|
||||
return (tx || db)(tableName).where(filter);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
create: async (data: Tables[Tname]["insert"], tx?: Knex) => {
|
||||
try {
|
||||
const [user] = await (tx || db)(tableName).insert(data).returning("*");
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create" });
|
||||
}
|
||||
},
|
||||
updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
|
||||
try {
|
||||
const [user] = await (tx || db)(tableName)
|
||||
.where({ id } as any)
|
||||
.update(data as any)
|
||||
.returning("*");
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update by id" });
|
||||
}
|
||||
},
|
||||
update: async (
|
||||
filter: Partial<Tables[Tname]["base"]>,
|
||||
data: Tables[Tname]["update"],
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const user = await (tx || db)(tableName)
|
||||
.where(filter)
|
||||
.update(data as any)
|
||||
.returning("*");
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update" });
|
||||
}
|
||||
},
|
||||
deleteById: async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const [user] = await (tx || db)(tableName)
|
||||
.where({ id } as any)
|
||||
.delete()
|
||||
.returning("*");
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete by id" });
|
||||
}
|
||||
},
|
||||
delete: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
|
||||
try {
|
||||
const user = await (tx || db)(tableName).where(filter).delete().returning("*");
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete" });
|
||||
}
|
||||
},
|
||||
...(dal || {})
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
import fp from "fastify-plugin";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -27,7 +28,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
case AuthMode.SERVICE_ACCESS_TOKEN:
|
||||
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const;
|
||||
default:
|
||||
throw new UnauthorizedError({ name: "Invalid token type" });
|
||||
return { authMode: null, token: null } as const;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,13 +42,13 @@ const getJwtIdentity = async (server: FastifyZodProvider, token: AuthModeJwtToke
|
||||
if (token.accessVersion !== session.accessVersion)
|
||||
throw new UnauthorizedError({ name: "Stale session" });
|
||||
|
||||
const user = await server.store.user.getUserById(session.userId);
|
||||
const user = await server.store.user.findById(session.userId);
|
||||
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
|
||||
|
||||
return user;
|
||||
return { user, tokenVersionId: token.tokenVersionId };
|
||||
};
|
||||
|
||||
export const injectIdentity = (server: FastifyZodProvider) => {
|
||||
export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auth", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -56,8 +57,11 @@ export const injectIdentity = (server: FastifyZodProvider) => {
|
||||
// TODO(akhilmhdh-pg): fill in rest of auth mode logic
|
||||
switch (authMode) {
|
||||
case AuthMode.JWT: {
|
||||
const user = await getJwtIdentity(server, token as AuthModeJwtTokenPayload);
|
||||
req.auth = { authMode: AuthMode.JWT, user, userId: user.id };
|
||||
const { user, tokenVersionId } = await getJwtIdentity(
|
||||
server,
|
||||
token as AuthModeJwtTokenPayload
|
||||
);
|
||||
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId };
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN:
|
||||
@@ -72,4 +76,4 @@ export const injectIdentity = (server: FastifyZodProvider) => {
|
||||
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
11
backend-pg/src/server/plugins/auth/superAdmin.ts
Normal file
11
backend-pg/src/server/plugins/auth/superAdmin.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
export const verifySuperAdmin = async <T extends FastifyRequest>(req: T) => {
|
||||
if (!req.auth.user.superAdmin)
|
||||
throw new UnauthorizedError({
|
||||
name: "Unauthorized access",
|
||||
message: "Requires superadmin access"
|
||||
});
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const verifyAuth =
|
||||
<T extends FastifyRequest>(authStrats: AuthMode[]) =>
|
||||
(req: T) => {
|
||||
async (req: T) => {
|
||||
if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array");
|
||||
if (!req.auth)
|
||||
throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" });
|
||||
|
||||
@@ -1,47 +1,142 @@
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||
import { permissionDalFactory } from "@app/ee/services/permission/permission-dal";
|
||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal";
|
||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { authDalFactory } from "@app/services/auth/auth-dal";
|
||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgDalFactory } from "@app/services/org/org-dal";
|
||||
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
|
||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||
import { serverCfgDalFactory } from "@app/services/server-cfg/server-cfg-dal";
|
||||
import { serverCfgServiceFactory } from "@app/services/server-cfg/server-cfg-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { tokenDalFactory } from "@app/services/token/token-dal";
|
||||
import { tokenServiceFactory } from "@app/services/token/token-service";
|
||||
import { userDalFactory } from "@app/services/user/user-dal";
|
||||
import { userServiceFactory } from "@app/services/user/user-service";
|
||||
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
{ db, smtp }: { db: Knex; smtp: TSmtpService }
|
||||
{ db, smtp: smtpService }: { db: Knex; smtp: TSmtpService }
|
||||
) => {
|
||||
// db layers
|
||||
const userDal = userDalFactory(db);
|
||||
const authDal = authDalFactory(db);
|
||||
const authTokenDal = tokenDalFactory(db);
|
||||
const orgDal = orgDalFactory(db);
|
||||
const incidentContactDal = incidentContactDalFactory(db);
|
||||
const orgRoleDal = orgRoleDalFactory(db);
|
||||
const serverCfgDal = serverCfgDalFactory(db);
|
||||
const apiKeyDal = apiKeyDalFactory(db);
|
||||
|
||||
// ee db layer ops
|
||||
const permissionDal = permissionDalFactory(db);
|
||||
|
||||
// ee services
|
||||
const permissionService = permissionServiceFactory({ permissionDal });
|
||||
|
||||
// service layers
|
||||
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
||||
const loginService = authLoginServiceFactory({ authDal, smtpService: smtp, tokenService });
|
||||
const passwordService = authPaswordServiceFactory({ tokenService, smtpService: smtp, authDal });
|
||||
const signupService = authSignupServiceFactory({ tokenService, smtpService: smtp, authDal });
|
||||
const userService = userServiceFactory({ userDal });
|
||||
const loginService = authLoginServiceFactory({ userDal, smtpService, tokenService });
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
authDal,
|
||||
userDal
|
||||
});
|
||||
const orgService = orgServiceFactory({
|
||||
orgRoleDal,
|
||||
permissionService,
|
||||
orgDal,
|
||||
incidentContactDal,
|
||||
tokenService,
|
||||
smtpService,
|
||||
userDal
|
||||
});
|
||||
const signupService = authSignupServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
authDal,
|
||||
userDal,
|
||||
orgDal,
|
||||
orgService
|
||||
});
|
||||
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDal });
|
||||
const serverCfgService = serverCfgServiceFactory({
|
||||
userDal,
|
||||
authService: loginService,
|
||||
serverCfgDal
|
||||
});
|
||||
const apiKeyService = apiKeyServiceFactory({ apiKeyDal });
|
||||
|
||||
await serverCfgService.initServerCfg();
|
||||
// inject all services
|
||||
server.decorate("services", {
|
||||
login: loginService,
|
||||
password: passwordService,
|
||||
signup: signupService
|
||||
signup: signupService,
|
||||
user: userService,
|
||||
permission: permissionService,
|
||||
org: orgService,
|
||||
orgRole: orgRoleService,
|
||||
serverCfg: serverCfgService,
|
||||
apiKey: apiKeyService,
|
||||
authToken: tokenService
|
||||
} as FastifyZodProvider["services"]);
|
||||
|
||||
server.decorate("store", {
|
||||
user: authDal
|
||||
user: userDal
|
||||
} as FastifyZodProvider["store"]);
|
||||
|
||||
await server.register(injectIdentity);
|
||||
|
||||
server.route({
|
||||
url: "/status",
|
||||
method: "GET",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
date: z.date(),
|
||||
message: z.literal("Ok"),
|
||||
emailConfigured: z.boolean().optional(),
|
||||
inviteOnlySignup: z.boolean().optional(),
|
||||
redisConfigured: z.boolean().optional(),
|
||||
secretScanningConfigured: z.boolean().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: () => {
|
||||
const appCfg = getConfig();
|
||||
return {
|
||||
date: new Date(),
|
||||
message: "Ok" as const,
|
||||
emailConfigured: appCfg.isSmtpConfigured,
|
||||
inviteOnlySignup: false,
|
||||
redisConfigured: false,
|
||||
secretScanningConfigured: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// register routes for v1
|
||||
await server.register(registerV1Routes, { prefix: "/v1" });
|
||||
await server.register(registerV2Routes, { prefix: "/v2" });
|
||||
await server.register(registerV3Routes, { prefix: "/v3" });
|
||||
|
||||
await server.register(registerV1EERoutes, { prefix: "/ee/v1" });
|
||||
};
|
||||
|
||||
101
backend-pg/src/server/routes/v1/admin.ts
Normal file
101
backend-pg/src/server/routes/v1/admin.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ServerConfigSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "GET",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
config: ServerConfigSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: () => {
|
||||
const config = server.services.serverCfg.getServerCfg();
|
||||
return { config };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
body: z.object({
|
||||
allowSignUp: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
config: ServerConfigSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: (req) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req);
|
||||
verifySuperAdmin(req);
|
||||
},
|
||||
handler: async (req) => {
|
||||
const config = await server.services.serverCfg.updateServerCfg(req.body);
|
||||
return { config };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/signup",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().email().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
user: UsersSchema,
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = server.services.serverCfg.getServerCfg();
|
||||
if (serverCfg.initialized)
|
||||
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
|
||||
const { user, token } = await server.services.serverCfg.adminSignUp({
|
||||
...req.body,
|
||||
ip: req.realIp,
|
||||
userAgent: req.headers["user-agent"] || ""
|
||||
});
|
||||
|
||||
res.setCookie("jid", token.refresh, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully set up admin account",
|
||||
user: user.user,
|
||||
token: token.access
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
88
backend-pg/src/server/routes/v1/auth.ts
Normal file
88
backend-pg/src/server/routes/v1/auth.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import {
|
||||
AuthMode,
|
||||
AuthModeRefreshJwtTokenPayload,
|
||||
AuthTokenType
|
||||
} from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/logout",
|
||||
method: "POST",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
const appCfg = getConfig();
|
||||
await server.services.login.logout(req.auth.userId, req.auth.tokenVersionId);
|
||||
res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
return { message: "Successfully logged out" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/token",
|
||||
method: "POST",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const refreshToken = req.cookies.jid;
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
throw new BadRequestError({
|
||||
name: "Auth token route",
|
||||
message: "Failed to find refresh token"
|
||||
});
|
||||
|
||||
const decodedToken = jwt.verify(
|
||||
refreshToken,
|
||||
appCfg.JWT_AUTH_SECRET
|
||||
) as AuthModeRefreshJwtTokenPayload;
|
||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
||||
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||
|
||||
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
|
||||
decodedToken.tokenVersionId,
|
||||
decodedToken.userId
|
||||
);
|
||||
if (!tokenVersion)
|
||||
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
|
||||
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: decodedToken.userId,
|
||||
tokenVersionId: tokenVersion.id,
|
||||
accessVersion: tokenVersion.accessVersion
|
||||
},
|
||||
appCfg.JWT_AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||
);
|
||||
|
||||
return { token };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,17 @@
|
||||
import { registerAdminRouter } from "./admin";
|
||||
import { registerAuthRoutes } from "./auth";
|
||||
import { registerInviteOrgRouter } from "./invite-org";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerUserActionRouter } from "./user-action-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerAuthRoutes, { prefix: "/auth" });
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
};
|
||||
|
||||
70
backend-pg/src/server/routes/v1/invite-org.ts
Normal file
70
backend-pg/src/server/routes/v1/invite-org.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/signup",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
inviteeEmail: z.string().trim().email(),
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
completeInviteLink: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const completeInviteLink = await server.services.org.inviteUserToOrganization({
|
||||
orgId: req.body.organizationId,
|
||||
userId: req.auth.userId,
|
||||
inviteeEmail: req.body.inviteeEmail
|
||||
});
|
||||
|
||||
return {
|
||||
completeInviteLink,
|
||||
message: `Send an invite link to ${req.body.inviteeEmail}`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/verify",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().trim().email(),
|
||||
organizationId: z.string().trim(),
|
||||
code: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
token: z.string().optional(),
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { user, token } = await server.services.org.verifyUserToOrg({
|
||||
orgId: req.body.organizationId,
|
||||
code: req.body.code,
|
||||
email: req.body.email
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
183
backend-pg/src/server/routes/v1/organization-router.ts
Normal file
183
backend-pg/src/server/routes/v1/organization-router.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
IncidentContactsSchema,
|
||||
OrganizationsSchema,
|
||||
OrgMembershipsSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: OrganizationsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const organizations = await server.services.org.findAllOrganizationOfUser(req.auth.userId);
|
||||
return { organizations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.findOrganizationById(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/users",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: OrgMembershipsSchema.merge(
|
||||
z.object({
|
||||
user: UsersSchema.pick({
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||
})
|
||||
)
|
||||
.omit({ createdAt: true, updatedAt: true })
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const users = await server.services.org.findAllOrgMembers(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { users };
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(akhilmhdh-pg): missing my-workspace list
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/name",
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({ name: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
organization: OrganizationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.updateOrgName(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
req.body.name
|
||||
);
|
||||
return {
|
||||
message: "Successfully changed organization name",
|
||||
organization
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/incidentContactOrg",
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
incidentContactsOrg: IncidentContactsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const incidentContactsOrg = await req.server.services.org.findIncidentContacts(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { incidentContactsOrg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:organizationId/incidentContactOrg",
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({ email: z.string().email().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
incidentContactsOrg: IncidentContactsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const incidentContactsOrg = await req.server.services.org.createIncidentContact(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
req.body.email
|
||||
);
|
||||
return { incidentContactsOrg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/incidentContactOrg/:incidentContactId",
|
||||
schema: {
|
||||
// TODO(akhilmhdh-pg): change accept id instead of email
|
||||
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
incidentContactsOrg: IncidentContactsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const incidentContactsOrg = await req.server.services.org.deleteIncidentContact(
|
||||
req.auth.userId,
|
||||
req.params.organizationId,
|
||||
req.params.incidentContactId
|
||||
);
|
||||
return { incidentContactsOrg };
|
||||
}
|
||||
});
|
||||
};
|
||||
54
backend-pg/src/server/routes/v1/user-action-router.ts
Normal file
54
backend-pg/src/server/routes/v1/user-action-router.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UserActionsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserActionRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
action: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
userAction: UserActionsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const userAction = await server.services.user.createUserAction(
|
||||
req.auth.userId,
|
||||
req.body.action
|
||||
);
|
||||
return { userAction, message: "Successfully recorded user action" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
action: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
userAction: UserActionsSchema.optional().nullable()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const userAction = await server.services.user.getUserAction(
|
||||
req.auth.userId,
|
||||
req.query.action
|
||||
);
|
||||
return { userAction };
|
||||
}
|
||||
});
|
||||
};
|
||||
24
backend-pg/src/server/routes/v1/user-router.ts
Normal file
24
backend-pg/src/server/routes/v1/user-router.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.getMe(req.auth.userId);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { registerMfaRouter } from "./mfa-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
|
||||
export const registerV2Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerMfaRouter, { prefix: "/auth" });
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
|
||||
throw new Error("Unauthorized access");
|
||||
|
||||
const user = await server.store.user.getUserById(decodedToken.userId);
|
||||
const user = await server.store.user.findById(decodedToken.userId);
|
||||
if (!user) throw new Error("User not found");
|
||||
req.mfa = { userId: user.id, user };
|
||||
});
|
||||
|
||||
140
backend-pg/src/server/routes/v2/organization-router.ts
Normal file
140
backend-pg/src/server/routes/v2/organization-router.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
OrganizationsSchema,
|
||||
OrgMembershipsSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/memberships",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: OrgMembershipsSchema.merge(
|
||||
z.object({
|
||||
user: UsersSchema.pick({
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||
})
|
||||
)
|
||||
.omit({ createdAt: true, updatedAt: true })
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const users = await server.services.org.findAllOrgMembers(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { users };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/memberships/:membershipId",
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||
body: z.object({
|
||||
role: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: OrgMembershipsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const membership = await server.services.org.updateOrgMembership({
|
||||
userId: req.auth.userId,
|
||||
role: req.body.role,
|
||||
orgId: req.params.organizationId,
|
||||
membershipId: req.params.membershipId
|
||||
});
|
||||
return { membership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/memberships/:membershipId",
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: OrgMembershipsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const membership = await server.services.org.deleteOrgMembership({
|
||||
userId: req.auth.userId,
|
||||
orgId: req.params.organizationId,
|
||||
membershipId: req.params.membershipId
|
||||
});
|
||||
return { membership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.createOrganization(
|
||||
req.auth.userId,
|
||||
req.body.name
|
||||
);
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId",
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.deleteOrganizationById(
|
||||
req.auth.userId,
|
||||
req.params.organizationId
|
||||
);
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
};
|
||||
225
backend-pg/src/server/routes/v2/user-router.ts
Normal file
225
backend-pg/src/server/routes/v2/user-router.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/me/mfa",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
body: z.object({
|
||||
isMfaEnabled: z.boolean()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.toggleUserMfa(req.auth.userId, req.body.isMfaEnabled);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/me/name",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
body: z.object({
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.updateUserName(
|
||||
req.auth.userId,
|
||||
req.body.firstName,
|
||||
req.body.lastName
|
||||
);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/me/auth-methods",
|
||||
method: "PUT",
|
||||
schema: {
|
||||
body: z.object({
|
||||
authMethods: z.nativeEnum(AuthMethod).array().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.updateAuthMethods(
|
||||
req.auth.userId,
|
||||
req.body.authMethods
|
||||
);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/organizations",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: OrganizationsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const organizations = await server.services.org.findAllOrganizationOfUser(req.auth.userId);
|
||||
return { organizations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/api-keys",
|
||||
schema: {
|
||||
response: {
|
||||
200: ApiKeysSchema.omit({ secretHash: true }).array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const apiKeys = await server.services.apiKey.getMyApiKeys(req.auth.userId);
|
||||
return apiKeys;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/me/api-keys",
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim(),
|
||||
expiresIn: z.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
apiKey: z.string(),
|
||||
apiKeyData: ApiKeysSchema.omit({ secretHash: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const apiKeys = await server.services.apiKey.createApiKey(
|
||||
req.auth.userId,
|
||||
req.body.name,
|
||||
req.body.expiresIn
|
||||
);
|
||||
return apiKeys;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/me/api-keys/:apiKeyDataId",
|
||||
schema: {
|
||||
params: z.object({
|
||||
apiKeyDataId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
apiKeyData: ApiKeysSchema.omit({ secretHash: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const apiKeyData = await server.services.apiKey.deleteApiKey(
|
||||
req.auth.userId,
|
||||
req.params.apiKeyDataId
|
||||
);
|
||||
return { apiKeyData };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/sessions",
|
||||
schema: {
|
||||
response: {
|
||||
200: AuthTokenSessionsSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const sessions = await server.services.authToken.getTokenSessionByUser(req.auth.userId);
|
||||
return sessions;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/me/sessions",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.authToken.revokeAllMySessions(req.auth.userId);
|
||||
return {
|
||||
message: "Successfully revoked all sessions"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.getMe(req.auth.userId);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/me",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const user = await server.services.user.deleteMe(req.auth.userId);
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { registerLoginRouter } from "./login-router";
|
||||
import { registerSignupRouter } from "./signup-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
|
||||
export const registerV3Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSignupRouter, { prefix: "/signup" });
|
||||
await server.register(registerLoginRouter, { prefix: "/auth" });
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
};
|
||||
|
||||
@@ -100,4 +100,54 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
return { message: "Successfully set up account", user, token: accessToken };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/complete-account/invite",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().email().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
user: UsersSchema,
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const userAgent = req.headers["user-agent"];
|
||||
if (!userAgent) throw new Error("user agent header is required");
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { user, accessToken, refreshToken } =
|
||||
await server.services.signup.completeAccountInvite({
|
||||
...req.body,
|
||||
ip: req.realIp,
|
||||
userAgent
|
||||
});
|
||||
|
||||
res.setCookie("jid", refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
// TODO(akhilmhdh-pg): add telemetry service
|
||||
|
||||
return { message: "Successfully set up account", user, token: accessToken };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
24
backend-pg/src/server/routes/v3/user-router.ts
Normal file
24
backend-pg/src/server/routes/v3/user-router.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/api-keys",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
apiKeyData: ApiKeysSchema.omit({ secretHash: true }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const apiKeyData = await server.services.apiKey.getMyApiKeys(req.auth.userId);
|
||||
return { apiKeyData };
|
||||
}
|
||||
});
|
||||
};
|
||||
7
backend-pg/src/services/api-key/api-key-dal.ts
Normal file
7
backend-pg/src/services/api-key/api-key-dal.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TApiKeyDalFactory = ReturnType<typeof apiKeyDalFactory>;
|
||||
|
||||
export const apiKeyDalFactory = (db: TDbClient) => ormify(db, TableName.ApiKey);
|
||||
55
backend-pg/src/services/api-key/api-key-service.ts
Normal file
55
backend-pg/src/services/api-key/api-key-service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TApiKeys } from "@app/db/schemas/api-keys";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TApiKeyDalFactory } from "./api-key-dal";
|
||||
|
||||
type TApiKeyServiceFactoryDep = {
|
||||
apiKeyDal: TApiKeyDalFactory;
|
||||
};
|
||||
|
||||
export type TApiKeyServiceFactory = ReturnType<typeof apiKeyServiceFactory>;
|
||||
|
||||
const formatApiKey = ({ secretHash, ...data }: TApiKeys) => data;
|
||||
|
||||
export const apiKeyServiceFactory = ({ apiKeyDal }: TApiKeyServiceFactoryDep) => {
|
||||
const getMyApiKeys = async (userId: string) => {
|
||||
const apiKeys = await apiKeyDal.find({ userId });
|
||||
return apiKeys.map((key) => formatApiKey(key));
|
||||
};
|
||||
|
||||
const createApiKey = async (userId: string, name: string, expiresIn: number) => {
|
||||
const appCfg = getConfig();
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, appCfg.SALT_ROUNDS);
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
const apiKeyData = await apiKeyDal.create({
|
||||
userId,
|
||||
name,
|
||||
expiresAt,
|
||||
secretHash,
|
||||
lastUsed: new Date()
|
||||
});
|
||||
const apiKey = `ak.${apiKeyData.id}.${secret}`;
|
||||
|
||||
return { apiKey, apiKeyData: formatApiKey(apiKeyData) };
|
||||
};
|
||||
|
||||
const deleteApiKey = async (userId: string, apiKeyId: string) => {
|
||||
const [apiKeyData] = await apiKeyDal.delete({ id: apiKeyId, userId });
|
||||
if (!apiKeyData)
|
||||
throw new BadRequestError({ message: "Failed to find api key", name: "delete api key" });
|
||||
return formatApiKey(apiKeyData);
|
||||
};
|
||||
|
||||
return {
|
||||
getMyApiKeys,
|
||||
createApiKey,
|
||||
deleteApiKey
|
||||
};
|
||||
};
|
||||
0
backend-pg/src/services/api-key/api-key-types.ts
Normal file
0
backend-pg/src/services/api-key/api-key-types.ts
Normal file
@@ -1,110 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TBackupPrivateKey, TUserEncryptionKeys, TUsers } from "@app/db/schemas";
|
||||
import { TableName, TBackupPrivateKey } from "@app/db/schemas";
|
||||
import { withTransaction } from "@app/lib/knex";
|
||||
|
||||
export type TAuthDalFactory = ReturnType<typeof authDalFactory>;
|
||||
|
||||
export const authDalFactory = (db: TDbClient) => {
|
||||
// getters
|
||||
const getUserByEmail = async (email: string): Promise<TUsers | undefined> =>
|
||||
db(TableName.Users).where({ email }).select("*").first();
|
||||
|
||||
const getUserById = async (userId: string): Promise<TUsers | undefined> =>
|
||||
db(TableName.Users).where({ id: userId }).select("*").first();
|
||||
|
||||
const getUserEncKeyByEmail = async (email: string) =>
|
||||
db(TableName.Users)
|
||||
.where({ email })
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.Users}.id`,
|
||||
`${TableName.UserEncryptionKey}.userId`
|
||||
)
|
||||
.first();
|
||||
|
||||
const getUserEncKeyByUserId = async (userId: string) =>
|
||||
db(TableName.Users)
|
||||
.where({ id: userId })
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.Users}.id`,
|
||||
`${TableName.UserEncryptionKey}.userId`
|
||||
)
|
||||
.first();
|
||||
|
||||
const getBackupPrivateKeyByUserId = async (userId: string) =>
|
||||
db(TableName.BackupPrivateKey).where({ userId }).first("*");
|
||||
|
||||
// all inserts and updates
|
||||
const createUser = async (
|
||||
email: string,
|
||||
data: Partial<TUsers> = {}
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await db(TableName.Users)
|
||||
.insert({ email, ...data })
|
||||
.returning("*");
|
||||
return user;
|
||||
};
|
||||
|
||||
const updateUser = async (
|
||||
email: string,
|
||||
data: Partial<TUsers> = {}
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await db(TableName.Users)
|
||||
.where({ email })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
return user;
|
||||
};
|
||||
|
||||
const updateUserById = async (
|
||||
id: string,
|
||||
data: Partial<TUsers> = {},
|
||||
tx?: Knex
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await (tx ? tx(TableName.Users) : db(TableName.Users))
|
||||
.where({ id })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
return user;
|
||||
};
|
||||
|
||||
const updateUserEncryptionByUserId = async (
|
||||
userId: string,
|
||||
data: Partial<TUserEncryptionKeys> = {},
|
||||
tx?: Knex
|
||||
): Promise<TUserEncryptionKeys | undefined> => {
|
||||
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||
.where({ userId })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
return userEnc;
|
||||
};
|
||||
|
||||
// all upserts
|
||||
const upsertUserEncryptionKey = async (
|
||||
userId: string,
|
||||
data: Partial<TUserEncryptionKeys>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||
// if user insert make sure to pass all required data
|
||||
.insert({ userId, ...data } as TUserEncryptionKeys)
|
||||
.onConflict("userId")
|
||||
.merge()
|
||||
.returning("*");
|
||||
return userEnc;
|
||||
};
|
||||
|
||||
const upsertBackupKey = async (
|
||||
userId: string,
|
||||
data: Partial<TBackupPrivateKey>,
|
||||
tx?: Knex
|
||||
): Promise<TBackupPrivateKey | undefined> => {
|
||||
const [backupKey] = await (tx ? tx(TableName.BackupPrivateKey) : db(TableName.BackupPrivateKey))
|
||||
.insert({ userId, ...data, updatedAt: new Date().toUTCString() } as TBackupPrivateKey)
|
||||
const [backupKey] = await (tx || db)(TableName.BackupPrivateKey)
|
||||
.insert({ userId, ...data, updatedAt: new Date() } as TBackupPrivateKey)
|
||||
.onConflict("userId")
|
||||
.merge()
|
||||
.returning("*");
|
||||
@@ -112,16 +24,7 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getUserEncKeyByEmail,
|
||||
getUserEncKeyByUserId,
|
||||
getBackupPrivateKeyByUserId,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserById,
|
||||
updateUserEncryptionByUserId,
|
||||
upsertUserEncryptionKey,
|
||||
upsertBackupKey
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import { TUserDalFactory } from "../user/user-dal";
|
||||
import {
|
||||
TLoginClientProofDTO,
|
||||
TLoginGenServerPublicKeyDTO,
|
||||
@@ -25,14 +25,14 @@ const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuth
|
||||
};
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
userDal: TUserDalFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
export const authLoginServiceFactory = ({
|
||||
authDal,
|
||||
userDal,
|
||||
tokenService,
|
||||
smtpService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
@@ -42,14 +42,14 @@ export const authLoginServiceFactory = ({
|
||||
* If new device is found. Will be saved and a mail will be send
|
||||
*/
|
||||
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
|
||||
const devices = await UserDeviceSchema.parseAsync(JSON.parse(user.devices || "[]"));
|
||||
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
||||
const isDeviceSeen = devices.some(
|
||||
(device) => device.ip === ip && device.userAgent === userAgent
|
||||
);
|
||||
|
||||
if (!isDeviceSeen) {
|
||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||
await authDal.updateUserById(user.id, { devices: JSON.stringify(newDeviceList) });
|
||||
await userDal.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.NewDeviceJoin,
|
||||
subjectLine: "Successful login from new device",
|
||||
@@ -68,23 +68,23 @@ export const authLoginServiceFactory = ({
|
||||
* Private
|
||||
* Send mfa code via email
|
||||
* */
|
||||
const sendUserMfaCode = async (user: TUsers) => {
|
||||
const sendUserMfaCode = async (userId: string, email: string) => {
|
||||
const code = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_MFA,
|
||||
userId: user.id
|
||||
userId
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.EmailMfa,
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [user.email],
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* Private
|
||||
/*
|
||||
* Check user device and send mail if new device
|
||||
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
||||
*/
|
||||
@@ -130,20 +130,19 @@ export const authLoginServiceFactory = ({
|
||||
providerAuthToken,
|
||||
clientPublicKey
|
||||
}: TLoginGenServerPublicKeyDTO) => {
|
||||
const user = await authDal.getUserEncKeyByEmail(email);
|
||||
if (!user || (user && !user.isAccepted)) {
|
||||
const userEnc = await userDal.findUserEncKeyByEmail(email);
|
||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||
throw new Error("Failed to find user");
|
||||
}
|
||||
const cfg = getConfig();
|
||||
|
||||
if (
|
||||
!user.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||
!userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
|
||||
) {
|
||||
throw new Error("Invalid authorization request");
|
||||
}
|
||||
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
|
||||
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
|
||||
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
|
||||
const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
clientPublicKey,
|
||||
serverPrivateKey: serverSrpKey.privateKey
|
||||
});
|
||||
@@ -161,46 +160,46 @@ export const authLoginServiceFactory = ({
|
||||
ip,
|
||||
userAgent
|
||||
}: TLoginClientProofDTO) => {
|
||||
const user = await authDal.getUserEncKeyByEmail(email);
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
const userEnc = await userDal.findUserEncKeyByEmail(email);
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
const cfg = getConfig();
|
||||
|
||||
if (
|
||||
!user.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||
!userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
|
||||
) {
|
||||
throw new Error("Invalid authorization request");
|
||||
}
|
||||
|
||||
if (!user.serverPrivateKey || !user.clientPublicKey)
|
||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
|
||||
throw new Error("Failed to authenticate. Try again?");
|
||||
const isValidClientProof = await srpCheckClientProof(
|
||||
user.salt,
|
||||
user.verifier,
|
||||
user.serverPrivateKey,
|
||||
user.clientPublicKey,
|
||||
userEnc.salt,
|
||||
userEnc.verifier,
|
||||
userEnc.serverPrivateKey,
|
||||
userEnc.clientPublicKey,
|
||||
clientProof
|
||||
);
|
||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||
|
||||
await authDal.updateUserEncryptionByUserId(user.id, {
|
||||
await userDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
});
|
||||
// send multi factor auth token if they it enabled
|
||||
if (user.isMfaEnabled) {
|
||||
if (userEnc.isMfaEnabled) {
|
||||
const mfaToken = jwt.sign(
|
||||
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: user.id },
|
||||
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: userEnc.userId },
|
||||
cfg.JWT_AUTH_SECRET,
|
||||
{ expiresIn: cfg.JWT_MFA_LIFETIME }
|
||||
);
|
||||
await sendUserMfaCode(user);
|
||||
await sendUserMfaCode(userEnc.userId, userEnc.email);
|
||||
|
||||
return { isMfaEnabled: true, token: mfaToken } as const;
|
||||
}
|
||||
|
||||
const token = await generateUserTokens(user, ip, userAgent);
|
||||
return { token, isMfaEnabled: false, user } as const;
|
||||
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
|
||||
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -208,9 +207,9 @@ export const authLoginServiceFactory = ({
|
||||
* saved in frontend
|
||||
*/
|
||||
const resendMfaToken = async (userId: string) => {
|
||||
const user = await authDal.getUserById(userId);
|
||||
const user = await userDal.findById(userId);
|
||||
if (!user) return;
|
||||
await sendUserMfaCode(user);
|
||||
await sendUserMfaCode(user.id, user.email);
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -223,11 +222,11 @@ export const authLoginServiceFactory = ({
|
||||
userId,
|
||||
code: mfaToken
|
||||
});
|
||||
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||
if (!user) throw new Error("Failed to authenticate user");
|
||||
const userEnc = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||
|
||||
const token = await generateUserTokens(user, ip, userAgent);
|
||||
return { token, user };
|
||||
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
|
||||
return { token, user: userEnc };
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -243,6 +242,7 @@ export const authLoginServiceFactory = ({
|
||||
loginExchangeClientProof,
|
||||
logout,
|
||||
resendMfaToken,
|
||||
verifyMfaToken
|
||||
verifyMfaToken,
|
||||
generateUserTokens
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TUserDalFactory } from "../user/user-dal";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import {
|
||||
TChangePasswordDTO,
|
||||
@@ -16,6 +17,7 @@ import { AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthPasswordServiceFactoryDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
userDal: TUserDalFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
@@ -23,6 +25,7 @@ type TAuthPasswordServiceFactoryDep = {
|
||||
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
||||
export const authPaswordServiceFactory = ({
|
||||
authDal,
|
||||
userDal,
|
||||
tokenService,
|
||||
smtpService
|
||||
}: TAuthPasswordServiceFactoryDep) => {
|
||||
@@ -31,11 +34,11 @@ export const authPaswordServiceFactory = ({
|
||||
* Gets srp server user salt and server public key
|
||||
*/
|
||||
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
|
||||
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
const userEnc = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
|
||||
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
|
||||
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
|
||||
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
|
||||
const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
clientPublicKey,
|
||||
serverPrivateKey: serverSrpKey.privateKey
|
||||
});
|
||||
@@ -59,10 +62,10 @@ export const authPaswordServiceFactory = ({
|
||||
verifier,
|
||||
tokenVersionId
|
||||
}: TChangePasswordDTO) => {
|
||||
const userEnc = await authDal.getUserEncKeyByUserId(userId);
|
||||
const userEnc = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
|
||||
await authDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
await userDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
});
|
||||
@@ -77,7 +80,7 @@ export const authPaswordServiceFactory = ({
|
||||
);
|
||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||
|
||||
await authDal.updateUserEncryptionByUserId(userId, {
|
||||
await userDal.updateUserEncryptionByUserId(userId, {
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
@@ -100,7 +103,7 @@ export const authPaswordServiceFactory = ({
|
||||
* Email password reset flow via email. Step 1 send email
|
||||
*/
|
||||
const sendPasswordResetEmail = async (email: string) => {
|
||||
const user = await authDal.getUserByEmail(email);
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||
if (!user || (user && !user.isAccepted)) return;
|
||||
|
||||
@@ -127,7 +130,7 @@ export const authPaswordServiceFactory = ({
|
||||
* */
|
||||
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
||||
const cfg = getConfig();
|
||||
const user = await authDal.getUserByEmail(email);
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||
if (!user || (user && !user.isAccepted)) {
|
||||
throw new Error("Failed email verification for pass reset");
|
||||
@@ -166,7 +169,7 @@ export const authPaswordServiceFactory = ({
|
||||
encryptedPrivateKeyTag
|
||||
}: TResetPasswordViaBackupKeyDTO
|
||||
) => {
|
||||
await authDal.updateUserEncryptionByUserId(userId, {
|
||||
await userDal.updateUserEncryptionByUserId(userId, {
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
@@ -193,23 +196,23 @@ export const authPaswordServiceFactory = ({
|
||||
tag,
|
||||
userId
|
||||
}: TCreateBackupPrivateKeyDTO) => {
|
||||
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||
if (!user || (user && !user.isAccepted)) {
|
||||
const userEnc = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||
throw new Error("Failed to find user");
|
||||
}
|
||||
|
||||
if (!user.clientPublicKey || !user.serverPrivateKey)
|
||||
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey)
|
||||
throw new Error("failed to create backup key");
|
||||
const isValidClientProff = await srpCheckClientProof(
|
||||
user.salt,
|
||||
user.verifier,
|
||||
user.serverPrivateKey,
|
||||
user.clientPublicKey,
|
||||
userEnc.salt,
|
||||
userEnc.verifier,
|
||||
userEnc.serverPrivateKey,
|
||||
userEnc.clientPublicKey,
|
||||
clientProof
|
||||
);
|
||||
if (!isValidClientProff) throw new Error("failed to create backup key");
|
||||
const backup = await authDal.transaction(async (tx) => {
|
||||
const backupKey = await authDal.upsertBackupKey(user.id, {
|
||||
const backupKey = await authDal.upsertBackupKey(userEnc.userId, {
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
@@ -217,8 +220,8 @@ export const authPaswordServiceFactory = ({
|
||||
verifier
|
||||
});
|
||||
|
||||
await authDal.updateUserEncryptionByUserId(
|
||||
user.id,
|
||||
await userDal.updateUserEncryptionByUserId(
|
||||
userEnc.userId,
|
||||
{
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
@@ -235,7 +238,7 @@ export const authPaswordServiceFactory = ({
|
||||
* Return user back up
|
||||
* */
|
||||
const getBackupPrivateKeyOfUser = async (userId: string) => {
|
||||
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||
const user = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!user || (user && !user.isAccepted)) {
|
||||
throw new Error("Failed to find user");
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipStatus } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
|
||||
import { TOrgDalFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TUserDalFactory } from "../user/user-dal";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import { TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthSignupDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
userDal: TUserDalFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
orgDal: TOrgDalFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
@@ -19,8 +27,11 @@ type TAuthSignupDep = {
|
||||
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||
export const authSignupServiceFactory = ({
|
||||
authDal,
|
||||
userDal,
|
||||
tokenService,
|
||||
smtpService
|
||||
smtpService,
|
||||
orgService,
|
||||
orgDal
|
||||
}: TAuthSignupDep) => {
|
||||
// first step of signup. create user and send email
|
||||
const beginEmailSignupProcess = async (email: string) => {
|
||||
@@ -29,13 +40,13 @@ export const authSignupServiceFactory = ({
|
||||
throw new Error("Provided a disposable email");
|
||||
}
|
||||
|
||||
let user = await authDal.getUserByEmail(email);
|
||||
let user = await userDal.findUserByEmail(email);
|
||||
if (user && user.isAccepted) {
|
||||
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
|
||||
throw new Error("Failed to send verification code for complete account");
|
||||
}
|
||||
if (!user) {
|
||||
user = await authDal.createUser(email, { authMethods: [AuthMethod.EMAIL] });
|
||||
user = await userDal.create({ authMethods: [AuthMethod.EMAIL], email });
|
||||
}
|
||||
if (!user) throw new Error("Failed to create user");
|
||||
|
||||
@@ -55,7 +66,7 @@ export const authSignupServiceFactory = ({
|
||||
};
|
||||
|
||||
const verifyEmailSignup = async (email: string, code: string) => {
|
||||
const user = await authDal.getUserByEmail(email);
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
|
||||
throw new Error("Failed to send verification code for complete account");
|
||||
@@ -91,7 +102,7 @@ export const authSignupServiceFactory = ({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
// organizationName,
|
||||
organizationName,
|
||||
// attributionSource,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
@@ -99,19 +110,15 @@ export const authSignupServiceFactory = ({
|
||||
ip,
|
||||
userAgent
|
||||
}: TCompleteAccountSignupDTO) => {
|
||||
const user = await authDal.getUserByEmail(email);
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
throw new Error("Failed to complete account for complete user");
|
||||
}
|
||||
|
||||
const updateduser = await authDal.transaction(async (tx) => {
|
||||
const us = await authDal.updateUserById(
|
||||
user.id,
|
||||
{ firstName, lastName, isAccepted: true },
|
||||
tx
|
||||
);
|
||||
const us = await userDal.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await authDal.upsertUserEncryptionKey(
|
||||
const userEncKey = await userDal.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
@@ -129,7 +136,116 @@ export const authSignupServiceFactory = ({
|
||||
return { info: us, key: userEncKey };
|
||||
});
|
||||
|
||||
// TODO(akhilmhdh-pg): add default org memberships
|
||||
const hasSamlEnabled = user?.authMethods?.some((authMethod) =>
|
||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(
|
||||
authMethod as AuthMethod
|
||||
)
|
||||
);
|
||||
|
||||
if (!hasSamlEnabled) {
|
||||
await orgService.createOrganization(user.id, organizationName);
|
||||
}
|
||||
|
||||
await orgDal.updateMembership(
|
||||
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
||||
{ userId: user.id, status: OrgMembershipStatus.Accepted }
|
||||
);
|
||||
|
||||
const tokenSession = await tokenService.getUserTokenSession({
|
||||
userAgent,
|
||||
ip,
|
||||
userId: updateduser.info.id
|
||||
});
|
||||
if (!tokenSession) throw new Error("Failed to create token");
|
||||
const appCfg = getConfig();
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
accessVersion: tokenSession.accessVersion
|
||||
},
|
||||
appCfg.JWT_AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
refreshVersion: tokenSession.refreshVersion
|
||||
},
|
||||
appCfg.JWT_AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
||||
);
|
||||
|
||||
return { user: updateduser.info, accessToken, refreshToken };
|
||||
};
|
||||
|
||||
/*
|
||||
* User signup flow when they are invited to join the org
|
||||
* */
|
||||
const completeAccountInvite = async ({
|
||||
ip,
|
||||
salt,
|
||||
email,
|
||||
verifier,
|
||||
firstName,
|
||||
publicKey,
|
||||
userAgent,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
throw new Error("Failed to complete account for complete user");
|
||||
}
|
||||
|
||||
const [orgMembership] = await orgDal.findMembership({
|
||||
inviteEmail: email,
|
||||
status: OrgMembershipStatus.Invited
|
||||
});
|
||||
if (!orgMembership)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find invitation for email",
|
||||
name: "complete account invite"
|
||||
});
|
||||
|
||||
const updateduser = await authDal.transaction(async (tx) => {
|
||||
const us = await userDal.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await userDal.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
encryptionVersion: 2,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await orgDal.updateMembership(
|
||||
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
||||
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
||||
tx
|
||||
);
|
||||
return { info: us, key: userEncKey };
|
||||
});
|
||||
|
||||
const tokenSession = await tokenService.getUserTokenSession({
|
||||
userAgent,
|
||||
ip,
|
||||
@@ -166,6 +282,7 @@ export const authSignupServiceFactory = ({
|
||||
return {
|
||||
beginEmailSignupProcess,
|
||||
verifyEmailSignup,
|
||||
completeEmailAccountSignup
|
||||
completeEmailAccountSignup,
|
||||
completeAccountInvite
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,3 +17,20 @@ export type TCompleteAccountSignupDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
export type TCompleteAccountInviteDTO = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
@@ -33,3 +33,10 @@ export type AuthModeJwtTokenPayload = {
|
||||
tokenVersionId: string;
|
||||
accessVersion: number;
|
||||
};
|
||||
|
||||
export type AuthModeRefreshJwtTokenPayload = {
|
||||
authTokenType: AuthTokenType.REFRESH_TOKEN;
|
||||
userId: string;
|
||||
tokenVersionId: string;
|
||||
refreshVersion: number;
|
||||
};
|
||||
|
||||
57
backend-pg/src/services/org/incident-contacts-dal.ts
Normal file
57
backend-pg/src/services/org/incident-contacts-dal.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TIncidentContacts } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
|
||||
export type TIncidentContactsDalFactory = ReturnType<typeof incidentContactDalFactory>;
|
||||
|
||||
export const incidentContactDalFactory = (db: TDbClient) => {
|
||||
const create = async (orgId: string, email: string) => {
|
||||
try {
|
||||
const [incidentContact] = await db(TableName.IncidentContact)
|
||||
.insert({ orgId, email })
|
||||
.returning("*");
|
||||
return incidentContact;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Incident contact create", error });
|
||||
}
|
||||
};
|
||||
|
||||
const findByOrgId = async (orgId: string) => {
|
||||
try {
|
||||
const incidentContacts = await db(TableName.IncidentContact).where({ orgId });
|
||||
return incidentContacts;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Incident contact list", error });
|
||||
}
|
||||
};
|
||||
|
||||
const findOne = async (orgId: string, data: Partial<TIncidentContacts>) => {
|
||||
try {
|
||||
const incidentContacts = await db(TableName.IncidentContact)
|
||||
.where({ orgId, ...data })
|
||||
.first();
|
||||
return incidentContacts;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Incident contact find one", error });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteById = async (id: string, orgId: string) => {
|
||||
try {
|
||||
const [incidentContact] = await db(TableName.IncidentContact)
|
||||
.where({ orgId, id })
|
||||
.delete()
|
||||
.returning("*");
|
||||
return incidentContact;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Incident contact delete", error });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findByOrgId,
|
||||
findOne,
|
||||
create,
|
||||
deleteById
|
||||
};
|
||||
};
|
||||
184
backend-pg/src/services/org/org-dal.ts
Normal file
184
backend-pg/src/services/org/org-dal.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TOrganizations,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { withTransaction } from "@app/lib/knex";
|
||||
|
||||
export type TOrgDalFactory = ReturnType<typeof orgDalFactory>;
|
||||
|
||||
export const orgDalFactory = (db: TDbClient) => {
|
||||
const findOrgById = async (orgId: string) => {
|
||||
try {
|
||||
const org = await db(TableName.Organization).where({ id: orgId }).first();
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org by id" });
|
||||
}
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => {
|
||||
try {
|
||||
const org = await db(TableName.OrgMembership)
|
||||
.where({ userId })
|
||||
.join(
|
||||
TableName.Organization,
|
||||
`${TableName.OrgMembership}.orgId`,
|
||||
`${TableName.Organization}.id`
|
||||
)
|
||||
.select(`${TableName.Organization}.*`);
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org by user id" });
|
||||
}
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllOrgMembers = async (orgId: string) => {
|
||||
try {
|
||||
const members = await db(TableName.OrgMembership)
|
||||
.where({ orgId })
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
);
|
||||
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { email, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({ name }: { name: string }, tx?: Knex) => {
|
||||
try {
|
||||
const [organization] = await (tx || db)(TableName.Organization)
|
||||
.insert({ name })
|
||||
.returning("*");
|
||||
return organization;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create organization" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteById = async (orgId: string, tx?: Knex) => {
|
||||
try {
|
||||
const [org] = await (tx || db)(TableName.Organization)
|
||||
.where({ id: orgId })
|
||||
.delete()
|
||||
.returning("*");
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update organization" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = async (orgId: string, data: Partial<TOrganizations>) => {
|
||||
try {
|
||||
const [org] = await db(TableName.Organization)
|
||||
.where({ id: orgId })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
return org;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update organization" });
|
||||
}
|
||||
};
|
||||
|
||||
// MEMBERSHIP OPERATIONS
|
||||
// --------------------
|
||||
const findMembership = async (filter: Partial<TOrgMemberships>, tx?: Knex) => {
|
||||
try {
|
||||
const membership = await (tx || db)(TableName.OrgMembership).where(filter);
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org membership" });
|
||||
}
|
||||
};
|
||||
|
||||
const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [membership] = await (tx || db)(TableName.OrgMembership).insert(data).returning("*");
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create org membership" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const [membership] = await (tx || db)(TableName.OrgMembership)
|
||||
.where({ id })
|
||||
.update(data)
|
||||
.returning("*");
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update org membership" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateMembership = async (
|
||||
filter: Partial<TOrgMemberships>,
|
||||
data: TOrgMembershipsUpdate,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const membership = await (tx || db)(TableName.OrgMembership)
|
||||
.where(filter)
|
||||
.update(data)
|
||||
.returning("*");
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update org memberships" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMembershipById = async (id: string, orgId: string, tx?: Knex) => {
|
||||
try {
|
||||
const [membership] = await (tx || db)(TableName.OrgMembership)
|
||||
.where({ id, orgId })
|
||||
.delete()
|
||||
.returning("*");
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete org membership" });
|
||||
}
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
findAllOrgMembers,
|
||||
findOrgById,
|
||||
findAllOrgsByUserId,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
findMembership,
|
||||
createMembership,
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
updateMembership
|
||||
});
|
||||
};
|
||||
67
backend-pg/src/services/org/org-role-dal.ts
Normal file
67
backend-pg/src/services/org/org-role-dal.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName,TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { withTransaction } from "@app/lib/knex";
|
||||
|
||||
export type TOrgRoleDalFactory = ReturnType<typeof orgRoleDalFactory>;
|
||||
|
||||
export const orgRoleDalFactory = (db: TDbClient) => {
|
||||
const find = async (data: TOrgRolesUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const role = await (tx || db)(TableName.OrgRoles).where(data);
|
||||
return role;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Org role find one" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOne = async (data: TOrgRolesUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const role = await (tx || db)(TableName.OrgRoles).where(data).first();
|
||||
return role;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Org role find one" });
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: TOrgRolesInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [role] = await (tx || db)(TableName.OrgRoles).insert(data).returning("*");
|
||||
return role;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Org role create" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateOne = async (
|
||||
filter: { id: string; orgId: string },
|
||||
data: TOrgRolesUpdate,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const [role] = await (tx || db)(TableName.OrgRoles).where(filter).update(data).returning("*");
|
||||
return role;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Org role create" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteOne = async (filter: { id: string; orgId: string }, tx?: Knex) => {
|
||||
try {
|
||||
const [role] = await (tx || db)(TableName.OrgRoles).where(filter).delete().returning("*");
|
||||
return role;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Org role create" });
|
||||
}
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
find,
|
||||
findOne,
|
||||
create,
|
||||
updateOne,
|
||||
deleteOne
|
||||
});
|
||||
};
|
||||
120
backend-pg/src/services/org/org-role-service.ts
Normal file
120
backend-pg/src/services/org/org-role-service.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
|
||||
import {
|
||||
orgAdminPermissions,
|
||||
orgMemberPermissions,
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TOrgRoleDalFactory } from "./org-role-dal";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
type TOrgRoleServiceFactoryDep = {
|
||||
orgRoleDal: TOrgRoleDalFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
||||
|
||||
export const orgRoleServiceFactory = ({
|
||||
orgRoleDal,
|
||||
permissionService
|
||||
}: TOrgRoleServiceFactoryDep) => {
|
||||
const createRole = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
data: Omit<TOrgRolesInsert, "orgId">
|
||||
) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Role
|
||||
);
|
||||
const existingRole = await orgRoleDal.findOne({ slug: data.slug, orgId });
|
||||
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
||||
|
||||
const role = await orgRoleDal.create({ ...data, orgId });
|
||||
return role;
|
||||
};
|
||||
|
||||
const updateRole = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
roleId: string,
|
||||
data: Omit<TOrgRolesUpdate, "orgId">
|
||||
) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Role
|
||||
);
|
||||
if (data?.slug) {
|
||||
const existingRole = await orgRoleDal.findOne({ slug: data.slug, orgId });
|
||||
if (existingRole && existingRole.id !== roleId)
|
||||
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||
}
|
||||
const updatedRole = await orgRoleDal.updateOne({ id: roleId, orgId }, { ...data });
|
||||
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||
return updatedRole;
|
||||
};
|
||||
|
||||
const deleteRole = async (userId: string, orgId: string, roleId: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Role
|
||||
);
|
||||
const deletedRole = await orgRoleDal.deleteOne({ id: roleId, orgId });
|
||||
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||
|
||||
return deletedRole;
|
||||
};
|
||||
|
||||
const listRoles = async (userId: string, orgId: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Role
|
||||
);
|
||||
const customRoles = await orgRoleDal.find({ orgId });
|
||||
const roles = [
|
||||
{
|
||||
id: "admin",
|
||||
orgId: "",
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
description: "Complete administration access over the organization",
|
||||
permissions: packRules(orgAdminPermissions.rules),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "member",
|
||||
orgId: "",
|
||||
name: "Member",
|
||||
slug: "member",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: packRules(orgMemberPermissions.rules),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
...(customRoles || []).map(({ permissions, ...data }) => ({
|
||||
...data,
|
||||
permissions
|
||||
}))
|
||||
];
|
||||
|
||||
return roles;
|
||||
};
|
||||
|
||||
const getUserPermission = async (userId: string, orgId: string) => {
|
||||
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
return { permissions: packRules(permission.rules), membership };
|
||||
};
|
||||
|
||||
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
|
||||
};
|
||||
368
backend-pg/src/services/org/org-service.ts
Normal file
368
backend-pg/src/services/org/org-service.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
|
||||
import { AuthTokenType } from "../auth/auth-type";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TUserDalFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDalFactory } from "./incident-contacts-dal";
|
||||
import { TOrgDalFactory } from "./org-dal";
|
||||
import { TOrgRoleDalFactory } from "./org-role-dal";
|
||||
import {
|
||||
TDeleteOrgMembershipDTO,
|
||||
TInviteUserToOrgDTO,
|
||||
TUpdateOrgMembershipDTO,
|
||||
TVerifyUserToOrgDTO
|
||||
} from "./org-types";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
type TOrgServiceFactoryDep = {
|
||||
orgDal: TOrgDalFactory;
|
||||
orgRoleDal: TOrgRoleDalFactory;
|
||||
userDal: TUserDalFactory;
|
||||
incidentContactDal: TIncidentContactsDalFactory;
|
||||
smtpService: TSmtpService;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||
|
||||
export const orgServiceFactory = ({
|
||||
orgDal,
|
||||
userDal,
|
||||
orgRoleDal,
|
||||
incidentContactDal,
|
||||
permissionService,
|
||||
smtpService,
|
||||
tokenService
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
* */
|
||||
const findOrganizationById = async (userId: string, orgId: string) => {
|
||||
await permissionService.getUserOrgPermission(userId, orgId);
|
||||
const org = await orgDal.findOrgById(orgId);
|
||||
if (!org)
|
||||
throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||
return org;
|
||||
};
|
||||
/*
|
||||
* Get all organization a user part of
|
||||
* */
|
||||
const findAllOrganizationOfUser = async (userId: string) => {
|
||||
const orgs = await orgDal.findAllOrgsByUserId(userId);
|
||||
return orgs;
|
||||
};
|
||||
/*
|
||||
* Get all workspace members
|
||||
* */
|
||||
const findAllOrgMembers = async (userId: string, orgId: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const members = await orgDal.findAllOrgMembers(orgId);
|
||||
return members;
|
||||
};
|
||||
/*
|
||||
* Update organization settings
|
||||
* */
|
||||
const updateOrgName = async (userId: string, orgId: string, name: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Settings
|
||||
);
|
||||
const org = await orgDal.updateById(orgId, { name });
|
||||
if (!org)
|
||||
throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||
return org;
|
||||
};
|
||||
/*
|
||||
* Create organization
|
||||
* */
|
||||
const createOrganization = async (userId: string, orgName: string) => {
|
||||
const organization = await orgDal.transaction(async (tx) => {
|
||||
const org = await orgDal.create({ name: orgName }, tx);
|
||||
await orgDal.createMembership(
|
||||
{
|
||||
userId,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
return org;
|
||||
});
|
||||
|
||||
return organization;
|
||||
};
|
||||
|
||||
/*
|
||||
* Delete organization by id
|
||||
* */
|
||||
const deleteOrganizationById = async (userId: string, orgId: string) => {
|
||||
const { membership } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
if (membership.role !== OrgMembershipRole.Admin)
|
||||
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
|
||||
|
||||
const organization = await orgDal.deleteById(orgId);
|
||||
return organization;
|
||||
};
|
||||
/*
|
||||
* Org membership management
|
||||
* Not another service because it has close ties with how an org works doesn't make sense to seperate them
|
||||
* */
|
||||
const updateOrgMembership = async ({
|
||||
role,
|
||||
orgId,
|
||||
userId,
|
||||
membershipId
|
||||
}: TUpdateOrgMembershipDTO) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const customRole = await orgRoleDal.findOne({ slug: role, orgId });
|
||||
if (!customRole)
|
||||
throw new BadRequestError({ name: "Update membership", message: "Role not found" });
|
||||
const membership = await orgDal.updateMembershipById(membershipId, {
|
||||
role: OrgMembershipRole.Custom,
|
||||
roleId: customRole.id
|
||||
});
|
||||
return membership;
|
||||
}
|
||||
|
||||
const membership = await orgDal.updateMembershipById(membershipId, { role, roleId: null });
|
||||
return membership;
|
||||
};
|
||||
/*
|
||||
* Invite user to organization
|
||||
*/
|
||||
const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail }: TInviteUserToOrgDTO) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
// TODO(akhilmhdh-pg): SAML SSO check and licence check limit org members
|
||||
const invitee = await orgDal.transaction(async (tx) => {
|
||||
const inviteeUser = await userDal.findUserByEmail(inviteeEmail, tx);
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDal.findMembership(
|
||||
{ orgId, userId: inviteeUser.id },
|
||||
tx
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite an existing member of org",
|
||||
name: "Invite user to org"
|
||||
});
|
||||
}
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDal.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return inviteeUser;
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDal.create({ email: inviteeEmail, isAccepted: false });
|
||||
await orgDal.createMembership({
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
});
|
||||
return user;
|
||||
});
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
});
|
||||
|
||||
const org = await orgDal.findOrgById(orgId);
|
||||
const user = await userDal.findById(userId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: user.firstName,
|
||||
inviterEmail: user.email,
|
||||
organizationName: org?.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
|
||||
if (!appCfg.isSmtpConfigured) {
|
||||
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
||||
* magic link and issue a temporary signup token for user to complete setting up their account
|
||||
*/
|
||||
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
|
||||
const user = await userDal.findUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
|
||||
}
|
||||
const [orgMembership] = await orgDal.findMembership({
|
||||
userId: user.id,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
orgId
|
||||
});
|
||||
if (!orgMembership)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find invitation",
|
||||
name: "Verify user to org"
|
||||
});
|
||||
|
||||
await tokenService.validateTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: user.id,
|
||||
orgId: orgMembership.orgId,
|
||||
code
|
||||
});
|
||||
|
||||
if (user.isAccepted) {
|
||||
// this means user has already completed signup process
|
||||
// isAccepted is set true when keys are exchanged
|
||||
await orgDal.updateMembershipById(orgMembership.id, {
|
||||
orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
// TODO(akhilmhdh-pg): update org licence subscription
|
||||
return { user };
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const token = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user.id
|
||||
},
|
||||
appCfg.JWT_AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_SIGNUP_LIFETIME
|
||||
}
|
||||
);
|
||||
|
||||
return { token, user };
|
||||
};
|
||||
|
||||
const deleteOrgMembership = async ({ orgId, userId, membershipId }: TDeleteOrgMembershipDTO) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const membership = await orgDal.deleteMembershipById(membershipId, orgId);
|
||||
return membership;
|
||||
};
|
||||
|
||||
/*
|
||||
* CRUD operations of incident contacts
|
||||
* */
|
||||
const findIncidentContacts = async (userId: string, orgId: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
const incidentContacts = await incidentContactDal.findByOrgId(orgId);
|
||||
return incidentContacts;
|
||||
};
|
||||
|
||||
const createIncidentContact = async (userId: string, orgId: string, email: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
const doesIncidentContactExist = await incidentContactDal.findOne(orgId, { email });
|
||||
if (doesIncidentContactExist) {
|
||||
throw new BadRequestError({
|
||||
message: "Incident contact already exist",
|
||||
name: "Incident contact exist"
|
||||
});
|
||||
}
|
||||
|
||||
const incidentContact = await incidentContactDal.create(orgId, email);
|
||||
return incidentContact;
|
||||
};
|
||||
|
||||
const deleteIncidentContact = async (userId: string, orgId: string, id: string) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContact = await incidentContactDal.deleteById(id, orgId);
|
||||
return incidentContact;
|
||||
};
|
||||
|
||||
return {
|
||||
findOrganizationById,
|
||||
findAllOrgMembers,
|
||||
findAllOrganizationOfUser,
|
||||
inviteUserToOrganization,
|
||||
verifyUserToOrg,
|
||||
updateOrgName,
|
||||
createOrganization,
|
||||
deleteOrganizationById,
|
||||
deleteOrgMembership,
|
||||
updateOrgMembership,
|
||||
// incident contacts
|
||||
findIncidentContacts,
|
||||
createIncidentContact,
|
||||
deleteIncidentContact
|
||||
};
|
||||
};
|
||||
24
backend-pg/src/services/org/org-types.ts
Normal file
24
backend-pg/src/services/org/org-types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type TUpdateOrgMembershipDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
membershipId: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type TDeleteOrgMembershipDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
membershipId: string;
|
||||
};
|
||||
|
||||
export type TInviteUserToOrgDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
inviteeEmail: string;
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
email: string;
|
||||
orgId: string;
|
||||
code: string;
|
||||
};
|
||||
7
backend-pg/src/services/server-cfg/server-cfg-dal.ts
Normal file
7
backend-pg/src/services/server-cfg/server-cfg-dal.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TServerCfgDalFactory = ReturnType<typeof serverCfgDalFactory>;
|
||||
|
||||
export const serverCfgDalFactory = (db: TDbClient) => ormify(db, TableName.ServerConfig, {});
|
||||
106
backend-pg/src/services/server-cfg/server-cfg-service.ts
Normal file
106
backend-pg/src/services/server-cfg/server-cfg-service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { TServerConfig, TServerConfigUpdate } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
import { TUserDalFactory } from "../user/user-dal";
|
||||
import { TServerCfgDalFactory } from "./server-cfg-dal";
|
||||
import { TAdminSignUpDTO } from "./server-cfg-types";
|
||||
|
||||
type TServerCfgServiceFactoryDep = {
|
||||
serverCfgDal: TServerCfgDalFactory;
|
||||
userDal: TUserDalFactory;
|
||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||
};
|
||||
|
||||
export type TServerCfgServiceFactory = ReturnType<typeof serverCfgServiceFactory>;
|
||||
|
||||
export const serverCfgServiceFactory = ({
|
||||
serverCfgDal,
|
||||
userDal,
|
||||
authService
|
||||
}: TServerCfgServiceFactoryDep) => {
|
||||
let serverCfg: TServerConfig;
|
||||
|
||||
const initServerCfg = async () => {
|
||||
serverCfg = await serverCfgDal.findOne({});
|
||||
if (!serverCfg) {
|
||||
const newCfg = await serverCfgDal.create({ initialized: true, allowSignUp: true });
|
||||
serverCfg = newCfg;
|
||||
return newCfg;
|
||||
}
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const getServerCfg = () => {
|
||||
if (!serverCfg)
|
||||
throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" });
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (data: TServerConfigUpdate) => {
|
||||
const cfg = await serverCfgDal.updateById(serverCfg.id, data);
|
||||
return cfg;
|
||||
};
|
||||
|
||||
const adminSignUp = async ({
|
||||
lastName,
|
||||
firstName,
|
||||
salt,
|
||||
email,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
ip,
|
||||
userAgent
|
||||
}: TAdminSignUpDTO) => {
|
||||
const existingUser = await userDal.findOne({ email });
|
||||
if (!existingUser)
|
||||
throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
|
||||
|
||||
const userInfo = await userDal.transaction(async (tx) => {
|
||||
const newUser = await userDal.create(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
superAdmin: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
const userEnc = await userDal.createUserEncryption(
|
||||
{
|
||||
salt,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
verifier,
|
||||
userId: newUser.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { user: newUser, enc: userEnc };
|
||||
});
|
||||
|
||||
await updateServerCfg({ initialized: true });
|
||||
const token = await authService.generateUserTokens(userInfo.user, ip, userAgent);
|
||||
// TODO(akhilmhdh-pg): telemetry service
|
||||
return { token, user: userInfo };
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
getServerCfg,
|
||||
updateServerCfg,
|
||||
adminSignUp
|
||||
};
|
||||
};
|
||||
16
backend-pg/src/services/server-cfg/server-cfg-types.ts
Normal file
16
backend-pg/src/services/server-cfg/server-cfg-types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type TAdminSignUpDTO = {
|
||||
email: string;
|
||||
publicKey: string;
|
||||
salt: string;
|
||||
lastName?: string;
|
||||
verifier: string;
|
||||
firstName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
@@ -1,55 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import {
|
||||
TDeleteTokenForUserDalDTO,
|
||||
TGetTokenForUserDalDTO,
|
||||
TUpsertTokenForUserDalDTO
|
||||
} from "./token-types";
|
||||
import { TDeleteTokenForUserDalDTO } from "./token-types";
|
||||
|
||||
export type TTokenDalConfig = {};
|
||||
|
||||
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
|
||||
|
||||
// TODO(akhilmhdh-pg): wrap all with database error
|
||||
export const tokenDalFactory = (db: TDbClient) => {
|
||||
const upsertTokenForUser = async ({
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
userId,
|
||||
type,
|
||||
triesLeft
|
||||
}: TUpsertTokenForUserDalDTO): Promise<TAuthTokens | undefined> => {
|
||||
const token = await db.transaction(async (tx) => {
|
||||
await tx(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||
const [newToken] = await tx(TableName.AuthTokens)
|
||||
.insert({ tokenHash, expiresAt: expiresAt.toUTCString(), type, userId, triesLeft })
|
||||
.returning("*");
|
||||
return newToken;
|
||||
});
|
||||
return token;
|
||||
};
|
||||
const authOrm = ormify(db, TableName.AuthTokens);
|
||||
|
||||
const getTokenForUser = async ({
|
||||
userId,
|
||||
type
|
||||
}: TGetTokenForUserDalDTO): Promise<TAuthTokens | undefined> =>
|
||||
db(TableName.AuthTokens).where({ userId, type }).first();
|
||||
|
||||
const getTokenSession = async (
|
||||
userId: string,
|
||||
ip: string,
|
||||
userAgent: string
|
||||
const findOneTokenSession = async (
|
||||
filter: Partial<TAuthTokenSessions>
|
||||
): Promise<TAuthTokenSessions | undefined> =>
|
||||
db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first();
|
||||
|
||||
const getTokenSessionById = async (id: string, userId: string) =>
|
||||
db(TableName.AuthTokenSession).where({ id, userId }).first();
|
||||
db(TableName.AuthTokenSession).where(filter).first();
|
||||
|
||||
const deleteTokenForUser = async ({
|
||||
userId,
|
||||
type
|
||||
type,
|
||||
orgId
|
||||
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> =>
|
||||
db(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||
db(TableName.AuthTokens).where({ userId, type, orgId }).delete().returning("*");
|
||||
|
||||
const decrementTriesField = async ({
|
||||
userId,
|
||||
@@ -58,6 +34,15 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1);
|
||||
};
|
||||
|
||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||
try {
|
||||
const sessions = await (tx || db)(TableName.AuthTokenSession).where(filter);
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Find all token session", error });
|
||||
}
|
||||
};
|
||||
|
||||
const insertTokenSession = async (
|
||||
userId: string,
|
||||
ip: string,
|
||||
@@ -70,13 +55,13 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
userAgent,
|
||||
accessVersion: 1,
|
||||
refreshVersion: 1,
|
||||
lastUsed: new Date().toUTCString()
|
||||
lastUsed: new Date()
|
||||
})
|
||||
.returning("*");
|
||||
return session;
|
||||
};
|
||||
|
||||
const incrementVersion = async (
|
||||
const incrementTokenSessionVersion = async (
|
||||
userId: string,
|
||||
sessionId: string
|
||||
): Promise<TAuthTokenSessions | undefined> => {
|
||||
@@ -88,14 +73,26 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
return session;
|
||||
};
|
||||
|
||||
const deleteTokenSession = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||
try {
|
||||
const sessions = await (tx || db)(TableName.AuthTokenSession)
|
||||
.where(filter)
|
||||
.del()
|
||||
.returning("*");
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Delete token session", error });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getTokenForUser,
|
||||
getTokenSessionById,
|
||||
upsertTokenForUser,
|
||||
...authOrm,
|
||||
findTokenSessions,
|
||||
deleteTokenForUser,
|
||||
decrementTriesField,
|
||||
getTokenSession,
|
||||
findOneTokenSession,
|
||||
insertTokenSession,
|
||||
incrementVersion
|
||||
incrementTokenSessionVersion,
|
||||
deleteTokenSession
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,30 +56,37 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId }: TCreateTokenForUserDTO) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
|
||||
await tokenDal.upsertTokenForUser({
|
||||
userId,
|
||||
type,
|
||||
expiresAt: tkCfg.expiresAt,
|
||||
tokenHash,
|
||||
triesLeft: tkCfg?.triesLeft
|
||||
await tokenDal.transaction(async (tx) => {
|
||||
await tokenDal.delete({ userId, type, orgId: orgId || null }, tx);
|
||||
const newToken = await tokenDal.create({
|
||||
tokenHash,
|
||||
expiresAt: tkCfg.expiresAt.toUTCString(),
|
||||
type,
|
||||
userId,
|
||||
orgId,
|
||||
triesLeft: tkCfg?.triesLeft
|
||||
});
|
||||
return newToken;
|
||||
});
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
const validateTokenForUser = async ({
|
||||
type,
|
||||
userId,
|
||||
code
|
||||
code,
|
||||
orgId
|
||||
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
|
||||
const token = await tokenDal.getTokenForUser({ type, userId });
|
||||
const token = await tokenDal.findOne({ type, userId, orgId: orgId || null });
|
||||
// validate token
|
||||
if (!token) throw new Error("Failed to find token");
|
||||
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) {
|
||||
await tokenDal.deleteTokenForUser({ type, userId });
|
||||
await tokenDal.delete({ type, userId, orgId });
|
||||
throw new Error("Token expired. Please try again");
|
||||
}
|
||||
|
||||
@@ -87,15 +94,15 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
|
||||
if (!isValidToken) {
|
||||
if (token?.triesLeft) {
|
||||
if (token.triesLeft === 1) {
|
||||
await tokenDal.deleteTokenForUser({ type, userId });
|
||||
await tokenDal.deleteTokenForUser({ type, userId, orgId: orgId || null });
|
||||
} else {
|
||||
await tokenDal.decrementTriesField({ type, userId });
|
||||
await tokenDal.decrementTriesField({ type, userId, orgId: orgId || null });
|
||||
}
|
||||
}
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
const deletedToken = await tokenDal.deleteTokenForUser({ type, userId });
|
||||
const deletedToken = await tokenDal.delete({ type, userId, orgId: orgId || null });
|
||||
return deletedToken?.[0];
|
||||
};
|
||||
|
||||
@@ -104,7 +111,7 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
|
||||
ip,
|
||||
userAgent
|
||||
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
|
||||
let session = await tokenDal.getTokenSession(userId, ip, userAgent);
|
||||
let session = await tokenDal.findOneTokenSession({ userId, ip, userAgent });
|
||||
if (!session) {
|
||||
session = await tokenDal.insertTokenSession(userId, ip, userAgent);
|
||||
}
|
||||
@@ -112,18 +119,25 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
|
||||
};
|
||||
|
||||
const getUserTokenSessionById = async (id: string, userId: string) =>
|
||||
tokenDal.getTokenSessionById(id, userId);
|
||||
tokenDal.findOneTokenSession({ id, userId });
|
||||
|
||||
const clearTokenSessionById = async (
|
||||
userId: string,
|
||||
sessionId: string
|
||||
): Promise<TAuthTokenSessions | undefined> => tokenDal.incrementVersion(userId, sessionId);
|
||||
): Promise<TAuthTokenSessions | undefined> =>
|
||||
tokenDal.incrementTokenSessionVersion(userId, sessionId);
|
||||
|
||||
const getTokenSessionByUser = async (userId: string) => tokenDal.findTokenSessions({ userId });
|
||||
|
||||
const revokeAllMySessions = async (userId: string) => tokenDal.deleteTokenSession({ userId });
|
||||
|
||||
return {
|
||||
createTokenForUser,
|
||||
validateTokenForUser,
|
||||
getUserTokenSession,
|
||||
clearTokenSessionById,
|
||||
getUserTokenSessionById
|
||||
getUserTokenSessionById,
|
||||
getTokenSessionByUser,
|
||||
revokeAllMySessions
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,12 +8,19 @@ export enum TokenType {
|
||||
export type TCreateTokenForUserDTO = {
|
||||
type: TokenType;
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export type TCreateOrgInviteTokenDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TValidateTokenForUserDTO = {
|
||||
type: TokenType;
|
||||
code: string;
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export type TUpsertTokenForUserDalDTO = {
|
||||
@@ -32,6 +39,7 @@ export type TGetTokenForUserDalDTO = {
|
||||
export type TDeleteTokenForUserDalDTO = {
|
||||
userId: string;
|
||||
type: TokenType;
|
||||
orgId: string | null;
|
||||
};
|
||||
|
||||
export type TIssueAuthTokenDTO = {
|
||||
|
||||
108
backend-pg/src/services/user/user-dal.ts
Normal file
108
backend-pg/src/services/user/user-dal.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TUserActionsInsert,
|
||||
TUserActionsUpdate,
|
||||
TUserEncryptionKeys,
|
||||
TUserEncryptionKeysInsert,
|
||||
TUserEncryptionKeysUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TUserDalFactory = ReturnType<typeof userDalFactory>;
|
||||
|
||||
export const userDalFactory = (db: TDbClient) => {
|
||||
const userOrm = ormify(db, TableName.Users);
|
||||
const findUserByEmail = async (email: string, tx?: Knex) => userOrm.findOne({ email }, tx);
|
||||
|
||||
// USER ENCRYPTION FUNCTIONS
|
||||
// -------------------------
|
||||
const findUserEncKeyByEmail = async (email: string) =>
|
||||
db(TableName.Users)
|
||||
.where({ email })
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.Users}.id`,
|
||||
`${TableName.UserEncryptionKey}.userId`
|
||||
)
|
||||
.first();
|
||||
|
||||
const findUserEncKeyByUserId = async (userId: string) =>
|
||||
db(TableName.Users)
|
||||
.where({ [`${TableName.Users}.id`]: userId })
|
||||
.join(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.Users}.id`,
|
||||
`${TableName.UserEncryptionKey}.userId`
|
||||
)
|
||||
.first();
|
||||
|
||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||
return userEnc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create user encryption" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserEncryptionByUserId = async (
|
||||
userId: string,
|
||||
data: TUserEncryptionKeysUpdate,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey)
|
||||
.where({ userId })
|
||||
.update({ ...data })
|
||||
.returning("*");
|
||||
return userEnc;
|
||||
};
|
||||
|
||||
const upsertUserEncryptionKey = async (
|
||||
userId: string,
|
||||
data: Omit<TUserEncryptionKeysUpdate, "userId">,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||
// if user insert make sure to pass all required data
|
||||
.insert({ userId, ...data } as TUserEncryptionKeys)
|
||||
.onConflict("userId")
|
||||
.merge()
|
||||
.returning("*");
|
||||
return userEnc;
|
||||
};
|
||||
|
||||
// USER ACTION FUNCTIONS
|
||||
// ---------------------
|
||||
const findOneUserAction = (filter: TUserActionsUpdate, tx?: Knex) => {
|
||||
try {
|
||||
return (tx || db)(TableName.UserAction).where(filter).first("*");
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one user action" });
|
||||
}
|
||||
};
|
||||
|
||||
const createUserAction = async (data: TUserActionsInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [userAction] = await (tx || db)(TableName.UserAction).insert(data).returning("*");
|
||||
return userAction;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create user action" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...userOrm,
|
||||
findUserByEmail,
|
||||
findUserEncKeyByEmail,
|
||||
findUserEncKeyByUserId,
|
||||
updateUserEncryptionByUserId,
|
||||
upsertUserEncryptionKey,
|
||||
createUserEncryption,
|
||||
findOneUserAction,
|
||||
createUserAction
|
||||
};
|
||||
};
|
||||
84
backend-pg/src/services/user/user-service.ts
Normal file
84
backend-pg/src/services/user/user-service.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { AuthMethod } from "../auth/auth-type";
|
||||
import { TUserDalFactory } from "./user-dal";
|
||||
|
||||
type TUserServiceFactoryDep = {
|
||||
userDal: TUserDalFactory;
|
||||
};
|
||||
|
||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||
|
||||
export const userServiceFactory = ({ userDal }: TUserServiceFactoryDep) => {
|
||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
||||
const updatedUser = await userDal.updateById(userId, {
|
||||
isMfaEnabled,
|
||||
mfaMethods: isMfaEnabled ? ["email"] : []
|
||||
});
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
const updateUserName = async (userId: string, firstName: string, lastName: string) => {
|
||||
const updatedUser = await userDal.updateById(userId, {
|
||||
firstName,
|
||||
lastName
|
||||
});
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
const updateAuthMethods = async (userId: string, authMethods: AuthMethod[]) => {
|
||||
const user = await userDal.findById(userId);
|
||||
if (!user) throw new BadRequestError({ name: "Update auth methods" });
|
||||
|
||||
const hasSamlEnabled = user?.authMethods?.some((method) =>
|
||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(
|
||||
method as AuthMethod
|
||||
)
|
||||
);
|
||||
if (hasSamlEnabled)
|
||||
throw new BadRequestError({
|
||||
name: "Update auth method",
|
||||
message: "Failed to update auth methods due to SAML SSO "
|
||||
});
|
||||
|
||||
const updatedUser = await userDal.updateById(userId, { authMethods });
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
const getMe = async (userId: string) => {
|
||||
const user = await userDal.findUserEncKeyByUserId(userId);
|
||||
if (!user) throw new BadRequestError({ message: "user not found", name: "Get Me" });
|
||||
return user;
|
||||
};
|
||||
|
||||
const deleteMe = async (userId: string) => {
|
||||
const user = await userDal.deleteById(userId);
|
||||
return user;
|
||||
};
|
||||
|
||||
// user actions operations
|
||||
const createUserAction = async (userId: string, action: string) => {
|
||||
const userAction = await userDal.transaction(async (tx) => {
|
||||
const existingAction = await userDal.findOneUserAction({ action, userId }, tx);
|
||||
if (existingAction) return existingAction;
|
||||
return userDal.createUserAction({ action, userId }, tx);
|
||||
});
|
||||
|
||||
return userAction;
|
||||
};
|
||||
|
||||
const getUserAction = async (userId: string, action: string) => {
|
||||
const userAction = await userDal.findOneUserAction({ action, userId });
|
||||
return userAction;
|
||||
};
|
||||
|
||||
return {
|
||||
toggleUserMfa,
|
||||
updateUserName,
|
||||
updateAuthMethods,
|
||||
deleteMe,
|
||||
getMe,
|
||||
createUserAction,
|
||||
getUserAction
|
||||
};
|
||||
};
|
||||
0
backend-pg/src/services/user/user-types.ts
Normal file
0
backend-pg/src/services/user/user-types.ts
Normal file
@@ -22,6 +22,6 @@
|
||||
"@server/*": ["./src/server/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*","scripts/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { requireAuth, validateRequest } from "../../middleware";
|
||||
import { membershipOrgController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
// depreciated completely
|
||||
// ignored for new codebase
|
||||
router.post(
|
||||
// TODO endpoint: check dashboard
|
||||
"/membershipOrg/:membershipOrgId/change-role",
|
||||
|
||||
@@ -25,6 +25,7 @@ services:
|
||||
POSTGRES_DB: infisical
|
||||
|
||||
backend:
|
||||
container_name: infisical-dev-api
|
||||
build:
|
||||
context: ./backend-pg
|
||||
dockerfile: Dockerfile.dev
|
||||
|
||||
@@ -72,7 +72,7 @@ export const completeAccountSignup = async (details: CompleteAccountSignupDTO) =
|
||||
};
|
||||
|
||||
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
|
||||
const { data } = await apiRequest.post("/api/v3/signup/complete-account/invite", details);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ export const useDeleteIncidentContact = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteIncidentContactDTO>({
|
||||
mutationFn: async ({ orgId, email }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
|
||||
data: { email }
|
||||
});
|
||||
mutationFn: async ({ orgId, incidentContactId }) => {
|
||||
const { data } = await apiRequest.delete(
|
||||
`/api/v1/organization/${orgId}/incidentContactOrg/${incidentContactId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export type IncidentContact = {
|
||||
|
||||
export type DeleteIncidentContactDTO = {
|
||||
orgId: string;
|
||||
email: string;
|
||||
incidentContactId: string;
|
||||
};
|
||||
|
||||
export type AddIncidentContactDTO = {
|
||||
|
||||
@@ -67,13 +67,11 @@ const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
|
||||
if (orgId === "") return { permissions: [], membership: null };
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
data: {
|
||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||
membership: OrgUser;
|
||||
};
|
||||
}>(`/api/v1/roles/organization/${orgId}/permissions`);
|
||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||
membership: OrgUser;
|
||||
}>(`/api/ee/v1/organization/${orgId}/permissions`);
|
||||
|
||||
return data.data;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
||||
|
||||
@@ -82,13 +82,14 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
// TODO(akhilmhdh): bring back server config later
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
<ServerConfigProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</ServerConfigProvider>
|
||||
</NotificationProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setLoginError(true);
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||
@@ -236,7 +237,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex flex-row text-sm text-bunker-400 mt-2">
|
||||
<div className="mt-2 flex flex-row text-sm text-bunker-400">
|
||||
<Link href="/verify-email">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Forgot password? Recover your account
|
||||
|
||||
@@ -35,12 +35,12 @@ export const OrgIncidentContactsTable = () => {
|
||||
|
||||
const onRemoveIncidentContact = async () => {
|
||||
try {
|
||||
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||
const incidentContactId = (popUp?.removeContact?.data as { id: string })?.id;
|
||||
|
||||
if (!currentOrg?.id) return;
|
||||
await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
email: incidentContactEmail
|
||||
incidentContactId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -80,7 +80,7 @@ export const OrgIncidentContactsTable = () => {
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
|
||||
{filteredContacts?.map(({ email }) => (
|
||||
{filteredContacts?.map(({ email, id }) => (
|
||||
<Tr key={email}>
|
||||
<Td className="w-full">{email}</Td>
|
||||
<Td className="mr-4">
|
||||
@@ -92,7 +92,7 @@ export const OrgIncidentContactsTable = () => {
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||
onClick={() => handlePopUpOpen("removeContact", { email, id })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
|
||||
Reference in New Issue
Block a user