feat(infisical-pg): test first milestone one flow

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:22 +05:30
parent aaba4a0895
commit 15b57de0ed
92 changed files with 3668 additions and 436 deletions
+2
View File
@@ -0,0 +1,2 @@
.eslintrc.js
./scripts
+4 -3
View File
@@ -13,12 +13,13 @@ module.exports = {
tsconfigRootDir: __dirname tsconfigRootDir: __dirname
}, },
rules: { rules: {
"import/prefer-default-export": "off", "consistent-return": "off", // my style
"simple-import-sort/exports": "error", 'import/order': 'off', // for simple-import-order
"import/prefer-default-export": "off", // why
"import/first": "error", "import/first": "error",
"import/newline-after-import": "error", "import/newline-after-import": "error",
"import/no-duplicates": "error", "import/no-duplicates": "error",
"consistent-return": "off", "simple-import-sort/exports": "error",
"simple-import-sort/imports": [ "simple-import-sort/imports": [
"warn", "warn",
{ {
@@ -4,4 +4,4 @@
"trailingComma": "none", "trailingComma": "none",
"tabWidth": 2, "tabWidth": 2,
"semi": true "semi": true
} }
+151 -64
View File
@@ -9,12 +9,14 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0", "@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1", "@fastify/cors": "^8.4.1",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/swagger": "^8.12.0", "@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^1.10.1",
"@ucast/mongo2js": "^1.3.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-airbnb-typescript": "^17.1.0",
@@ -27,6 +29,7 @@
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"ora": "^7.0.1", "ora": "^7.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"zod": "^3.22.4", "zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0" "zod-to-json-schema": "^3.22.0"
@@ -37,12 +40,13 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/node": "^20.9.5", "@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.54.0", "eslint": "^8.55.0",
"eslint-config-airbnb-base": "^15.0.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-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
@@ -63,6 +67,17 @@
"node": ">=0.10.0" "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": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -450,9 +465,9 @@
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "2.1.3", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dependencies": { "dependencies": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
"debug": "^4.3.2", "debug": "^4.3.2",
@@ -513,9 +528,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.54.0", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
@@ -1092,6 +1107,12 @@
"@types/node": "*" "@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": { "node_modules/@types/prompt-sync": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
@@ -1104,15 +1125,15 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==" "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
"integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.12.0", "@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/type-utils": "6.12.0", "@typescript-eslint/type-utils": "6.13.2",
"@typescript-eslint/utils": "6.12.0", "@typescript-eslint/utils": "6.13.2",
"@typescript-eslint/visitor-keys": "6.12.0", "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@@ -1159,14 +1180,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz",
"integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", "integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.12.0", "@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/types": "6.12.0", "@typescript-eslint/types": "6.13.2",
"@typescript-eslint/typescript-estree": "6.12.0", "@typescript-eslint/typescript-estree": "6.13.2",
"@typescript-eslint/visitor-keys": "6.12.0", "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1207,12 +1228,12 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
"integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.12.0", "@typescript-eslint/types": "6.13.2",
"@typescript-eslint/visitor-keys": "6.12.0" "@typescript-eslint/visitor-keys": "6.13.2"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@@ -1223,12 +1244,12 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz",
"integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "6.12.0", "@typescript-eslint/typescript-estree": "6.13.2",
"@typescript-eslint/utils": "6.12.0", "@typescript-eslint/utils": "6.13.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
}, },
@@ -1270,9 +1291,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
"integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
}, },
@@ -1282,12 +1303,12 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
"integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.12.0", "@typescript-eslint/types": "6.13.2",
"@typescript-eslint/visitor-keys": "6.12.0", "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1329,16 +1350,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.12.0", "@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/types": "6.12.0", "@typescript-eslint/types": "6.13.2",
"@typescript-eslint/typescript-estree": "6.12.0", "@typescript-eslint/typescript-estree": "6.13.2",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"engines": { "engines": {
@@ -1353,11 +1374,11 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
"integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.12.0", "@typescript-eslint/types": "6.13.2",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
"engines": { "engines": {
@@ -1368,6 +1389,37 @@
"url": "https://opencollective.com/typescript-eslint" "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": { "node_modules/@ungap/structured-clone": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1523,6 +1575,18 @@
"node": ">= 8" "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": { "node_modules/aproba": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -2453,14 +2517,14 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.54.0", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3", "@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.54.0", "@eslint/js": "8.55.0",
"@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@@ -2547,9 +2611,9 @@
} }
}, },
"node_modules/eslint-config-prettier": { "node_modules/eslint-config-prettier": {
"version": "9.0.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true, "dev": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
@@ -4576,6 +4640,17 @@
"node": ">=8.6" "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": { "node_modules/mime": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@@ -5192,11 +5267,11 @@
"dev": true "dev": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
"engines": { "engines": {
"node": ">=8.6" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
@@ -5471,6 +5546,18 @@
"node": ">=8.10.0" "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": { "node_modules/real-require": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+9 -5
View File
@@ -8,7 +8,7 @@
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine", "dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon", "dev:docker": "nodemon",
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix 'src/**/*.ts'", "lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts",
@@ -28,12 +28,13 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/node": "^20.9.5", "@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.54.0", "eslint": "^8.55.0",
"eslint-config-airbnb-base": "^15.0.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-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
@@ -46,12 +47,14 @@
"typescript": "^5.3.2" "typescript": "^5.3.2"
}, },
"dependencies": { "dependencies": {
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0", "@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1", "@fastify/cors": "^8.4.1",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/swagger": "^8.12.0", "@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^1.10.1",
"@ucast/mongo2js": "^1.3.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-airbnb-typescript": "^17.1.0",
@@ -64,6 +67,7 @@
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"ora": "^7.0.1", "ora": "^7.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"zod": "^3.22.4", "zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0" "zod-to-json-schema": "^3.22.0"
+16 -9
View File
@@ -16,11 +16,18 @@ const componentType = parseInt(prompt("Select a component: "), 10);
if (componentType === 1) { if (componentType === 1) {
const componentName = prompt("Enter service name: "); const componentName = prompt("Enter service name: ");
const dir = path.join(__dirname, `../src/services/${componentName}`); const dir = path.join(__dirname, `../src/services/${componentName}`);
const capitalizedComponentName = componentName.at(0)?.toUpperCase() + componentName.slice(1); const pascalCase = componentName
const dalTypeName = `T${capitalizedComponentName}DalFactory`; .split("-")
const dalName = `${componentName}DalFactory`; .map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
const serviceTypeName = `T${capitalizedComponentName}ServiceFactory`; .join("");
const serviceName = `${componentName}ServiceFactory`; 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); mkdirSync(dir);
@@ -29,9 +36,9 @@ if (componentType === 1) {
`import { TDbClient } from "@app/db"; `import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; 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 { }; return { };
}; };
@@ -43,12 +50,12 @@ export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
`import { ${dalTypeName} } from "./${componentName}-dal"; `import { ${dalTypeName} } from "./${componentName}-dal";
type ${serviceTypeName}Dep = { type ${serviceTypeName}Dep = {
${componentName}Dal: ${dalTypeName}; ${camelCase}Dal: ${dalTypeName};
}; };
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>; export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) => { export const ${serviceName} = ({ ${camelCase}Dal }: ${serviceTypeName}Dep) => {
return {}; return {};
}; };
` `
+4 -3
View File
@@ -3,6 +3,7 @@ import path from "path";
import knex from "knex"; import knex from "knex";
import { appendFileSync, readFileSync, writeFileSync } from "fs"; import { appendFileSync, readFileSync, writeFileSync } from "fs";
import promptSync from "prompt-sync"; import promptSync from "prompt-sync";
import { TableName } from "@app/db/schemas";
const prompt = promptSync(); const prompt = promptSync();
@@ -27,11 +28,11 @@ const getZodPrimitiveType = (type: string) => {
case "boolean": case "boolean":
return "z.boolean()"; return "z.boolean()";
case "jsonb": case "jsonb":
return "z.string()"; return "z.unknown()";
case "json": case "json":
return "z.string()"; return "z.unknown()";
case "timestamp with time zone": case "timestamp with time zone":
return "z.string().datetime()"; return "z.date()";
case "integer": case "integer":
return "z.number()"; return "z.number()";
case "text": case "text":
+18 -5
View File
@@ -1,10 +1,16 @@
import { TUser } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAuthDalFactory } from "@app/services/auth/auth-dal"; 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 { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { AuthMode } from "@app/services/auth/auth-signup-type"; 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 { 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"; import "fastify";
@@ -14,13 +20,14 @@ declare module "fastify" {
// used for mfa session authentication // used for mfa session authentication
mfa: { mfa: {
userId: string; userId: string;
user: TUser; user: TUsers;
}; };
// identity injection. depending on which kinda of token the information is filled in auth // identity injection. depending on which kinda of token the information is filled in auth
auth: { auth: {
authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY; authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY;
userId: string; userId: string;
user: TUser; tokenVersionId: string; // the session id of token used
user: TUsers;
}; };
} }
@@ -30,12 +37,18 @@ declare module "fastify" {
password: TAuthPasswordFactory; password: TAuthPasswordFactory;
signup: TAuthSignupFactory; signup: TAuthSignupFactory;
authToken: TAuthTokenServiceFactory; 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 // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
store: { store: {
user: Pick<TAuthDalFactory, "getUserById">; user: Pick<TUserDalFactory, "findById">;
}; };
} }
} }
+36 -4
View File
@@ -10,10 +10,24 @@ import {
TBackupPrivateKey, TBackupPrivateKey,
TBackupPrivateKeyInsert, TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate, TBackupPrivateKeyUpdate,
TOrganizationMemberships, TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate,
TOrganizations, TOrganizations,
TOrganizationsInsert, TOrganizationsInsert,
TOrganizationsUpdate, TOrganizationsUpdate,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TServerConfig,
TServerConfigInsert,
TServerConfigUpdate,
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate, TUserEncryptionKeysUpdate,
@@ -21,6 +35,7 @@ import {
TUsersInsert, TUsersInsert,
TUsersUpdate TUsersUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TApiKeys, TApiKeysInsert, TApiKeysUpdate } from "@app/db/schemas/api-keys";
declare module "knex/types/tables" { declare module "knex/types/tables" {
interface Tables extends { [key in TableName]: Knex.CompositeTableType<any> } { interface Tables extends { [key in TableName]: Knex.CompositeTableType<any> } {
@@ -51,9 +66,26 @@ declare module "knex/types/tables" {
TOrganizationsUpdate TOrganizationsUpdate
>; >;
[TableName.OrgMembership]: Knex.CompositeTableType< [TableName.OrgMembership]: Knex.CompositeTableType<
TOrganizationMemberships, TOrgMemberships,
TOrganizationsInsert, TOrgMembershipsInsert,
TOrganizationsUpdate 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.text("clientPublicKey"); t.text("clientPublicKey");
t.text("serverPrivateKey"); t.text("serverPrivateKey");
t.integer("encryptionVersion").defaultTo(1); t.integer("encryptionVersion").defaultTo(2);
t.text("protectedKey").notNullable(); t.text("protectedKey").notNullable();
t.text("protectedKeyIV").notNullable(); t.text("protectedKeyIV").notNullable();
t.text("protectedKeyTag").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 // does not need update trigger we will do it manually
t.timestamps(true, true, true); 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 // this is a one time function
await createOnUpdateTrigger(knex, TableName.Organization); await createOnUpdateTrigger(knex, TableName.Organization);
} }
export async function down(knex: Knex): Promise<void> { 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 knex.schema.dropTableIfExists(TableName.Organization);
await dropOnUpdateTrigger(knex, TableName.Organization); await dropOnUpdateTrigger(knex, TableName.Organization);
} }
@@ -5,8 +5,23 @@ import { OrgMembershipStatus } from "../schemas/models";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.OrgMembership); const isOrgRolePresent = await knex.schema.hasTable(TableName.OrgRoles);
if (!isTablePresent) { 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) => { await knex.schema.createTable(TableName.OrgMembership, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable(); t.string("role").notNullable();
@@ -14,10 +29,13 @@ export async function up(knex: Knex): Promise<void> {
t.string("inviteEmail"); t.string("inviteEmail");
// does not need update trigger we will do it manually // does not need update trigger we will do it manually
t.timestamps(true, true, true); t.timestamps(true, true, true);
t.uuid("userId").notNullable(); t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("orgId").notNullable(); t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); 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 // 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> { export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.OrgMembership); await knex.schema.dropTableIfExists(TableName.OrgMembership);
await knex.schema.dropTableIfExists(TableName.OrgRoles);
await dropOnUpdateTrigger(knex, TableName.OrgMembership); 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);
}
@@ -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);
}
@@ -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);
}
@@ -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
View 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(), userAgent: z.string().nullable().optional(),
refreshVersion: z.number().default(1), refreshVersion: z.number().default(1),
accessVersion: z.number().default(1), accessVersion: z.number().default(1),
lastUsed: z.string().datetime(), lastUsed: z.date(),
createdAt: z.string().datetime(), createdAt: z.date(),
updatedAt: z.string().datetime(), updatedAt: z.date(),
userId: z.string().uuid(), userId: z.string().uuid(),
}); });
+4 -3
View File
@@ -13,10 +13,11 @@ export const AuthTokensSchema = z.object({
phoneNumber: z.string().nullable().optional(), phoneNumber: z.string().nullable().optional(),
tokenHash: z.string(), tokenHash: z.string(),
triesLeft: z.number().nullable().optional(), triesLeft: z.number().nullable().optional(),
expiresAt: z.string().datetime(), expiresAt: z.date(),
createdAt: z.string().datetime(), createdAt: z.date(),
updatedAt: z.string().datetime(), updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
}); });
export type TAuthTokens = z.infer<typeof AuthTokensSchema>; export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
@@ -16,8 +16,8 @@ export const BackupPrivateKeySchema = z.object({
keyEncoding: z.string(), keyEncoding: z.string(),
salt: z.string(), salt: z.string(),
verifier: z.string(), verifier: z.string(),
createdAt: z.string().datetime(), createdAt: z.date(),
updatedAt: z.string().datetime(), updatedAt: z.date(),
userId: z.string().uuid(), userId: z.string().uuid(),
}); });
@@ -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>>;
+5 -1
View File
@@ -1,8 +1,12 @@
export * from "./auth-token-sessions"; export * from "./auth-token-sessions";
export * from "./auth-tokens"; export * from "./auth-tokens";
export * from "./backup-private-key"; export * from "./backup-private-key";
export * from "./incident-contacts";
export * from "./models"; export * from "./models";
export * from "./organization-memberships"; export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations"; export * from "./organizations";
export * from "./server-config";
export * from "./user-actions";
export * from "./user-encryption-keys"; export * from "./user-encryption-keys";
export * from "./users"; export * from "./users";
+6 -1
View File
@@ -7,7 +7,12 @@ export enum TableName {
AuthTokenSession = "auth_token_sessions", AuthTokenSession = "auth_token_sessions",
BackupPrivateKey = "backup_private_key", BackupPrivateKey = "backup_private_key",
Organization = "organizations", 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"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@@ -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
View 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>>;
+2 -2
View File
@@ -11,8 +11,8 @@ export const OrganizationsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
customerId: z.string().nullable().optional(), customerId: z.string().nullable().optional(),
createdAt: z.string().datetime(), createdAt: z.date(),
updatedAt: z.string().datetime(), updatedAt: z.date(),
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
@@ -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
View 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>>;
+3 -3
View File
@@ -17,9 +17,9 @@ export const UsersSchema = z.object({
isAccepted: z.boolean().default(false).nullable().optional(), isAccepted: z.boolean().default(false).nullable().optional(),
isMfaEnabled: z.boolean().default(false).nullable().optional(), isMfaEnabled: z.boolean().default(false).nullable().optional(),
mfaMethods: z.string().array().nullable().optional(), mfaMethods: z.string().array().nullable().optional(),
devices: z.string().nullable().optional(), devices: z.unknown().nullable().optional(),
createdAt: z.string().datetime(), createdAt: z.date(),
updatedAt: z.string().datetime(), updatedAt: z.date(),
}); });
export type TUsers = z.infer<typeof UsersSchema>; export type TUsers = z.infer<typeof UsersSchema>;
+36
View 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
View 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
View 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 };
}
});
};
@@ -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();
@@ -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
};
};
@@ -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
};
};
+29 -27
View File
@@ -8,33 +8,35 @@ const zodStrBool = z
.optional() .optional()
.transform((val) => val === "true"); .transform((val) => val === "true");
const envSchema = z.object({ const envSchema = z
PORT: z.coerce.number().default(4000), .object({
HOST: zpStr(z.string().default("localhost")), PORT: z.coerce.number().default(4000),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")), HOST: zpStr(z.string().default("localhost")),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"), DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
SALT_ROUNDS: z.coerce.number().default(10), NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
// TODO(akhilmhdh): will be changed to one SALT_ROUNDS: z.coerce.number().default(10),
ENCRYPTION_KEY: zpStr(z.string().optional()), // TODO(akhilmhdh): will be changed to one
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), ENCRYPTION_KEY: zpStr(z.string().optional()),
HTTPS_ENABLED: zodStrBool, ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
// smtp options HTTPS_ENABLED: zodStrBool,
SMTP_HOST: zpStr(z.string().optional()), // smtp options
SMTP_SECURE: zodStrBool, SMTP_HOST: zpStr(z.string().optional()),
SMTP_PORT: z.coerce.number().default(587), SMTP_SECURE: zodStrBool,
SMTP_USERNAME: zpStr(z.string().optional()), SMTP_PORT: z.coerce.number().default(587),
SMTP_PASSWORD: zpStr(z.string().optional()), SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_FROM_ADDRESS: zpStr(z.string().optional()), SMTP_PASSWORD: zpStr(z.string().optional()),
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")), SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"), SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
SITE_URL: zpStr(z.string().optional()), COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
// jwt options SITE_URL: zpStr(z.string().optional()),
JWT_AUTH_SECRET: zpStr(z.string()), // jwt options
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")), JWT_AUTH_SECRET: zpStr(z.string()),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")), JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")), JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")) 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>>; let envCfg: Readonly<z.infer<typeof envSchema>>;
+1 -1
View File
@@ -28,7 +28,7 @@ export class BadRequestError extends Error {
error: unknown; 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"); super(message ?? "The request is invalid");
this.name = name; this.name = name;
this.error = error; this.error = error;
+99 -1
View File
@@ -1,10 +1,108 @@
import { Knex } from "knex"; 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) => ({ 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) => { db.transaction(async (trx) => {
const res = await cb(trx); const res = await cb(trx);
return res; return res;
}), }),
...dal ...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 { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken"; import jwt, { JwtPayload } from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -27,7 +28,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
case AuthMode.SERVICE_ACCESS_TOKEN: case AuthMode.SERVICE_ACCESS_TOKEN:
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const; return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const;
default: 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) if (token.accessVersion !== session.accessVersion)
throw new UnauthorizedError({ name: "Stale session" }); 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" }); 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.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => { server.addHook("onRequest", async (req) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -56,8 +57,11 @@ export const injectIdentity = (server: FastifyZodProvider) => {
// TODO(akhilmhdh-pg): fill in rest of auth mode logic // TODO(akhilmhdh-pg): fill in rest of auth mode logic
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { case AuthMode.JWT: {
const user = await getJwtIdentity(server, token as AuthModeJwtTokenPayload); const { user, tokenVersionId } = await getJwtIdentity(
req.auth = { authMode: AuthMode.JWT, user, userId: user.id }; server,
token as AuthModeJwtTokenPayload
);
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId };
break; break;
} }
case AuthMode.SERVICE_TOKEN: case AuthMode.SERVICE_TOKEN:
@@ -72,4 +76,4 @@ export const injectIdentity = (server: FastifyZodProvider) => {
throw new UnauthorizedError({ name: "Unknown token strategy" }); throw new UnauthorizedError({ name: "Unknown token strategy" });
} }
}); });
}; });
@@ -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 = export const verifyAuth =
<T extends FastifyRequest>(authStrats: AuthMode[]) => <T extends FastifyRequest>(authStrats: AuthMode[]) =>
(req: T) => { async (req: T) => {
if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array"); if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array");
if (!req.auth) if (!req.auth)
throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" }); throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" });
+102 -7
View File
@@ -1,47 +1,142 @@
import { Knex } from "knex"; 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 { authDalFactory } from "@app/services/auth/auth-dal";
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service"; import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service"; import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-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 { TSmtpService } from "@app/services/smtp/smtp-service";
import { tokenDalFactory } from "@app/services/token/token-dal"; import { tokenDalFactory } from "@app/services/token/token-dal";
import { tokenServiceFactory } from "@app/services/token/token-service"; 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 { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
import { injectIdentity } from "../plugins/auth/inject-identity";
export const registerRoutes = async ( export const registerRoutes = async (
server: FastifyZodProvider, server: FastifyZodProvider,
{ db, smtp }: { db: Knex; smtp: TSmtpService } { db, smtp: smtpService }: { db: Knex; smtp: TSmtpService }
) => { ) => {
// db layers // db layers
const userDal = userDalFactory(db);
const authDal = authDalFactory(db); const authDal = authDalFactory(db);
const authTokenDal = tokenDalFactory(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 // service layers
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal }); const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
const loginService = authLoginServiceFactory({ authDal, smtpService: smtp, tokenService }); const userService = userServiceFactory({ userDal });
const passwordService = authPaswordServiceFactory({ tokenService, smtpService: smtp, authDal }); const loginService = authLoginServiceFactory({ userDal, smtpService, tokenService });
const signupService = authSignupServiceFactory({ tokenService, smtpService: smtp, authDal }); 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 // inject all services
server.decorate("services", { server.decorate("services", {
login: loginService, login: loginService,
password: passwordService, password: passwordService,
signup: signupService signup: signupService,
user: userService,
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
serverCfg: serverCfgService,
apiKey: apiKeyService,
authToken: tokenService
} as FastifyZodProvider["services"]); } as FastifyZodProvider["services"]);
server.decorate("store", { server.decorate("store", {
user: authDal user: userDal
} as FastifyZodProvider["store"]); } as FastifyZodProvider["store"]);
await server.register(injectIdentity); 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 // register routes for v1
await server.register(registerV1Routes, { prefix: "/v1" }); await server.register(registerV1Routes, { prefix: "/v1" });
await server.register(registerV2Routes, { prefix: "/v2" }); await server.register(registerV2Routes, { prefix: "/v2" });
await server.register(registerV3Routes, { prefix: "/v3" }); await server.register(registerV3Routes, { prefix: "/v3" });
await server.register(registerV1EERoutes, { prefix: "/ee/v1" });
}; };
+101
View 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
View 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 };
}
});
};
+12
View File
@@ -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 { registerPasswordRouter } from "./password-router";
import { registerUserActionRouter } from "./user-action-router";
import { registerUserRouter } from "./user-router";
export const registerV1Routes = async (server: FastifyZodProvider) => { export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerAuthRoutes, { prefix: "/auth" });
await server.register(registerPasswordRouter, { prefix: "/password" }); 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" });
}; };
@@ -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
};
}
});
};
@@ -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 };
}
});
};
@@ -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 };
}
});
};
@@ -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 };
}
});
};
+2
View File
@@ -1,5 +1,7 @@
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerUserRouter } from "./user-router";
export const registerV2Routes = async (server: FastifyZodProvider) => { export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" }); 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) if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
throw new Error("Unauthorized access"); 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"); if (!user) throw new Error("User not found");
req.mfa = { userId: user.id, user }; req.mfa = { userId: user.id, user };
}); });
@@ -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 };
}
});
};
@@ -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 };
}
});
};
+2
View File
@@ -1,7 +1,9 @@
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
import { registerSignupRouter } from "./signup-router"; import { registerSignupRouter } from "./signup-router";
import { registerUserRouter } from "./user-router";
export const registerV3Routes = async (server: FastifyZodProvider) => { export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerSignupRouter, { prefix: "/signup" }); await server.register(registerSignupRouter, { prefix: "/signup" });
await server.register(registerLoginRouter, { prefix: "/auth" }); 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 }; 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 };
}
});
}; };
@@ -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 };
}
});
};
@@ -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);
@@ -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
};
};
+3 -100
View File
@@ -1,110 +1,22 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; 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"; import { withTransaction } from "@app/lib/knex";
export type TAuthDalFactory = ReturnType<typeof authDalFactory>; export type TAuthDalFactory = ReturnType<typeof authDalFactory>;
export const authDalFactory = (db: TDbClient) => { 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) => const getBackupPrivateKeyByUserId = async (userId: string) =>
db(TableName.BackupPrivateKey).where({ userId }).first("*"); 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 ( const upsertBackupKey = async (
userId: string, userId: string,
data: Partial<TBackupPrivateKey>, data: Partial<TBackupPrivateKey>,
tx?: Knex tx?: Knex
): Promise<TBackupPrivateKey | undefined> => { ): Promise<TBackupPrivateKey | undefined> => {
const [backupKey] = await (tx ? tx(TableName.BackupPrivateKey) : db(TableName.BackupPrivateKey)) const [backupKey] = await (tx || db)(TableName.BackupPrivateKey)
.insert({ userId, ...data, updatedAt: new Date().toUTCString() } as TBackupPrivateKey) .insert({ userId, ...data, updatedAt: new Date() } as TBackupPrivateKey)
.onConflict("userId") .onConflict("userId")
.merge() .merge()
.returning("*"); .returning("*");
@@ -112,16 +24,7 @@ export const authDalFactory = (db: TDbClient) => {
}; };
return withTransaction(db, { return withTransaction(db, {
getUserByEmail,
getUserById,
getUserEncKeyByEmail,
getUserEncKeyByUserId,
getBackupPrivateKeyByUserId, getBackupPrivateKeyByUserId,
createUser,
updateUser,
updateUserById,
updateUserEncryptionByUserId,
upsertUserEncryptionKey,
upsertBackupKey upsertBackupKey
}); });
}; };
@@ -7,7 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service"; import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types"; import { TokenType } from "../token/token-types";
import { TAuthDalFactory } from "./auth-dal"; import { TUserDalFactory } from "../user/user-dal";
import { import {
TLoginClientProofDTO, TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO, TLoginGenServerPublicKeyDTO,
@@ -25,14 +25,14 @@ const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuth
}; };
type TAuthLoginServiceFactoryDep = { type TAuthLoginServiceFactoryDep = {
authDal: TAuthDalFactory; userDal: TUserDalFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
}; };
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>; export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
export const authLoginServiceFactory = ({ export const authLoginServiceFactory = ({
authDal, userDal,
tokenService, tokenService,
smtpService smtpService
}: TAuthLoginServiceFactoryDep) => { }: TAuthLoginServiceFactoryDep) => {
@@ -42,14 +42,14 @@ export const authLoginServiceFactory = ({
* If new device is found. Will be saved and a mail will be send * If new device is found. Will be saved and a mail will be send
*/ */
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => { 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( const isDeviceSeen = devices.some(
(device) => device.ip === ip && device.userAgent === userAgent (device) => device.ip === ip && device.userAgent === userAgent
); );
if (!isDeviceSeen) { if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]); 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({ await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin, template: SmtpTemplates.NewDeviceJoin,
subjectLine: "Successful login from new device", subjectLine: "Successful login from new device",
@@ -68,23 +68,23 @@ export const authLoginServiceFactory = ({
* Private * Private
* Send mfa code via email * Send mfa code via email
* */ * */
const sendUserMfaCode = async (user: TUsers) => { const sendUserMfaCode = async (userId: string, email: string) => {
const code = await tokenService.createTokenForUser({ const code = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA, type: TokenType.TOKEN_EMAIL_MFA,
userId: user.id userId
}); });
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.EmailMfa, template: SmtpTemplates.EmailMfa,
subjectLine: "Infisical MFA code", subjectLine: "Infisical MFA code",
recipients: [user.email], recipients: [email],
substitutions: { substitutions: {
code code
} }
}); });
}; };
/* Private /*
* Check user device and send mail if new device * 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 * 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, providerAuthToken,
clientPublicKey clientPublicKey
}: TLoginGenServerPublicKeyDTO) => { }: TLoginGenServerPublicKeyDTO) => {
const user = await authDal.getUserEncKeyByEmail(email); const userEnc = await userDal.findUserEncKeyByEmail(email);
if (!user || (user && !user.isAccepted)) { if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
const cfg = getConfig(); const cfg = getConfig();
if ( if (
!user.authMethods?.includes(AuthMethod.EMAIL) && !userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken) !isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
) { ) {
throw new Error("Invalid authorization request"); throw new Error("Invalid authorization request");
} }
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier); const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, { const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey, clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey serverPrivateKey: serverSrpKey.privateKey
}); });
@@ -161,46 +160,46 @@ export const authLoginServiceFactory = ({
ip, ip,
userAgent userAgent
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const user = await authDal.getUserEncKeyByEmail(email); const userEnc = await userDal.findUserEncKeyByEmail(email);
if (!user) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig(); const cfg = getConfig();
if ( if (
!user.authMethods?.includes(AuthMethod.EMAIL) && !userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken) !isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
) { ) {
throw new Error("Invalid authorization request"); throw new Error("Invalid authorization request");
} }
if (!user.serverPrivateKey || !user.clientPublicKey) if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
throw new Error("Failed to authenticate. Try again?"); throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof( const isValidClientProof = await srpCheckClientProof(
user.salt, userEnc.salt,
user.verifier, userEnc.verifier,
user.serverPrivateKey, userEnc.serverPrivateKey,
user.clientPublicKey, userEnc.clientPublicKey,
clientProof clientProof
); );
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await authDal.updateUserEncryptionByUserId(user.id, { await userDal.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null, serverPrivateKey: null,
clientPublicKey: null clientPublicKey: null
}); });
// send multi factor auth token if they it enabled // send multi factor auth token if they it enabled
if (user.isMfaEnabled) { if (userEnc.isMfaEnabled) {
const mfaToken = jwt.sign( const mfaToken = jwt.sign(
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: user.id }, { authTokenType: AuthTokenType.MFA_TOKEN, userId: userEnc.userId },
cfg.JWT_AUTH_SECRET, cfg.JWT_AUTH_SECRET,
{ expiresIn: cfg.JWT_MFA_LIFETIME } { expiresIn: cfg.JWT_MFA_LIFETIME }
); );
await sendUserMfaCode(user); await sendUserMfaCode(userEnc.userId, userEnc.email);
return { isMfaEnabled: true, token: mfaToken } as const; return { isMfaEnabled: true, token: mfaToken } as const;
} }
const token = await generateUserTokens(user, ip, userAgent); const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, isMfaEnabled: false, user } as const; return { token, isMfaEnabled: false, user: userEnc } as const;
}; };
/* /*
@@ -208,9 +207,9 @@ export const authLoginServiceFactory = ({
* saved in frontend * saved in frontend
*/ */
const resendMfaToken = async (userId: string) => { const resendMfaToken = async (userId: string) => {
const user = await authDal.getUserById(userId); const user = await userDal.findById(userId);
if (!user) return; if (!user) return;
await sendUserMfaCode(user); await sendUserMfaCode(user.id, user.email);
}; };
/* /*
@@ -223,11 +222,11 @@ export const authLoginServiceFactory = ({
userId, userId,
code: mfaToken code: mfaToken
}); });
const user = await authDal.getUserEncKeyByUserId(userId); const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!user) throw new Error("Failed to authenticate user"); if (!userEnc) throw new Error("Failed to authenticate user");
const token = await generateUserTokens(user, ip, userAgent); const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, user }; return { token, user: userEnc };
}; };
/* /*
@@ -243,6 +242,7 @@ export const authLoginServiceFactory = ({
loginExchangeClientProof, loginExchangeClientProof,
logout, logout,
resendMfaToken, resendMfaToken,
verifyMfaToken verifyMfaToken,
generateUserTokens
}; };
}; };
@@ -6,6 +6,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service"; import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types"; import { TokenType } from "../token/token-types";
import { TUserDalFactory } from "../user/user-dal";
import { TAuthDalFactory } from "./auth-dal"; import { TAuthDalFactory } from "./auth-dal";
import { import {
TChangePasswordDTO, TChangePasswordDTO,
@@ -16,6 +17,7 @@ import { AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = { type TAuthPasswordServiceFactoryDep = {
authDal: TAuthDalFactory; authDal: TAuthDalFactory;
userDal: TUserDalFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
}; };
@@ -23,6 +25,7 @@ type TAuthPasswordServiceFactoryDep = {
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>; export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
export const authPaswordServiceFactory = ({ export const authPaswordServiceFactory = ({
authDal, authDal,
userDal,
tokenService, tokenService,
smtpService smtpService
}: TAuthPasswordServiceFactoryDep) => { }: TAuthPasswordServiceFactoryDep) => {
@@ -31,11 +34,11 @@ export const authPaswordServiceFactory = ({
* Gets srp server user salt and server public key * Gets srp server user salt and server public key
*/ */
const generateServerPubKey = async (userId: string, clientPublicKey: string) => { const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
const user = await authDal.getUserEncKeyByUserId(userId); const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!user) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier); const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, { const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey, clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey serverPrivateKey: serverSrpKey.privateKey
}); });
@@ -59,10 +62,10 @@ export const authPaswordServiceFactory = ({
verifier, verifier,
tokenVersionId tokenVersionId
}: TChangePasswordDTO) => { }: TChangePasswordDTO) => {
const userEnc = await authDal.getUserEncKeyByUserId(userId); const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
await authDal.updateUserEncryptionByUserId(userEnc.userId, { await userDal.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null, serverPrivateKey: null,
clientPublicKey: null clientPublicKey: null
}); });
@@ -77,7 +80,7 @@ export const authPaswordServiceFactory = ({
); );
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await authDal.updateUserEncryptionByUserId(userId, { await userDal.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
@@ -100,7 +103,7 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email * Email password reset flow via email. Step 1 send email
*/ */
const sendPasswordResetEmail = async (email: string) => { 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 // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) return; if (!user || (user && !user.isAccepted)) return;
@@ -127,7 +130,7 @@ export const authPaswordServiceFactory = ({
* */ * */
const verifyPasswordResetEmail = async (email: string, code: string) => { const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig(); 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 // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset"); throw new Error("Failed email verification for pass reset");
@@ -166,7 +169,7 @@ export const authPaswordServiceFactory = ({
encryptedPrivateKeyTag encryptedPrivateKeyTag
}: TResetPasswordViaBackupKeyDTO }: TResetPasswordViaBackupKeyDTO
) => { ) => {
await authDal.updateUserEncryptionByUserId(userId, { await userDal.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
@@ -193,23 +196,23 @@ export const authPaswordServiceFactory = ({
tag, tag,
userId userId
}: TCreateBackupPrivateKeyDTO) => { }: TCreateBackupPrivateKeyDTO) => {
const user = await authDal.getUserEncKeyByUserId(userId); const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) { if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user"); 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"); throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof( const isValidClientProff = await srpCheckClientProof(
user.salt, userEnc.salt,
user.verifier, userEnc.verifier,
user.serverPrivateKey, userEnc.serverPrivateKey,
user.clientPublicKey, userEnc.clientPublicKey,
clientProof clientProof
); );
if (!isValidClientProff) throw new Error("failed to create backup key"); if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDal.transaction(async (tx) => { const backup = await authDal.transaction(async (tx) => {
const backupKey = await authDal.upsertBackupKey(user.id, { const backupKey = await authDal.upsertBackupKey(userEnc.userId, {
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
tag, tag,
@@ -217,8 +220,8 @@ export const authPaswordServiceFactory = ({
verifier verifier
}); });
await authDal.updateUserEncryptionByUserId( await userDal.updateUserEncryptionByUserId(
user.id, userEnc.userId,
{ {
serverPrivateKey: null, serverPrivateKey: null,
clientPublicKey: null clientPublicKey: null
@@ -235,7 +238,7 @@ export const authPaswordServiceFactory = ({
* Return user back up * Return user back up
* */ * */
const getBackupPrivateKeyOfUser = async (userId: string) => { const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await authDal.getUserEncKeyByUserId(userId); const user = await userDal.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
@@ -1,17 +1,25 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; 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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service"; import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types"; import { TokenType } from "../token/token-types";
import { TUserDalFactory } from "../user/user-dal";
import { TAuthDalFactory } from "./auth-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"; import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthSignupDep = { type TAuthSignupDep = {
authDal: TAuthDalFactory; authDal: TAuthDalFactory;
userDal: TUserDalFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgDal: TOrgDalFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
}; };
@@ -19,8 +27,11 @@ type TAuthSignupDep = {
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>; export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
export const authSignupServiceFactory = ({ export const authSignupServiceFactory = ({
authDal, authDal,
userDal,
tokenService, tokenService,
smtpService smtpService,
orgService,
orgDal
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => { const beginEmailSignupProcess = async (email: string) => {
@@ -29,13 +40,13 @@ export const authSignupServiceFactory = ({
throw new Error("Provided a disposable email"); throw new Error("Provided a disposable email");
} }
let user = await authDal.getUserByEmail(email); let user = await userDal.findUserByEmail(email);
if (user && user.isAccepted) { if (user && user.isAccepted) {
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues // 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"); throw new Error("Failed to send verification code for complete account");
} }
if (!user) { 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"); if (!user) throw new Error("Failed to create user");
@@ -55,7 +66,7 @@ export const authSignupServiceFactory = ({
}; };
const verifyEmailSignup = async (email: string, code: string) => { const verifyEmailSignup = async (email: string, code: string) => {
const user = await authDal.getUserByEmail(email); const user = await userDal.findUserByEmail(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues // 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"); throw new Error("Failed to send verification code for complete account");
@@ -91,7 +102,7 @@ export const authSignupServiceFactory = ({
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
// organizationName, organizationName,
// attributionSource, // attributionSource,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
@@ -99,19 +110,15 @@ export const authSignupServiceFactory = ({
ip, ip,
userAgent userAgent
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
const user = await authDal.getUserByEmail(email); const user = await userDal.findUserByEmail(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
const updateduser = await authDal.transaction(async (tx) => { const updateduser = await authDal.transaction(async (tx) => {
const us = await authDal.updateUserById( const us = await userDal.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
user.id,
{ firstName, lastName, isAccepted: true },
tx
);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const userEncKey = await authDal.upsertUserEncryptionKey( const userEncKey = await userDal.upsertUserEncryptionKey(
us.id, us.id,
{ {
salt, salt,
@@ -129,7 +136,116 @@ export const authSignupServiceFactory = ({
return { info: us, key: userEncKey }; 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({ const tokenSession = await tokenService.getUserTokenSession({
userAgent, userAgent,
ip, ip,
@@ -166,6 +282,7 @@ export const authSignupServiceFactory = ({
return { return {
beginEmailSignupProcess, beginEmailSignupProcess,
verifyEmailSignup, verifyEmailSignup,
completeEmailAccountSignup completeEmailAccountSignup,
completeAccountInvite
}; };
}; };
@@ -17,3 +17,20 @@ export type TCompleteAccountSignupDTO = {
ip: string; ip: string;
userAgent: 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; tokenVersionId: string;
accessVersion: number; accessVersion: number;
}; };
export type AuthModeRefreshJwtTokenPayload = {
authTokenType: AuthTokenType.REFRESH_TOKEN;
userId: string;
tokenVersionId: string;
refreshVersion: number;
};
@@ -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
View 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
});
};
@@ -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
});
};
@@ -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
View 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
View 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;
};
@@ -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, {});
@@ -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
};
};
@@ -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;
};
+41 -44
View File
@@ -1,55 +1,31 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { TableName, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { import { TDeleteTokenForUserDalDTO } from "./token-types";
TDeleteTokenForUserDalDTO,
TGetTokenForUserDalDTO,
TUpsertTokenForUserDalDTO
} from "./token-types";
export type TTokenDalConfig = {}; export type TTokenDalConfig = {};
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>; export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
// TODO(akhilmhdh-pg): wrap all with database error
export const tokenDalFactory = (db: TDbClient) => { export const tokenDalFactory = (db: TDbClient) => {
const upsertTokenForUser = async ({ const authOrm = ormify(db, TableName.AuthTokens);
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 getTokenForUser = async ({ const findOneTokenSession = async (
userId, filter: Partial<TAuthTokenSessions>
type
}: TGetTokenForUserDalDTO): Promise<TAuthTokens | undefined> =>
db(TableName.AuthTokens).where({ userId, type }).first();
const getTokenSession = async (
userId: string,
ip: string,
userAgent: string
): Promise<TAuthTokenSessions | undefined> => ): Promise<TAuthTokenSessions | undefined> =>
db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first(); db(TableName.AuthTokenSession).where(filter).first();
const getTokenSessionById = async (id: string, userId: string) =>
db(TableName.AuthTokenSession).where({ id, userId }).first();
const deleteTokenForUser = async ({ const deleteTokenForUser = async ({
userId, userId,
type type,
orgId
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> => }: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> =>
db(TableName.AuthTokens).where({ userId, type }).delete().returning("*"); db(TableName.AuthTokens).where({ userId, type, orgId }).delete().returning("*");
const decrementTriesField = async ({ const decrementTriesField = async ({
userId, userId,
@@ -58,6 +34,15 @@ export const tokenDalFactory = (db: TDbClient) => {
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1); 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 ( const insertTokenSession = async (
userId: string, userId: string,
ip: string, ip: string,
@@ -70,13 +55,13 @@ export const tokenDalFactory = (db: TDbClient) => {
userAgent, userAgent,
accessVersion: 1, accessVersion: 1,
refreshVersion: 1, refreshVersion: 1,
lastUsed: new Date().toUTCString() lastUsed: new Date()
}) })
.returning("*"); .returning("*");
return session; return session;
}; };
const incrementVersion = async ( const incrementTokenSessionVersion = async (
userId: string, userId: string,
sessionId: string sessionId: string
): Promise<TAuthTokenSessions | undefined> => { ): Promise<TAuthTokenSessions | undefined> => {
@@ -88,14 +73,26 @@ export const tokenDalFactory = (db: TDbClient) => {
return session; 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 { return {
getTokenForUser, ...authOrm,
getTokenSessionById, findTokenSessions,
upsertTokenForUser,
deleteTokenForUser, deleteTokenForUser,
decrementTriesField, decrementTriesField,
getTokenSession, findOneTokenSession,
insertTokenSession, insertTokenSession,
incrementVersion incrementTokenSessionVersion,
deleteTokenSession
}; };
}; };
+31 -17
View File
@@ -56,30 +56,37 @@ export const getTokenConfig = (tokenType: TokenType) => {
}; };
export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) => { export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId }: TCreateTokenForUserDTO) => { const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS); const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
await tokenDal.upsertTokenForUser({ await tokenDal.transaction(async (tx) => {
userId, await tokenDal.delete({ userId, type, orgId: orgId || null }, tx);
type, const newToken = await tokenDal.create({
expiresAt: tkCfg.expiresAt, tokenHash,
tokenHash, expiresAt: tkCfg.expiresAt.toUTCString(),
triesLeft: tkCfg?.triesLeft type,
userId,
orgId,
triesLeft: tkCfg?.triesLeft
});
return newToken;
}); });
return token; return token;
}; };
const validateTokenForUser = async ({ const validateTokenForUser = async ({
type, type,
userId, userId,
code code,
orgId
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => { }: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
const token = await tokenDal.getTokenForUser({ type, userId }); const token = await tokenDal.findOne({ type, userId, orgId: orgId || null });
// validate token // validate token
if (!token) throw new Error("Failed to find token"); if (!token) throw new Error("Failed to find token");
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) { 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"); throw new Error("Token expired. Please try again");
} }
@@ -87,15 +94,15 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
if (!isValidToken) { if (!isValidToken) {
if (token?.triesLeft) { if (token?.triesLeft) {
if (token.triesLeft === 1) { if (token.triesLeft === 1) {
await tokenDal.deleteTokenForUser({ type, userId }); await tokenDal.deleteTokenForUser({ type, userId, orgId: orgId || null });
} else { } else {
await tokenDal.decrementTriesField({ type, userId }); await tokenDal.decrementTriesField({ type, userId, orgId: orgId || null });
} }
} }
throw new Error("Invalid token"); 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]; return deletedToken?.[0];
}; };
@@ -104,7 +111,7 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
ip, ip,
userAgent userAgent
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => { }: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDal.getTokenSession(userId, ip, userAgent); let session = await tokenDal.findOneTokenSession({ userId, ip, userAgent });
if (!session) { if (!session) {
session = await tokenDal.insertTokenSession(userId, ip, userAgent); session = await tokenDal.insertTokenSession(userId, ip, userAgent);
} }
@@ -112,18 +119,25 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
}; };
const getUserTokenSessionById = async (id: string, userId: string) => const getUserTokenSessionById = async (id: string, userId: string) =>
tokenDal.getTokenSessionById(id, userId); tokenDal.findOneTokenSession({ id, userId });
const clearTokenSessionById = async ( const clearTokenSessionById = async (
userId: string, userId: string,
sessionId: 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 { return {
createTokenForUser, createTokenForUser,
validateTokenForUser, validateTokenForUser,
getUserTokenSession, getUserTokenSession,
clearTokenSessionById, clearTokenSessionById,
getUserTokenSessionById getUserTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions
}; };
}; };
@@ -8,12 +8,19 @@ export enum TokenType {
export type TCreateTokenForUserDTO = { export type TCreateTokenForUserDTO = {
type: TokenType; type: TokenType;
userId: string; userId: string;
orgId?: string;
};
export type TCreateOrgInviteTokenDTO = {
userId: string;
orgId: string;
}; };
export type TValidateTokenForUserDTO = { export type TValidateTokenForUserDTO = {
type: TokenType; type: TokenType;
code: string; code: string;
userId: string; userId: string;
orgId?: string;
}; };
export type TUpsertTokenForUserDalDTO = { export type TUpsertTokenForUserDalDTO = {
@@ -32,6 +39,7 @@ export type TGetTokenForUserDalDTO = {
export type TDeleteTokenForUserDalDTO = { export type TDeleteTokenForUserDalDTO = {
userId: string; userId: string;
type: TokenType; type: TokenType;
orgId: string | null;
}; };
export type TIssueAuthTokenDTO = { export type TIssueAuthTokenDTO = {
+108
View 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
};
};
@@ -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
};
};
+1 -1
View File
@@ -22,6 +22,6 @@
"@server/*": ["./src/server/*"] "@server/*": ["./src/server/*"]
} }
}, },
"include": ["src/**/*"], "include": ["src/**/*","scripts/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }
+2
View File
@@ -5,6 +5,8 @@ import { requireAuth, validateRequest } from "../../middleware";
import { membershipOrgController } from "../../controllers/v1"; import { membershipOrgController } from "../../controllers/v1";
import { AuthMode } from "../../variables"; import { AuthMode } from "../../variables";
// depreciated completely
// ignored for new codebase
router.post( router.post(
// TODO endpoint: check dashboard // TODO endpoint: check dashboard
"/membershipOrg/:membershipOrgId/change-role", "/membershipOrg/:membershipOrgId/change-role",
+1
View File
@@ -25,6 +25,7 @@ services:
POSTGRES_DB: infisical POSTGRES_DB: infisical
backend: backend:
container_name: infisical-dev-api
build: build:
context: ./backend-pg context: ./backend-pg
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
+1 -1
View File
@@ -72,7 +72,7 @@ export const completeAccountSignup = async (details: CompleteAccountSignupDTO) =
}; };
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { 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; return data;
}; };
@@ -42,10 +42,10 @@ export const useDeleteIncidentContact = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteIncidentContactDTO>({ return useMutation<{}, {}, DeleteIncidentContactDTO>({
mutationFn: async ({ orgId, email }) => { mutationFn: async ({ orgId, incidentContactId }) => {
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, { const { data } = await apiRequest.delete(
data: { email } `/api/v1/organization/${orgId}/incidentContactOrg/${incidentContactId}`
}); );
return data; return data;
}, },
onSuccess: (_, { orgId }) => { onSuccess: (_, { orgId }) => {
@@ -9,7 +9,7 @@ export type IncidentContact = {
export type DeleteIncidentContactDTO = { export type DeleteIncidentContactDTO = {
orgId: string; orgId: string;
email: string; incidentContactId: string;
}; };
export type AddIncidentContactDTO = { export type AddIncidentContactDTO = {
+4 -6
View File
@@ -67,13 +67,11 @@ const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return { permissions: [], membership: null }; if (orgId === "") return { permissions: [], membership: null };
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]; membership: OrgUser;
membership: OrgUser; }>(`/api/ee/v1/organization/${orgId}/permissions`);
};
}>(`/api/v1/roles/organization/${orgId}/permissions`);
return data.data; return data;
}; };
export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) => export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =>
+5 -4
View File
@@ -82,13 +82,14 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) || publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
!Component.requireAuth !Component.requireAuth
) { ) {
// TODO(akhilmhdh): bring back server config later
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NotificationProvider> <NotificationProvider>
<AuthProvider> <ServerConfigProvider>
<Component {...pageProps} /> <AuthProvider>
</AuthProvider> <Component {...pageProps} />
</AuthProvider>
</ServerConfigProvider>
</NotificationProvider> </NotificationProvider>
</QueryClientProvider> </QueryClientProvider>
); );
@@ -95,6 +95,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
} }
} }
} catch (err) { } catch (err) {
console.error(err);
setLoginError(true); setLoginError(true);
createNotification({ createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.", text: "Login unsuccessful. Double-check your credentials and try again.",
@@ -236,7 +237,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : ( ) : (
<div /> <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"> <Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"> <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 Forgot password? Recover your account
@@ -35,12 +35,12 @@ export const OrgIncidentContactsTable = () => {
const onRemoveIncidentContact = async () => { const onRemoveIncidentContact = async () => {
try { try {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email; const incidentContactId = (popUp?.removeContact?.data as { id: string })?.id;
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
await mutateAsync({ await mutateAsync({
orgId: currentOrg.id, orgId: currentOrg.id,
email: incidentContactEmail incidentContactId
}); });
createNotification({ createNotification({
@@ -80,7 +80,7 @@ export const OrgIncidentContactsTable = () => {
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />} {isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
{filteredContacts?.map(({ email }) => ( {filteredContacts?.map(({ email, id }) => (
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
<Td className="mr-4"> <Td className="mr-4">
@@ -92,7 +92,7 @@ export const OrgIncidentContactsTable = () => {
<IconButton <IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })} onClick={() => handlePopUpOpen("removeContact", { email, id })}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />