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

This commit is contained in:
Akhil Mohan
2023-12-09 19:18:01 +05:30
parent aaba4a0895
commit 15b57de0ed
92 changed files with 3668 additions and 436 deletions

2
backend-pg/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
.eslintrc.js
./scripts

View File

@@ -13,12 +13,13 @@ module.exports = {
tsconfigRootDir: __dirname
},
rules: {
"import/prefer-default-export": "off",
"simple-import-sort/exports": "error",
"consistent-return": "off", // my style
'import/order': 'off', // for simple-import-order
"import/prefer-default-export": "off", // why
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"consistent-return": "off",
"simple-import-sort/exports": "error",
"simple-import-sort/imports": [
"warn",
{

View File

@@ -4,4 +4,4 @@
"trailingComma": "none",
"tabWidth": 2,
"semi": true
}
}

View File

@@ -9,12 +9,14 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1",
"@fastify/helmet": "^11.1.1",
"@fastify/rate-limit": "^9.0.0",
"@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1",
"@ucast/mongo2js": "^1.3.4",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1",
"eslint-config-airbnb-typescript": "^17.1.0",
@@ -27,6 +29,7 @@
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"pg": "^8.11.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0"
@@ -37,12 +40,13 @@
"@types/jsrp": "^0.2.6",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint": "^8.54.0",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.55.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
@@ -63,6 +67,17 @@
"node": ">=0.10.0"
}
},
"node_modules/@casl/ability": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
"dependencies": {
"@ucast/mongo2js": "^1.3.0"
},
"funding": {
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -450,9 +465,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
"integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -513,9 +528,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@eslint/js": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -1092,6 +1107,12 @@
"@types/node": "*"
}
},
"node_modules/@types/picomatch": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz",
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
"dev": true
},
"node_modules/@types/prompt-sync": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
@@ -1104,15 +1125,15 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
"integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
"integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==",
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/type-utils": "6.12.0",
"@typescript-eslint/utils": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/type-utils": "6.13.2",
"@typescript-eslint/utils": "6.13.2",
"@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -1159,14 +1180,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@typescript-eslint/parser": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
"integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz",
"integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==",
"dependencies": {
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/types": "6.13.2",
"@typescript-eslint/typescript-estree": "6.13.2",
"@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4"
},
"engines": {
@@ -1207,12 +1228,12 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
"integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
"integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
"dependencies": {
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0"
"@typescript-eslint/types": "6.13.2",
"@typescript-eslint/visitor-keys": "6.13.2"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1223,12 +1244,12 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
"integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz",
"integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==",
"dependencies": {
"@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/utils": "6.12.0",
"@typescript-eslint/typescript-estree": "6.13.2",
"@typescript-eslint/utils": "6.13.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -1270,9 +1291,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@typescript-eslint/types": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
"integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
"integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
@@ -1282,12 +1303,12 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
"integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
"integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
"dependencies": {
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"@typescript-eslint/types": "6.13.2",
"@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1329,16 +1350,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@typescript-eslint/utils": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
"integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/scope-manager": "6.13.2",
"@typescript-eslint/types": "6.13.2",
"@typescript-eslint/typescript-estree": "6.13.2",
"semver": "^7.5.4"
},
"engines": {
@@ -1353,11 +1374,11 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
"integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
"integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
"dependencies": {
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/types": "6.13.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -1368,6 +1389,37 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ucast/core": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
},
"node_modules/@ucast/js": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
"dependencies": {
"@ucast/core": "^1.0.0"
}
},
"node_modules/@ucast/mongo": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
"dependencies": {
"@ucast/core": "^1.4.1"
}
},
"node_modules/@ucast/mongo2js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
"dependencies": {
"@ucast/core": "^1.6.1",
"@ucast/js": "^3.0.0",
"@ucast/mongo": "^2.4.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1523,6 +1575,18 @@
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -2453,14 +2517,14 @@
}
},
"node_modules/eslint": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.54.0",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.55.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -2547,9 +2611,9 @@
}
},
"node_modules/eslint-config-prettier": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@@ -4576,6 +4640,17 @@
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@@ -5192,11 +5267,11 @@
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
"integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
"engines": {
"node": ">=8.6"
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -5471,6 +5546,18 @@
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",

View File

@@ -8,7 +8,7 @@
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix 'src/**/*.ts'",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
@@ -28,12 +28,13 @@
"@types/jsrp": "^0.2.6",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint": "^8.54.0",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.55.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
@@ -46,12 +47,14 @@
"typescript": "^5.3.2"
},
"dependencies": {
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1",
"@fastify/helmet": "^11.1.1",
"@fastify/rate-limit": "^9.0.0",
"@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1",
"@ucast/mongo2js": "^1.3.4",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1",
"eslint-config-airbnb-typescript": "^17.1.0",
@@ -64,6 +67,7 @@
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"pg": "^8.11.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0"

View File

@@ -16,11 +16,18 @@ const componentType = parseInt(prompt("Select a component: "), 10);
if (componentType === 1) {
const componentName = prompt("Enter service name: ");
const dir = path.join(__dirname, `../src/services/${componentName}`);
const capitalizedComponentName = componentName.at(0)?.toUpperCase() + componentName.slice(1);
const dalTypeName = `T${capitalizedComponentName}DalFactory`;
const dalName = `${componentName}DalFactory`;
const serviceTypeName = `T${capitalizedComponentName}ServiceFactory`;
const serviceName = `${componentName}ServiceFactory`;
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DalFactory`;
const dalName = `${camelCase}DalFactory`;
const serviceTypeName = `T${pascalCase}ServiceFactory`;
const serviceName = `${camelCase}ServiceFactory`;
mkdirSync(dir);
@@ -29,9 +36,9 @@ if (componentType === 1) {
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = {};
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
export const ${dalName} = (db: TDbClient) => {
return { };
};
@@ -43,12 +50,12 @@ export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
`import { ${dalTypeName} } from "./${componentName}-dal";
type ${serviceTypeName}Dep = {
${componentName}Dal: ${dalTypeName};
${camelCase}Dal: ${dalTypeName};
};
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) => {
export const ${serviceName} = ({ ${camelCase}Dal }: ${serviceTypeName}Dep) => {
return {};
};
`

View File

@@ -3,6 +3,7 @@ import path from "path";
import knex from "knex";
import { appendFileSync, readFileSync, writeFileSync } from "fs";
import promptSync from "prompt-sync";
import { TableName } from "@app/db/schemas";
const prompt = promptSync();
@@ -27,11 +28,11 @@ const getZodPrimitiveType = (type: string) => {
case "boolean":
return "z.boolean()";
case "jsonb":
return "z.string()";
return "z.unknown()";
case "json":
return "z.string()";
return "z.unknown()";
case "timestamp with time zone":
return "z.string().datetime()";
return "z.date()";
case "integer":
return "z.number()";
case "text":

View File

@@ -1,10 +1,16 @@
import { TUser } from "@app/db/schemas";
import { TAuthDalFactory } from "@app/services/auth/auth-dal";
import { TUsers } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { AuthMode } from "@app/services/auth/auth-signup-type";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TServerCfgServiceFactory } from "@app/services/server-cfg/server-cfg-service";
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
import { TUserDalFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import "fastify";
@@ -14,13 +20,14 @@ declare module "fastify" {
// used for mfa session authentication
mfa: {
userId: string;
user: TUser;
user: TUsers;
};
// identity injection. depending on which kinda of token the information is filled in auth
auth: {
authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY;
userId: string;
user: TUser;
tokenVersionId: string; // the session id of token used
user: TUsers;
};
}
@@ -30,12 +37,18 @@ declare module "fastify" {
password: TAuthPasswordFactory;
signup: TAuthSignupFactory;
authToken: TAuthTokenServiceFactory;
permission: TPermissionServiceFactory;
org: TOrgServiceFactory;
orgRole: TOrgRoleServiceFactory;
serverCfg: TServerCfgServiceFactory;
user: TUserServiceFactory;
apiKey: TApiKeyServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
store: {
user: Pick<TAuthDalFactory, "getUserById">;
user: Pick<TUserDalFactory, "findById">;
};
}
}

View File

@@ -10,10 +10,24 @@ import {
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
TOrganizationMemberships,
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TServerConfig,
TServerConfigInsert,
TServerConfigUpdate,
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
@@ -21,6 +35,7 @@ import {
TUsersInsert,
TUsersUpdate
} from "@app/db/schemas";
import { TApiKeys, TApiKeysInsert, TApiKeysUpdate } from "@app/db/schemas/api-keys";
declare module "knex/types/tables" {
interface Tables extends { [key in TableName]: Knex.CompositeTableType<any> } {
@@ -51,9 +66,26 @@ declare module "knex/types/tables" {
TOrganizationsUpdate
>;
[TableName.OrgMembership]: Knex.CompositeTableType<
TOrganizationMemberships,
TOrganizationsInsert,
TOrganizationsUpdate
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate
>;
[TableName.OrgRoles]: Knex.CompositeTableType<TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate>;
[TableName.IncidentContact]: Knex.CompositeTableType<
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate
>;
[TableName.UserAction]: Knex.CompositeTableType<
TUserActions,
TUserActionsInsert,
TUserActionsUpdate
>;
[TableName.ServerConfig]: Knex.CompositeTableType<
TServerConfig,
TServerConfigInsert,
TServerConfigUpdate
>;
[TableName.ApiKey]: Knex.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
}
}

View File

@@ -9,7 +9,7 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.text("clientPublicKey");
t.text("serverPrivateKey");
t.integer("encryptionVersion").defaultTo(1);
t.integer("encryptionVersion").defaultTo(2);
t.text("protectedKey").notNullable();
t.text("protectedKeyIV").notNullable();
t.text("protectedKeyTag").notNullable();

View File

@@ -13,12 +13,21 @@ export async function up(knex: Knex): Promise<void> {
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
});
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.uuid("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.Organization);
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AuthTokens, "orgId")) {
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.dropColumn("orgId");
});
}
await knex.schema.dropTableIfExists(TableName.Organization);
await dropOnUpdateTrigger(knex, TableName.Organization);
}

View File

@@ -5,8 +5,23 @@ import { OrgMembershipStatus } from "../schemas/models";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
if (!isTablePresent) {
const isOrgRolePresent = await knex.schema.hasTable(TableName.OrgRoles);
if (!isOrgRolePresent) {
await knex.schema.createTable(TableName.OrgRoles, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.string("description");
t.string("slug").notNullable();
t.json("permissions").notNullable();
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
const isOrgTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
if (!isOrgTablePresent) {
await knex.schema.createTable(TableName.OrgMembership, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable();
@@ -14,10 +29,13 @@ export async function up(knex: Knex): Promise<void> {
t.string("inviteEmail");
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("userId").notNullable();
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
// until role is changed/removed the role should not deleted
t.uuid("roleId");
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
});
}
// this is a one time function
@@ -26,5 +44,6 @@ export async function up(knex: Knex): Promise<void> {
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.OrgMembership);
await knex.schema.dropTableIfExists(TableName.OrgRoles);
await dropOnUpdateTrigger(knex, TableName.OrgMembership);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.UserAction);
if (!isTablePresent) {
await knex.schema.createTable(TableName.UserAction, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("action").notNullable();
t.timestamps(true, true, true);
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.UserAction);
}

View File

@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.ServerConfig);
if (!isTablePresent) {
await knex.schema.createTable(TableName.ServerConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.boolean("initialized").defaultTo(false);
t.boolean("allowSignUp").defaultTo(true);
t.timestamps(true, true, true);
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.ServerConfig);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ServerConfig);
await dropOnUpdateTrigger(knex, TableName.ServerConfig);
}

View File

@@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.ApiKey);
if (!isTablePresent) {
await knex.schema.createTable(TableName.ApiKey, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.datetime("lastUsed");
t.datetime("expiresAt");
t.string("secretHash").notNullable();
t.timestamps(true, true, true);
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
await createOnUpdateTrigger(knex, TableName.ApiKey);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ApiKey);
await dropOnUpdateTrigger(knex, TableName.ApiKey);
}

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>>;

View File

@@ -13,9 +13,9 @@ export const AuthTokenSessionsSchema = z.object({
userAgent: z.string().nullable().optional(),
refreshVersion: z.number().default(1),
accessVersion: z.number().default(1),
lastUsed: z.string().datetime(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
lastUsed: z.date(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.string().uuid(),
});

View File

@@ -13,10 +13,11 @@ export const AuthTokensSchema = z.object({
phoneNumber: z.string().nullable().optional(),
tokenHash: z.string(),
triesLeft: z.number().nullable().optional(),
expiresAt: z.string().datetime(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
expiresAt: z.date(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
});
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;

View File

@@ -16,8 +16,8 @@ export const BackupPrivateKeySchema = z.object({
keyEncoding: z.string(),
salt: z.string(),
verifier: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.string().uuid(),
});

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 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>>;

View File

@@ -1,8 +1,12 @@
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./backup-private-key";
export * from "./incident-contacts";
export * from "./models";
export * from "./organization-memberships";
export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
export * from "./server-config";
export * from "./user-actions";
export * from "./user-encryption-keys";
export * from "./users";

View File

@@ -7,7 +7,12 @@ export enum TableName {
AuthTokenSession = "auth_token_sessions",
BackupPrivateKey = "backup_private_key",
Organization = "organizations",
OrgMembership = "organization_memberships"
OrgMembership = "org_memberships",
OrgRoles = "org_roles",
IncidentContact = "incident_contacts",
UserAction = "user_actions",
ServerConfig = "server_config",
ApiKey = "api_keys"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const OrgMembershipsSchema = z.object({
id: z.string().uuid(),
role: z.string(),
status: z.string().default('invited'),
inviteEmail: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional(),
});
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
export type TOrgMembershipsInsert = Omit<TOrgMemberships, TImmutableDBKeys>;
export type TOrgMembershipsUpdate = Partial<Omit<TOrgMemberships, TImmutableDBKeys>>;

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>>;

View File

@@ -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>>;

View File

@@ -11,8 +11,8 @@ export const OrganizationsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
customerId: z.string().nullable().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

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 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>>;

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>>;

View File

@@ -17,9 +17,9 @@ export const UsersSchema = z.object({
isAccepted: z.boolean().default(false).nullable().optional(),
isMfaEnabled: z.boolean().default(false).nullable().optional(),
mfaMethods: z.string().array().nullable().optional(),
devices: z.string().nullable().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
devices: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TUsers = z.infer<typeof UsersSchema>;

36
backend-pg/src/ee/LICENSE Normal file
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.

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" });
};

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 };
}
});
};

View File

@@ -0,0 +1,124 @@
import picomatch from "picomatch";
import {
AbilityBuilder,
buildMongoQueryMatcher,
createMongoAbility,
MongoAbility
} from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
Member = "member",
Settings = "settings",
IncidentAccount = "incident-contact",
Sso = "sso",
Billing = "billing",
SecretScanning = "secret-scanning"
}
export type OrgPermissionSet =
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
// role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
return build({ conditionsMatcher });
};
export const orgAdminPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
return build({ conditionsMatcher });
};
export const orgMemberPermissions = buildMemberPermission();

View File

@@ -0,0 +1,23 @@
import { TDbClient } from "@app/db";
import { TableName, TOrgMemberships } from "@app/db/schemas";
export type TPermissionDalFactory = ReturnType<typeof permissionDalFactory>;
export const permissionDalFactory = (db: TDbClient) => {
const getOrgPermission = async (
userId: string,
orgId: string
): Promise<(TOrgMemberships & { permissions: string }) | undefined> => {
const membership = await db(TableName.OrgMembership)
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.select(`${TableName.OrgMembership}.*`, `${TableName.OrgRoles}.permissions`)
.where({ userId, [`${TableName.OrgMembership}.orgId`]: orgId })
.first();
return membership;
};
return {
getOrgPermission
};
};

View File

@@ -0,0 +1,50 @@
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import {
conditionsMatcher,
orgAdminPermissions,
orgMemberPermissions,
OrgPermissionSet
} from "./org-permission";
import { TPermissionDalFactory } from "./permission-dal";
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { unpackRules } from "@casl/ability/extra";
type TPermissionServiceFactoryDep = {
permissionDal: TPermissionDalFactory;
};
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFactoryDep) => {
/*
* Get user permission in an organization
* */
const getUserOrgPermission = async (userId: string, orgId: string) => {
const membership = await permissionDal.getOrgPermission(userId, orgId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
if (membership.role === "custom" && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" });
}
if (membership.role === "admin") return { permission: orgAdminPermissions, membership };
if (membership.role === "member") return { permission: orgMemberPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
{
conditionsMatcher
}
);
return { permission, membership };
}
throw new BadRequestError({ name: "Role missing", message: "User role not found" });
};
return {
getUserOrgPermission
};
};

View File

@@ -8,33 +8,35 @@ const zodStrBool = z
.optional()
.transform((val) => val === "true");
const envSchema = z.object({
PORT: z.coerce.number().default(4000),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
SALT_ROUNDS: z.coerce.number().default(10),
// TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_SECURE: zodStrBool,
SMTP_PORT: z.coerce.number().default(587),
SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_PASSWORD: zpStr(z.string().optional()),
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
SITE_URL: zpStr(z.string().optional()),
// jwt options
JWT_AUTH_SECRET: zpStr(z.string()),
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
});
const envSchema = z
.object({
PORT: z.coerce.number().default(4000),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
SALT_ROUNDS: z.coerce.number().default(10),
// TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_SECURE: zodStrBool,
SMTP_PORT: z.coerce.number().default(587),
SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_PASSWORD: zpStr(z.string().optional()),
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
SITE_URL: zpStr(z.string().optional()),
// jwt options
JWT_AUTH_SECRET: zpStr(z.string()),
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
})
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
let envCfg: Readonly<z.infer<typeof envSchema>>;

View File

@@ -28,7 +28,7 @@ export class BadRequestError extends Error {
error: unknown;
constructor({ name, error, message }: { message?: string; name: string; error: unknown }) {
constructor({ name, error, message }: { message?: string; name: string; error?: unknown }) {
super(message ?? "The request is invalid");
this.name = name;
this.error = error;

View File

@@ -1,10 +1,108 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "../errors";
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
transaction: async <T>(cb: (tx: Knex) => T) =>
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
db.transaction(async (trx) => {
const res = await cb(trx);
return res;
}),
...dal
});
// What is ormify
// It is to inject typical operations like find, findOne, update, delete, create
// This will avoid writing most common ones each time
export const ormify = <DbOps extends object, Tname extends TableName>(
db: Knex,
tableName: Tname,
dal?: DbOps
) => ({
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
db.transaction(async (trx) => {
const res = await cb(trx);
return res;
}),
findById: (id: string, tx?: Knex) => {
try {
return (tx || db)(tableName)
.where({ id } as any)
.first("*");
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
},
findOne: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const res = await (tx || db)(tableName).where(filter).first("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
},
find: (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
try {
return (tx || db)(tableName).where(filter);
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
},
create: async (data: Tables[Tname]["insert"], tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName).insert(data).returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName)
.where({ id } as any)
.update(data as any)
.returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Update by id" });
}
},
update: async (
filter: Partial<Tables[Tname]["base"]>,
data: Tables[Tname]["update"],
tx?: Knex
) => {
try {
const user = await (tx || db)(tableName)
.where(filter)
.update(data as any)
.returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Update" });
}
},
deleteById: async (id: string, tx?: Knex) => {
try {
const [user] = await (tx || db)(tableName)
.where({ id } as any)
.delete()
.returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Delete by id" });
}
},
delete: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const user = await (tx || db)(tableName).where(filter).delete().returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Delete" });
}
},
...(dal || {})
});

View File

@@ -1,4 +1,5 @@
import { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env";
@@ -27,7 +28,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
case AuthMode.SERVICE_ACCESS_TOKEN:
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const;
default:
throw new UnauthorizedError({ name: "Invalid token type" });
return { authMode: null, token: null } as const;
}
};
@@ -41,13 +42,13 @@ const getJwtIdentity = async (server: FastifyZodProvider, token: AuthModeJwtToke
if (token.accessVersion !== session.accessVersion)
throw new UnauthorizedError({ name: "Stale session" });
const user = await server.store.user.getUserById(session.userId);
const user = await server.store.user.findById(session.userId);
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
return user;
return { user, tokenVersionId: token.tokenVersionId };
};
export const injectIdentity = (server: FastifyZodProvider) => {
export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
@@ -56,8 +57,11 @@ export const injectIdentity = (server: FastifyZodProvider) => {
// TODO(akhilmhdh-pg): fill in rest of auth mode logic
switch (authMode) {
case AuthMode.JWT: {
const user = await getJwtIdentity(server, token as AuthModeJwtTokenPayload);
req.auth = { authMode: AuthMode.JWT, user, userId: user.id };
const { user, tokenVersionId } = await getJwtIdentity(
server,
token as AuthModeJwtTokenPayload
);
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId };
break;
}
case AuthMode.SERVICE_TOKEN:
@@ -72,4 +76,4 @@ export const injectIdentity = (server: FastifyZodProvider) => {
throw new UnauthorizedError({ name: "Unknown token strategy" });
}
});
};
});

View File

@@ -0,0 +1,11 @@
import { FastifyRequest } from "fastify";
import { UnauthorizedError } from "@app/lib/errors";
export const verifySuperAdmin = async <T extends FastifyRequest>(req: T) => {
if (!req.auth.user.superAdmin)
throw new UnauthorizedError({
name: "Unauthorized access",
message: "Requires superadmin access"
});
};

View File

@@ -5,7 +5,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
export const verifyAuth =
<T extends FastifyRequest>(authStrats: AuthMode[]) =>
(req: T) => {
async (req: T) => {
if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array");
if (!req.auth)
throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" });

View File

@@ -1,47 +1,142 @@
import { Knex } from "knex";
import { z } from "zod";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { permissionDalFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { authDalFactory } from "@app/services/auth/auth-dal";
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal";
import { orgDalFactory } from "@app/services/org/org-dal";
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { serverCfgDalFactory } from "@app/services/server-cfg/server-cfg-dal";
import { serverCfgServiceFactory } from "@app/services/server-cfg/server-cfg-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { tokenDalFactory } from "@app/services/token/token-dal";
import { tokenServiceFactory } from "@app/services/token/token-service";
import { userDalFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service";
import { injectIdentity } from "../plugins/auth/inject-identity";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
import { injectIdentity } from "../plugins/auth/inject-identity";
export const registerRoutes = async (
server: FastifyZodProvider,
{ db, smtp }: { db: Knex; smtp: TSmtpService }
{ db, smtp: smtpService }: { db: Knex; smtp: TSmtpService }
) => {
// db layers
const userDal = userDalFactory(db);
const authDal = authDalFactory(db);
const authTokenDal = tokenDalFactory(db);
const orgDal = orgDalFactory(db);
const incidentContactDal = incidentContactDalFactory(db);
const orgRoleDal = orgRoleDalFactory(db);
const serverCfgDal = serverCfgDalFactory(db);
const apiKeyDal = apiKeyDalFactory(db);
// ee db layer ops
const permissionDal = permissionDalFactory(db);
// ee services
const permissionService = permissionServiceFactory({ permissionDal });
// service layers
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
const loginService = authLoginServiceFactory({ authDal, smtpService: smtp, tokenService });
const passwordService = authPaswordServiceFactory({ tokenService, smtpService: smtp, authDal });
const signupService = authSignupServiceFactory({ tokenService, smtpService: smtp, authDal });
const userService = userServiceFactory({ userDal });
const loginService = authLoginServiceFactory({ userDal, smtpService, tokenService });
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,
authDal,
userDal
});
const orgService = orgServiceFactory({
orgRoleDal,
permissionService,
orgDal,
incidentContactDal,
tokenService,
smtpService,
userDal
});
const signupService = authSignupServiceFactory({
tokenService,
smtpService,
authDal,
userDal,
orgDal,
orgService
});
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDal });
const serverCfgService = serverCfgServiceFactory({
userDal,
authService: loginService,
serverCfgDal
});
const apiKeyService = apiKeyServiceFactory({ apiKeyDal });
await serverCfgService.initServerCfg();
// inject all services
server.decorate("services", {
login: loginService,
password: passwordService,
signup: signupService
signup: signupService,
user: userService,
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
serverCfg: serverCfgService,
apiKey: apiKeyService,
authToken: tokenService
} as FastifyZodProvider["services"]);
server.decorate("store", {
user: authDal
user: userDal
} as FastifyZodProvider["store"]);
await server.register(injectIdentity);
server.route({
url: "/status",
method: "GET",
schema: {
response: {
200: z.object({
date: z.date(),
message: z.literal("Ok"),
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional()
})
}
},
handler: () => {
const appCfg = getConfig();
return {
date: new Date(),
message: "Ok" as const,
emailConfigured: appCfg.isSmtpConfigured,
inviteOnlySignup: false,
redisConfigured: false,
secretScanningConfigured: false
};
}
});
// register routes for v1
await server.register(registerV1Routes, { prefix: "/v1" });
await server.register(registerV2Routes, { prefix: "/v2" });
await server.register(registerV3Routes, { prefix: "/v3" });
await server.register(registerV1EERoutes, { prefix: "/ee/v1" });
};

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
};
}
});
};

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 };
}
});
};

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 { registerUserActionRouter } from "./user-action-router";
import { registerUserRouter } from "./user-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerAuthRoutes, { prefix: "/auth" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerUserRouter, { prefix: "/user" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" });
};

View File

@@ -0,0 +1,70 @@
import { z } from "zod";
import { UsersSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/signup",
method: "POST",
schema: {
body: z.object({
inviteeEmail: z.string().trim().email(),
organizationId: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
completeInviteLink: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const completeInviteLink = await server.services.org.inviteUserToOrganization({
orgId: req.body.organizationId,
userId: req.auth.userId,
inviteeEmail: req.body.inviteeEmail
});
return {
completeInviteLink,
message: `Send an invite link to ${req.body.inviteeEmail}`
};
}
});
server.route({
url: "/verify",
method: "POST",
schema: {
body: z.object({
email: z.string().trim().email(),
organizationId: z.string().trim(),
code: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
token: z.string().optional(),
user: UsersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { user, token } = await server.services.org.verifyUserToOrg({
orgId: req.body.organizationId,
code: req.body.code,
email: req.body.email
});
return {
message: "Successfully verified email",
user,
token
};
}
});
};

View File

@@ -0,0 +1,183 @@
import { z } from "zod";
import {
IncidentContactsSchema,
OrganizationsSchema,
OrgMembershipsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: z.object({
organizations: OrganizationsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const organizations = await server.services.org.findAllOrganizationOfUser(req.auth.userId);
return { organizations };
}
});
server.route({
method: "GET",
url: "/:organizationId",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
organization: OrganizationsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const organization = await server.services.org.findOrganizationById(
req.auth.userId,
req.params.organizationId
);
return { organization };
}
});
server.route({
method: "GET",
url: "/:organizationId/users",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
users: OrgMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const users = await server.services.org.findAllOrgMembers(
req.auth.userId,
req.params.organizationId
);
return { users };
}
});
// TODO(akhilmhdh-pg): missing my-workspace list
server.route({
method: "PATCH",
url: "/:organizationId/name",
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ name: z.string().trim() }),
response: {
200: z.object({
message: z.string(),
organization: OrganizationsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const organization = await server.services.org.updateOrgName(
req.auth.userId,
req.params.organizationId,
req.body.name
);
return {
message: "Successfully changed organization name",
organization
};
}
});
server.route({
method: "GET",
url: "/:organizationId/incidentContactOrg",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.object({
incidentContactsOrg: IncidentContactsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const incidentContactsOrg = await req.server.services.org.findIncidentContacts(
req.auth.userId,
req.params.organizationId
);
return { incidentContactsOrg };
}
});
server.route({
method: "POST",
url: "/:organizationId/incidentContactOrg",
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ email: z.string().email().trim() }),
response: {
200: z.object({
incidentContactsOrg: IncidentContactsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const incidentContactsOrg = await req.server.services.org.createIncidentContact(
req.auth.userId,
req.params.organizationId,
req.body.email
);
return { incidentContactsOrg };
}
});
server.route({
method: "DELETE",
url: "/:organizationId/incidentContactOrg/:incidentContactId",
schema: {
// TODO(akhilmhdh-pg): change accept id instead of email
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
response: {
200: z.object({
incidentContactsOrg: IncidentContactsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const incidentContactsOrg = await req.server.services.org.deleteIncidentContact(
req.auth.userId,
req.params.organizationId,
req.params.incidentContactId
);
return { incidentContactsOrg };
}
});
};

View File

@@ -0,0 +1,54 @@
import { z } from "zod";
import { UserActionsSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserActionRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
action: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
userAction: UserActionsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const userAction = await server.services.user.createUserAction(
req.auth.userId,
req.body.action
);
return { userAction, message: "Successfully recorded user action" };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
action: z.string().trim()
}),
response: {
200: z.object({
userAction: UserActionsSchema.optional().nullable()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const userAction = await server.services.user.getUserAction(
req.auth.userId,
req.query.action
);
return { userAction };
}
});
};

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { UsersSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: z.object({
user: UsersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const user = await server.services.user.getMe(req.auth.userId);
return { user };
}
});
};

View File

@@ -1,5 +1,7 @@
import { registerMfaRouter } from "./mfa-router";
import { registerUserRouter } from "./user-router";
export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" });
};

View File

@@ -25,7 +25,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
throw new Error("Unauthorized access");
const user = await server.store.user.getUserById(decodedToken.userId);
const user = await server.store.user.findById(decodedToken.userId);
if (!user) throw new Error("User not found");
req.mfa = { userId: user.id, user };
});

View File

@@ -0,0 +1,140 @@
import { z } from "zod";
import {
OrganizationsSchema,
OrgMembershipsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/memberships",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
users: OrgMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const users = await server.services.org.findAllOrgMembers(
req.auth.userId,
req.params.organizationId
);
return { users };
}
});
server.route({
method: "PATCH",
url: "/:organizationId/memberships/:membershipId",
schema: {
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
body: z.object({
role: z.string().trim()
}),
response: {
200: z.object({
membership: OrgMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const membership = await server.services.org.updateOrgMembership({
userId: req.auth.userId,
role: req.body.role,
orgId: req.params.organizationId,
membershipId: req.params.membershipId
});
return { membership };
}
});
server.route({
method: "DELETE",
url: "/:organizationId/memberships/:membershipId",
schema: {
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
response: {
200: z.object({
membership: OrgMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const membership = await server.services.org.deleteOrgMembership({
userId: req.auth.userId,
orgId: req.params.organizationId,
membershipId: req.params.membershipId
});
return { membership };
}
});
server.route({
method: "POST",
url: "/",
schema: {
body: z.object({
name: z.string().trim()
}),
response: {
200: z.object({
organization: OrganizationsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const organization = await server.services.org.createOrganization(
req.auth.userId,
req.body.name
);
return { organization };
}
});
server.route({
method: "DELETE",
url: "/:organizationId",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
organization: OrganizationsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const organization = await server.services.org.deleteOrganizationById(
req.auth.userId,
req.params.organizationId
);
return { organization };
}
});
};

View File

@@ -0,0 +1,225 @@
import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/me/mfa",
method: "PATCH",
schema: {
body: z.object({
isMfaEnabled: z.boolean()
}),
response: {
200: z.object({
user: UsersSchema
})
}
},
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const user = await server.services.user.toggleUserMfa(req.auth.userId, req.body.isMfaEnabled);
return { user };
}
});
server.route({
url: "/me/name",
method: "PATCH",
schema: {
body: z.object({
firstName: z.string().trim(),
lastName: z.string().trim()
}),
response: {
200: z.object({
user: UsersSchema
})
}
},
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const user = await server.services.user.updateUserName(
req.auth.userId,
req.body.firstName,
req.body.lastName
);
return { user };
}
});
server.route({
url: "/me/auth-methods",
method: "PUT",
schema: {
body: z.object({
authMethods: z.nativeEnum(AuthMethod).array().min(1)
}),
response: {
200: z.object({
user: UsersSchema
})
}
},
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const user = await server.services.user.updateAuthMethods(
req.auth.userId,
req.body.authMethods
);
return { user };
}
});
server.route({
method: "GET",
url: "/me/organizations",
schema: {
response: {
200: z.object({
organizations: OrganizationsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const organizations = await server.services.org.findAllOrganizationOfUser(req.auth.userId);
return { organizations };
}
});
server.route({
method: "GET",
url: "/me/api-keys",
schema: {
response: {
200: ApiKeysSchema.omit({ secretHash: true }).array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const apiKeys = await server.services.apiKey.getMyApiKeys(req.auth.userId);
return apiKeys;
}
});
server.route({
method: "POST",
url: "/me/api-keys",
schema: {
body: z.object({
name: z.string().trim(),
expiresIn: z.number()
}),
response: {
200: z.object({
apiKey: z.string(),
apiKeyData: ApiKeysSchema.omit({ secretHash: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const apiKeys = await server.services.apiKey.createApiKey(
req.auth.userId,
req.body.name,
req.body.expiresIn
);
return apiKeys;
}
});
server.route({
method: "DELETE",
url: "/me/api-keys/:apiKeyDataId",
schema: {
params: z.object({
apiKeyDataId: z.string().trim()
}),
response: {
200: z.object({
apiKeyData: ApiKeysSchema.omit({ secretHash: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const apiKeyData = await server.services.apiKey.deleteApiKey(
req.auth.userId,
req.params.apiKeyDataId
);
return { apiKeyData };
}
});
server.route({
method: "GET",
url: "/me/sessions",
schema: {
response: {
200: AuthTokenSessionsSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const sessions = await server.services.authToken.getTokenSessionByUser(req.auth.userId);
return sessions;
}
});
server.route({
method: "DELETE",
url: "/me/sessions",
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.authToken.revokeAllMySessions(req.auth.userId);
return {
message: "Successfully revoked all sessions"
};
}
});
server.route({
method: "GET",
url: "/me",
schema: {
response: {
200: z.object({
user: UsersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const user = await server.services.user.getMe(req.auth.userId);
return { user };
}
});
server.route({
method: "DELETE",
url: "/me",
schema: {
response: {
200: z.object({
user: UsersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const user = await server.services.user.deleteMe(req.auth.userId);
return { user };
}
});
};

View File

@@ -1,7 +1,9 @@
import { registerLoginRouter } from "./login-router";
import { registerSignupRouter } from "./signup-router";
import { registerUserRouter } from "./user-router";
export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerSignupRouter, { prefix: "/signup" });
await server.register(registerLoginRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" });
};

View File

@@ -100,4 +100,54 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
return { message: "Successfully set up account", user, token: accessToken };
}
});
server.route({
url: "/complete-account/invite",
method: "POST",
schema: {
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
user: UsersSchema,
token: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const appCfg = getConfig();
const { user, accessToken, refreshToken } =
await server.services.signup.completeAccountInvite({
...req.body,
ip: req.realIp,
userAgent
});
res.setCookie("jid", refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
// TODO(akhilmhdh-pg): add telemetry service
return { message: "Successfully set up account", user, token: accessToken };
}
});
};

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me/api-keys",
schema: {
response: {
200: z.object({
apiKeyData: ApiKeysSchema.omit({ secretHash: true }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const apiKeyData = await server.services.apiKey.getMyApiKeys(req.auth.userId);
return { apiKeyData };
}
});
};

View File

@@ -0,0 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TApiKeyDalFactory = ReturnType<typeof apiKeyDalFactory>;
export const apiKeyDalFactory = (db: TDbClient) => ormify(db, TableName.ApiKey);

View File

@@ -0,0 +1,55 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { TApiKeys } from "@app/db/schemas/api-keys";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { TApiKeyDalFactory } from "./api-key-dal";
type TApiKeyServiceFactoryDep = {
apiKeyDal: TApiKeyDalFactory;
};
export type TApiKeyServiceFactory = ReturnType<typeof apiKeyServiceFactory>;
const formatApiKey = ({ secretHash, ...data }: TApiKeys) => data;
export const apiKeyServiceFactory = ({ apiKeyDal }: TApiKeyServiceFactoryDep) => {
const getMyApiKeys = async (userId: string) => {
const apiKeys = await apiKeyDal.find({ userId });
return apiKeys.map((key) => formatApiKey(key));
};
const createApiKey = async (userId: string, name: string, expiresIn: number) => {
const appCfg = getConfig();
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, appCfg.SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const apiKeyData = await apiKeyDal.create({
userId,
name,
expiresAt,
secretHash,
lastUsed: new Date()
});
const apiKey = `ak.${apiKeyData.id}.${secret}`;
return { apiKey, apiKeyData: formatApiKey(apiKeyData) };
};
const deleteApiKey = async (userId: string, apiKeyId: string) => {
const [apiKeyData] = await apiKeyDal.delete({ id: apiKeyId, userId });
if (!apiKeyData)
throw new BadRequestError({ message: "Failed to find api key", name: "delete api key" });
return formatApiKey(apiKeyData);
};
return {
getMyApiKeys,
createApiKey,
deleteApiKey
};
};

View File

@@ -1,110 +1,22 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TBackupPrivateKey, TUserEncryptionKeys, TUsers } from "@app/db/schemas";
import { TableName, TBackupPrivateKey } from "@app/db/schemas";
import { withTransaction } from "@app/lib/knex";
export type TAuthDalFactory = ReturnType<typeof authDalFactory>;
export const authDalFactory = (db: TDbClient) => {
// getters
const getUserByEmail = async (email: string): Promise<TUsers | undefined> =>
db(TableName.Users).where({ email }).select("*").first();
const getUserById = async (userId: string): Promise<TUsers | undefined> =>
db(TableName.Users).where({ id: userId }).select("*").first();
const getUserEncKeyByEmail = async (email: string) =>
db(TableName.Users)
.where({ email })
.join(
TableName.UserEncryptionKey,
`${TableName.Users}.id`,
`${TableName.UserEncryptionKey}.userId`
)
.first();
const getUserEncKeyByUserId = async (userId: string) =>
db(TableName.Users)
.where({ id: userId })
.join(
TableName.UserEncryptionKey,
`${TableName.Users}.id`,
`${TableName.UserEncryptionKey}.userId`
)
.first();
const getBackupPrivateKeyByUserId = async (userId: string) =>
db(TableName.BackupPrivateKey).where({ userId }).first("*");
// all inserts and updates
const createUser = async (
email: string,
data: Partial<TUsers> = {}
): Promise<TUsers | undefined> => {
const [user] = await db(TableName.Users)
.insert({ email, ...data })
.returning("*");
return user;
};
const updateUser = async (
email: string,
data: Partial<TUsers> = {}
): Promise<TUsers | undefined> => {
const [user] = await db(TableName.Users)
.where({ email })
.update({ ...data })
.returning("*");
return user;
};
const updateUserById = async (
id: string,
data: Partial<TUsers> = {},
tx?: Knex
): Promise<TUsers | undefined> => {
const [user] = await (tx ? tx(TableName.Users) : db(TableName.Users))
.where({ id })
.update({ ...data })
.returning("*");
return user;
};
const updateUserEncryptionByUserId = async (
userId: string,
data: Partial<TUserEncryptionKeys> = {},
tx?: Knex
): Promise<TUserEncryptionKeys | undefined> => {
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
.where({ userId })
.update({ ...data })
.returning("*");
return userEnc;
};
// all upserts
const upsertUserEncryptionKey = async (
userId: string,
data: Partial<TUserEncryptionKeys>,
tx?: Knex
) => {
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
// if user insert make sure to pass all required data
.insert({ userId, ...data } as TUserEncryptionKeys)
.onConflict("userId")
.merge()
.returning("*");
return userEnc;
};
const upsertBackupKey = async (
userId: string,
data: Partial<TBackupPrivateKey>,
tx?: Knex
): Promise<TBackupPrivateKey | undefined> => {
const [backupKey] = await (tx ? tx(TableName.BackupPrivateKey) : db(TableName.BackupPrivateKey))
.insert({ userId, ...data, updatedAt: new Date().toUTCString() } as TBackupPrivateKey)
const [backupKey] = await (tx || db)(TableName.BackupPrivateKey)
.insert({ userId, ...data, updatedAt: new Date() } as TBackupPrivateKey)
.onConflict("userId")
.merge()
.returning("*");
@@ -112,16 +24,7 @@ export const authDalFactory = (db: TDbClient) => {
};
return withTransaction(db, {
getUserByEmail,
getUserById,
getUserEncKeyByEmail,
getUserEncKeyByUserId,
getBackupPrivateKeyByUserId,
createUser,
updateUser,
updateUserById,
updateUserEncryptionByUserId,
upsertUserEncryptionKey,
upsertBackupKey
});
};

View File

@@ -7,7 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types";
import { TAuthDalFactory } from "./auth-dal";
import { TUserDalFactory } from "../user/user-dal";
import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
@@ -25,14 +25,14 @@ const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuth
};
type TAuthLoginServiceFactoryDep = {
authDal: TAuthDalFactory;
userDal: TUserDalFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
export const authLoginServiceFactory = ({
authDal,
userDal,
tokenService,
smtpService
}: TAuthLoginServiceFactoryDep) => {
@@ -42,14 +42,14 @@ export const authLoginServiceFactory = ({
* If new device is found. Will be saved and a mail will be send
*/
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
const devices = await UserDeviceSchema.parseAsync(JSON.parse(user.devices || "[]"));
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
const isDeviceSeen = devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]);
await authDal.updateUserById(user.id, { devices: JSON.stringify(newDeviceList) });
await userDal.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin,
subjectLine: "Successful login from new device",
@@ -68,23 +68,23 @@ export const authLoginServiceFactory = ({
* Private
* Send mfa code via email
* */
const sendUserMfaCode = async (user: TUsers) => {
const sendUserMfaCode = async (userId: string, email: string) => {
const code = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId: user.id
userId
});
await smtpService.sendMail({
template: SmtpTemplates.EmailMfa,
subjectLine: "Infisical MFA code",
recipients: [user.email],
recipients: [email],
substitutions: {
code
}
});
};
/* Private
/*
* Check user device and send mail if new device
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
*/
@@ -130,20 +130,19 @@ export const authLoginServiceFactory = ({
providerAuthToken,
clientPublicKey
}: TLoginGenServerPublicKeyDTO) => {
const user = await authDal.getUserEncKeyByEmail(email);
if (!user || (user && !user.isAccepted)) {
const userEnc = await userDal.findUserEncKeyByEmail(email);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
const cfg = getConfig();
if (
!user.authMethods?.includes(AuthMethod.EMAIL) &&
!userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
) {
throw new Error("Invalid authorization request");
}
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
@@ -161,46 +160,46 @@ export const authLoginServiceFactory = ({
ip,
userAgent
}: TLoginClientProofDTO) => {
const user = await authDal.getUserEncKeyByEmail(email);
if (!user) throw new Error("Failed to find user");
const userEnc = await userDal.findUserEncKeyByEmail(email);
if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig();
if (
!user.authMethods?.includes(AuthMethod.EMAIL) &&
!userEnc.authMethods?.includes(AuthMethod.EMAIL) &&
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
) {
throw new Error("Invalid authorization request");
}
if (!user.serverPrivateKey || !user.clientPublicKey)
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
user.salt,
user.verifier,
user.serverPrivateKey,
user.clientPublicKey,
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await authDal.updateUserEncryptionByUserId(user.id, {
await userDal.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
// send multi factor auth token if they it enabled
if (user.isMfaEnabled) {
if (userEnc.isMfaEnabled) {
const mfaToken = jwt.sign(
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: user.id },
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: userEnc.userId },
cfg.JWT_AUTH_SECRET,
{ expiresIn: cfg.JWT_MFA_LIFETIME }
);
await sendUserMfaCode(user);
await sendUserMfaCode(userEnc.userId, userEnc.email);
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens(user, ip, userAgent);
return { token, isMfaEnabled: false, user } as const;
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, isMfaEnabled: false, user: userEnc } as const;
};
/*
@@ -208,9 +207,9 @@ export const authLoginServiceFactory = ({
* saved in frontend
*/
const resendMfaToken = async (userId: string) => {
const user = await authDal.getUserById(userId);
const user = await userDal.findById(userId);
if (!user) return;
await sendUserMfaCode(user);
await sendUserMfaCode(user.id, user.email);
};
/*
@@ -223,11 +222,11 @@ export const authLoginServiceFactory = ({
userId,
code: mfaToken
});
const user = await authDal.getUserEncKeyByUserId(userId);
if (!user) throw new Error("Failed to authenticate user");
const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to authenticate user");
const token = await generateUserTokens(user, ip, userAgent);
return { token, user };
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
return { token, user: userEnc };
};
/*
@@ -243,6 +242,7 @@ export const authLoginServiceFactory = ({
loginExchangeClientProof,
logout,
resendMfaToken,
verifyMfaToken
verifyMfaToken,
generateUserTokens
};
};

View File

@@ -6,6 +6,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types";
import { TUserDalFactory } from "../user/user-dal";
import { TAuthDalFactory } from "./auth-dal";
import {
TChangePasswordDTO,
@@ -16,6 +17,7 @@ import { AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = {
authDal: TAuthDalFactory;
userDal: TUserDalFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
@@ -23,6 +25,7 @@ type TAuthPasswordServiceFactoryDep = {
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
export const authPaswordServiceFactory = ({
authDal,
userDal,
tokenService,
smtpService
}: TAuthPasswordServiceFactoryDep) => {
@@ -31,11 +34,11 @@ export const authPaswordServiceFactory = ({
* Gets srp server user salt and server public key
*/
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
const user = await authDal.getUserEncKeyByUserId(userId);
if (!user) throw new Error("Failed to find user");
const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDal.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
@@ -59,10 +62,10 @@ export const authPaswordServiceFactory = ({
verifier,
tokenVersionId
}: TChangePasswordDTO) => {
const userEnc = await authDal.getUserEncKeyByUserId(userId);
const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
await authDal.updateUserEncryptionByUserId(userEnc.userId, {
await userDal.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
@@ -77,7 +80,7 @@ export const authPaswordServiceFactory = ({
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await authDal.updateUserEncryptionByUserId(userId, {
await userDal.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
@@ -100,7 +103,7 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email
*/
const sendPasswordResetEmail = async (email: string) => {
const user = await authDal.getUserByEmail(email);
const user = await userDal.findUserByEmail(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) return;
@@ -127,7 +130,7 @@ export const authPaswordServiceFactory = ({
* */
const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig();
const user = await authDal.getUserByEmail(email);
const user = await userDal.findUserByEmail(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset");
@@ -166,7 +169,7 @@ export const authPaswordServiceFactory = ({
encryptedPrivateKeyTag
}: TResetPasswordViaBackupKeyDTO
) => {
await authDal.updateUserEncryptionByUserId(userId, {
await userDal.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
@@ -193,23 +196,23 @@ export const authPaswordServiceFactory = ({
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const user = await authDal.getUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
const userEnc = await userDal.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!user.clientPublicKey || !user.serverPrivateKey)
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey)
throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
user.salt,
user.verifier,
user.serverPrivateKey,
user.clientPublicKey,
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDal.transaction(async (tx) => {
const backupKey = await authDal.upsertBackupKey(user.id, {
const backupKey = await authDal.upsertBackupKey(userEnc.userId, {
encryptedPrivateKey,
iv,
tag,
@@ -217,8 +220,8 @@ export const authPaswordServiceFactory = ({
verifier
});
await authDal.updateUserEncryptionByUserId(
user.id,
await userDal.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
@@ -235,7 +238,7 @@ export const authPaswordServiceFactory = ({
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await authDal.getUserEncKeyByUserId(userId);
const user = await userDal.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed to find user");
}

View File

@@ -1,17 +1,25 @@
import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TOrgDalFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TAuthTokenServiceFactory } from "../token/token-service";
import { TokenType } from "../token/token-types";
import { TUserDalFactory } from "../user/user-dal";
import { TAuthDalFactory } from "./auth-dal";
import { TCompleteAccountSignupDTO } from "./auth-signup-type";
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthSignupDep = {
authDal: TAuthDalFactory;
userDal: TUserDalFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgDal: TOrgDalFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
@@ -19,8 +27,11 @@ type TAuthSignupDep = {
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
export const authSignupServiceFactory = ({
authDal,
userDal,
tokenService,
smtpService
smtpService,
orgService,
orgDal
}: TAuthSignupDep) => {
// first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => {
@@ -29,13 +40,13 @@ export const authSignupServiceFactory = ({
throw new Error("Provided a disposable email");
}
let user = await authDal.getUserByEmail(email);
let user = await userDal.findUserByEmail(email);
if (user && user.isAccepted) {
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account");
}
if (!user) {
user = await authDal.createUser(email, { authMethods: [AuthMethod.EMAIL] });
user = await userDal.create({ authMethods: [AuthMethod.EMAIL], email });
}
if (!user) throw new Error("Failed to create user");
@@ -55,7 +66,7 @@ export const authSignupServiceFactory = ({
};
const verifyEmailSignup = async (email: string, code: string) => {
const user = await authDal.getUserByEmail(email);
const user = await userDal.findUserByEmail(email);
if (!user || (user && user.isAccepted)) {
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account");
@@ -91,7 +102,7 @@ export const authSignupServiceFactory = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
// organizationName,
organizationName,
// attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@@ -99,19 +110,15 @@ export const authSignupServiceFactory = ({
ip,
userAgent
}: TCompleteAccountSignupDTO) => {
const user = await authDal.getUserByEmail(email);
const user = await userDal.findUserByEmail(email);
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
}
const updateduser = await authDal.transaction(async (tx) => {
const us = await authDal.updateUserById(
user.id,
{ firstName, lastName, isAccepted: true },
tx
);
const us = await userDal.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
const userEncKey = await authDal.upsertUserEncryptionKey(
const userEncKey = await userDal.upsertUserEncryptionKey(
us.id,
{
salt,
@@ -129,7 +136,116 @@ export const authSignupServiceFactory = ({
return { info: us, key: userEncKey };
});
// TODO(akhilmhdh-pg): add default org memberships
const hasSamlEnabled = user?.authMethods?.some((authMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(
authMethod as AuthMethod
)
);
if (!hasSamlEnabled) {
await orgService.createOrganization(user.id, organizationName);
}
await orgDal.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted }
);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion
},
appCfg.JWT_AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
);
const refreshToken = jwt.sign(
{
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion
},
appCfg.JWT_AUTH_SECRET,
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
);
return { user: updateduser.info, accessToken, refreshToken };
};
/*
* User signup flow when they are invited to join the org
* */
const completeAccountInvite = async ({
ip,
salt,
email,
verifier,
firstName,
publicKey,
userAgent,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}: TCompleteAccountInviteDTO) => {
const user = await userDal.findUserByEmail(email);
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
}
const [orgMembership] = await orgDal.findMembership({
inviteEmail: email,
status: OrgMembershipStatus.Invited
});
if (!orgMembership)
throw new BadRequestError({
message: "Failed to find invitation for email",
name: "complete account invite"
});
const updateduser = await authDal.transaction(async (tx) => {
const us = await userDal.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
const userEncKey = await userDal.upsertUserEncryptionKey(
us.id,
{
salt,
encryptionVersion: 2,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
},
tx
);
await orgDal.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted },
tx
);
return { info: us, key: userEncKey };
});
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
@@ -166,6 +282,7 @@ export const authSignupServiceFactory = ({
return {
beginEmailSignupProcess,
verifyEmailSignup,
completeEmailAccountSignup
completeEmailAccountSignup,
completeAccountInvite
};
};

View File

@@ -17,3 +17,20 @@ export type TCompleteAccountSignupDTO = {
ip: string;
userAgent: string;
};
export type TCompleteAccountInviteDTO = {
email: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string;
userAgent: string;
};

View File

@@ -33,3 +33,10 @@ export type AuthModeJwtTokenPayload = {
tokenVersionId: string;
accessVersion: number;
};
export type AuthModeRefreshJwtTokenPayload = {
authTokenType: AuthTokenType.REFRESH_TOKEN;
userId: string;
tokenVersionId: string;
refreshVersion: number;
};

View File

@@ -0,0 +1,57 @@
import { TDbClient } from "@app/db";
import { TableName, TIncidentContacts } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
export type TIncidentContactsDalFactory = ReturnType<typeof incidentContactDalFactory>;
export const incidentContactDalFactory = (db: TDbClient) => {
const create = async (orgId: string, email: string) => {
try {
const [incidentContact] = await db(TableName.IncidentContact)
.insert({ orgId, email })
.returning("*");
return incidentContact;
} catch (error) {
throw new DatabaseError({ name: "Incident contact create", error });
}
};
const findByOrgId = async (orgId: string) => {
try {
const incidentContacts = await db(TableName.IncidentContact).where({ orgId });
return incidentContacts;
} catch (error) {
throw new DatabaseError({ name: "Incident contact list", error });
}
};
const findOne = async (orgId: string, data: Partial<TIncidentContacts>) => {
try {
const incidentContacts = await db(TableName.IncidentContact)
.where({ orgId, ...data })
.first();
return incidentContacts;
} catch (error) {
throw new DatabaseError({ name: "Incident contact find one", error });
}
};
const deleteById = async (id: string, orgId: string) => {
try {
const [incidentContact] = await db(TableName.IncidentContact)
.where({ orgId, id })
.delete()
.returning("*");
return incidentContact;
} catch (error) {
throw new DatabaseError({ name: "Incident contact delete", error });
}
};
return {
findByOrgId,
findOne,
create,
deleteById
};
};

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
});
};

View File

@@ -0,0 +1,67 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName,TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { withTransaction } from "@app/lib/knex";
export type TOrgRoleDalFactory = ReturnType<typeof orgRoleDalFactory>;
export const orgRoleDalFactory = (db: TDbClient) => {
const find = async (data: TOrgRolesUpdate, tx?: Knex) => {
try {
const role = await (tx || db)(TableName.OrgRoles).where(data);
return role;
} catch (error) {
throw new DatabaseError({ error, name: "Org role find one" });
}
};
const findOne = async (data: TOrgRolesUpdate, tx?: Knex) => {
try {
const role = await (tx || db)(TableName.OrgRoles).where(data).first();
return role;
} catch (error) {
throw new DatabaseError({ error, name: "Org role find one" });
}
};
const create = async (data: TOrgRolesInsert, tx?: Knex) => {
try {
const [role] = await (tx || db)(TableName.OrgRoles).insert(data).returning("*");
return role;
} catch (error) {
throw new DatabaseError({ error, name: "Org role create" });
}
};
const updateOne = async (
filter: { id: string; orgId: string },
data: TOrgRolesUpdate,
tx?: Knex
) => {
try {
const [role] = await (tx || db)(TableName.OrgRoles).where(filter).update(data).returning("*");
return role;
} catch (error) {
throw new DatabaseError({ error, name: "Org role create" });
}
};
const deleteOne = async (filter: { id: string; orgId: string }, tx?: Knex) => {
try {
const [role] = await (tx || db)(TableName.OrgRoles).where(filter).delete().returning("*");
return role;
} catch (error) {
throw new DatabaseError({ error, name: "Org role create" });
}
};
return withTransaction(db, {
find,
findOne,
create,
updateOne,
deleteOne
});
};

View File

@@ -0,0 +1,120 @@
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
import {
orgAdminPermissions,
orgMemberPermissions,
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError } from "@app/lib/errors";
import { TOrgRoleDalFactory } from "./org-role-dal";
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
type TOrgRoleServiceFactoryDep = {
orgRoleDal: TOrgRoleDalFactory;
permissionService: TPermissionServiceFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({
orgRoleDal,
permissionService
}: TOrgRoleServiceFactoryDep) => {
const createRole = async (
userId: string,
orgId: string,
data: Omit<TOrgRolesInsert, "orgId">
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Role
);
const existingRole = await orgRoleDal.findOne({ slug: data.slug, orgId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await orgRoleDal.create({ ...data, orgId });
return role;
};
const updateRole = async (
userId: string,
orgId: string,
roleId: string,
data: Omit<TOrgRolesUpdate, "orgId">
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Role
);
if (data?.slug) {
const existingRole = await orgRoleDal.findOne({ slug: data.slug, orgId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const updatedRole = await orgRoleDal.updateOne({ id: roleId, orgId }, { ...data });
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole;
};
const deleteRole = async (userId: string, orgId: string, roleId: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Role
);
const deletedRole = await orgRoleDal.deleteOne({ id: roleId, orgId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return deletedRole;
};
const listRoles = async (userId: string, orgId: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Role
);
const customRoles = await orgRoleDal.find({ orgId });
const roles = [
{
id: "admin",
orgId: "",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "member",
orgId: "",
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles;
};
const getUserPermission = async (userId: string, orgId: string) => {
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId);
return { permissions: packRules(permission.rules), membership };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
};

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
};
};

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;
};

View File

@@ -0,0 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TServerCfgDalFactory = ReturnType<typeof serverCfgDalFactory>;
export const serverCfgDalFactory = (db: TDbClient) => ormify(db, TableName.ServerConfig, {});

View File

@@ -0,0 +1,106 @@
import { TServerConfig, TServerConfigUpdate } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { TUserDalFactory } from "../user/user-dal";
import { TServerCfgDalFactory } from "./server-cfg-dal";
import { TAdminSignUpDTO } from "./server-cfg-types";
type TServerCfgServiceFactoryDep = {
serverCfgDal: TServerCfgDalFactory;
userDal: TUserDalFactory;
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
};
export type TServerCfgServiceFactory = ReturnType<typeof serverCfgServiceFactory>;
export const serverCfgServiceFactory = ({
serverCfgDal,
userDal,
authService
}: TServerCfgServiceFactoryDep) => {
let serverCfg: TServerConfig;
const initServerCfg = async () => {
serverCfg = await serverCfgDal.findOne({});
if (!serverCfg) {
const newCfg = await serverCfgDal.create({ initialized: true, allowSignUp: true });
serverCfg = newCfg;
return newCfg;
}
return serverCfg;
};
const getServerCfg = () => {
if (!serverCfg)
throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" });
return serverCfg;
};
const updateServerCfg = async (data: TServerConfigUpdate) => {
const cfg = await serverCfgDal.updateById(serverCfg.id, data);
return cfg;
};
const adminSignUp = async ({
lastName,
firstName,
salt,
email,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip,
userAgent
}: TAdminSignUpDTO) => {
const existingUser = await userDal.findOne({ email });
if (!existingUser)
throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
const userInfo = await userDal.transaction(async (tx) => {
const newUser = await userDal.create(
{
firstName,
lastName,
email,
superAdmin: true
},
tx
);
const userEnc = await userDal.createUserEncryption(
{
salt,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
verifier,
userId: newUser.id
},
tx
);
return { user: newUser, enc: userEnc };
});
await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens(userInfo.user, ip, userAgent);
// TODO(akhilmhdh-pg): telemetry service
return { token, user: userInfo };
};
return {
initServerCfg,
getServerCfg,
updateServerCfg,
adminSignUp
};
};

View File

@@ -0,0 +1,16 @@
export type TAdminSignUpDTO = {
email: string;
publicKey: string;
salt: string;
lastName?: string;
verifier: string;
firstName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
ip: string;
userAgent: string;
};

View File

@@ -1,55 +1,31 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import {
TDeleteTokenForUserDalDTO,
TGetTokenForUserDalDTO,
TUpsertTokenForUserDalDTO
} from "./token-types";
import { TDeleteTokenForUserDalDTO } from "./token-types";
export type TTokenDalConfig = {};
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
// TODO(akhilmhdh-pg): wrap all with database error
export const tokenDalFactory = (db: TDbClient) => {
const upsertTokenForUser = async ({
tokenHash,
expiresAt,
userId,
type,
triesLeft
}: TUpsertTokenForUserDalDTO): Promise<TAuthTokens | undefined> => {
const token = await db.transaction(async (tx) => {
await tx(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
const [newToken] = await tx(TableName.AuthTokens)
.insert({ tokenHash, expiresAt: expiresAt.toUTCString(), type, userId, triesLeft })
.returning("*");
return newToken;
});
return token;
};
const authOrm = ormify(db, TableName.AuthTokens);
const getTokenForUser = async ({
userId,
type
}: TGetTokenForUserDalDTO): Promise<TAuthTokens | undefined> =>
db(TableName.AuthTokens).where({ userId, type }).first();
const getTokenSession = async (
userId: string,
ip: string,
userAgent: string
const findOneTokenSession = async (
filter: Partial<TAuthTokenSessions>
): Promise<TAuthTokenSessions | undefined> =>
db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first();
const getTokenSessionById = async (id: string, userId: string) =>
db(TableName.AuthTokenSession).where({ id, userId }).first();
db(TableName.AuthTokenSession).where(filter).first();
const deleteTokenForUser = async ({
userId,
type
type,
orgId
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> =>
db(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
db(TableName.AuthTokens).where({ userId, type, orgId }).delete().returning("*");
const decrementTriesField = async ({
userId,
@@ -58,6 +34,15 @@ export const tokenDalFactory = (db: TDbClient) => {
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1);
};
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
try {
const sessions = await (tx || db)(TableName.AuthTokenSession).where(filter);
return sessions;
} catch (error) {
throw new DatabaseError({ name: "Find all token session", error });
}
};
const insertTokenSession = async (
userId: string,
ip: string,
@@ -70,13 +55,13 @@ export const tokenDalFactory = (db: TDbClient) => {
userAgent,
accessVersion: 1,
refreshVersion: 1,
lastUsed: new Date().toUTCString()
lastUsed: new Date()
})
.returning("*");
return session;
};
const incrementVersion = async (
const incrementTokenSessionVersion = async (
userId: string,
sessionId: string
): Promise<TAuthTokenSessions | undefined> => {
@@ -88,14 +73,26 @@ export const tokenDalFactory = (db: TDbClient) => {
return session;
};
const deleteTokenSession = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
try {
const sessions = await (tx || db)(TableName.AuthTokenSession)
.where(filter)
.del()
.returning("*");
return sessions;
} catch (error) {
throw new DatabaseError({ name: "Delete token session", error });
}
};
return {
getTokenForUser,
getTokenSessionById,
upsertTokenForUser,
...authOrm,
findTokenSessions,
deleteTokenForUser,
decrementTriesField,
getTokenSession,
findOneTokenSession,
insertTokenSession,
incrementVersion
incrementTokenSessionVersion,
deleteTokenSession
};
};

View File

@@ -56,30 +56,37 @@ export const getTokenConfig = (tokenType: TokenType) => {
};
export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId }: TCreateTokenForUserDTO) => {
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig();
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
await tokenDal.upsertTokenForUser({
userId,
type,
expiresAt: tkCfg.expiresAt,
tokenHash,
triesLeft: tkCfg?.triesLeft
await tokenDal.transaction(async (tx) => {
await tokenDal.delete({ userId, type, orgId: orgId || null }, tx);
const newToken = await tokenDal.create({
tokenHash,
expiresAt: tkCfg.expiresAt.toUTCString(),
type,
userId,
orgId,
triesLeft: tkCfg?.triesLeft
});
return newToken;
});
return token;
};
const validateTokenForUser = async ({
type,
userId,
code
code,
orgId
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
const token = await tokenDal.getTokenForUser({ type, userId });
const token = await tokenDal.findOne({ type, userId, orgId: orgId || null });
// validate token
if (!token) throw new Error("Failed to find token");
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) {
await tokenDal.deleteTokenForUser({ type, userId });
await tokenDal.delete({ type, userId, orgId });
throw new Error("Token expired. Please try again");
}
@@ -87,15 +94,15 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
if (!isValidToken) {
if (token?.triesLeft) {
if (token.triesLeft === 1) {
await tokenDal.deleteTokenForUser({ type, userId });
await tokenDal.deleteTokenForUser({ type, userId, orgId: orgId || null });
} else {
await tokenDal.decrementTriesField({ type, userId });
await tokenDal.decrementTriesField({ type, userId, orgId: orgId || null });
}
}
throw new Error("Invalid token");
}
const deletedToken = await tokenDal.deleteTokenForUser({ type, userId });
const deletedToken = await tokenDal.delete({ type, userId, orgId: orgId || null });
return deletedToken?.[0];
};
@@ -104,7 +111,7 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
ip,
userAgent
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDal.getTokenSession(userId, ip, userAgent);
let session = await tokenDal.findOneTokenSession({ userId, ip, userAgent });
if (!session) {
session = await tokenDal.insertTokenSession(userId, ip, userAgent);
}
@@ -112,18 +119,25 @@ export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) =
};
const getUserTokenSessionById = async (id: string, userId: string) =>
tokenDal.getTokenSessionById(id, userId);
tokenDal.findOneTokenSession({ id, userId });
const clearTokenSessionById = async (
userId: string,
sessionId: string
): Promise<TAuthTokenSessions | undefined> => tokenDal.incrementVersion(userId, sessionId);
): Promise<TAuthTokenSessions | undefined> =>
tokenDal.incrementTokenSessionVersion(userId, sessionId);
const getTokenSessionByUser = async (userId: string) => tokenDal.findTokenSessions({ userId });
const revokeAllMySessions = async (userId: string) => tokenDal.deleteTokenSession({ userId });
return {
createTokenForUser,
validateTokenForUser,
getUserTokenSession,
clearTokenSessionById,
getUserTokenSessionById
getUserTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions
};
};

View File

@@ -8,12 +8,19 @@ export enum TokenType {
export type TCreateTokenForUserDTO = {
type: TokenType;
userId: string;
orgId?: string;
};
export type TCreateOrgInviteTokenDTO = {
userId: string;
orgId: string;
};
export type TValidateTokenForUserDTO = {
type: TokenType;
code: string;
userId: string;
orgId?: string;
};
export type TUpsertTokenForUserDalDTO = {
@@ -32,6 +39,7 @@ export type TGetTokenForUserDalDTO = {
export type TDeleteTokenForUserDalDTO = {
userId: string;
type: TokenType;
orgId: string | null;
};
export type TIssueAuthTokenDTO = {

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
};
};

View File

@@ -0,0 +1,84 @@
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod } from "../auth/auth-type";
import { TUserDalFactory } from "./user-dal";
type TUserServiceFactoryDep = {
userDal: TUserDalFactory;
};
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ userDal }: TUserServiceFactoryDep) => {
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
const updatedUser = await userDal.updateById(userId, {
isMfaEnabled,
mfaMethods: isMfaEnabled ? ["email"] : []
});
return updatedUser;
};
const updateUserName = async (userId: string, firstName: string, lastName: string) => {
const updatedUser = await userDal.updateById(userId, {
firstName,
lastName
});
return updatedUser;
};
const updateAuthMethods = async (userId: string, authMethods: AuthMethod[]) => {
const user = await userDal.findById(userId);
if (!user) throw new BadRequestError({ name: "Update auth methods" });
const hasSamlEnabled = user?.authMethods?.some((method) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(
method as AuthMethod
)
);
if (hasSamlEnabled)
throw new BadRequestError({
name: "Update auth method",
message: "Failed to update auth methods due to SAML SSO "
});
const updatedUser = await userDal.updateById(userId, { authMethods });
return updatedUser;
};
const getMe = async (userId: string) => {
const user = await userDal.findUserEncKeyByUserId(userId);
if (!user) throw new BadRequestError({ message: "user not found", name: "Get Me" });
return user;
};
const deleteMe = async (userId: string) => {
const user = await userDal.deleteById(userId);
return user;
};
// user actions operations
const createUserAction = async (userId: string, action: string) => {
const userAction = await userDal.transaction(async (tx) => {
const existingAction = await userDal.findOneUserAction({ action, userId }, tx);
if (existingAction) return existingAction;
return userDal.createUserAction({ action, userId }, tx);
});
return userAction;
};
const getUserAction = async (userId: string, action: string) => {
const userAction = await userDal.findOneUserAction({ action, userId });
return userAction;
};
return {
toggleUserMfa,
updateUserName,
updateAuthMethods,
deleteMe,
getMe,
createUserAction,
getUserAction
};
};

View File

@@ -22,6 +22,6 @@
"@server/*": ["./src/server/*"]
}
},
"include": ["src/**/*"],
"include": ["src/**/*","scripts/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -5,6 +5,8 @@ import { requireAuth, validateRequest } from "../../middleware";
import { membershipOrgController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
// depreciated completely
// ignored for new codebase
router.post(
// TODO endpoint: check dashboard
"/membershipOrg/:membershipOrgId/change-role",

View File

@@ -25,6 +25,7 @@ services:
POSTGRES_DB: infisical
backend:
container_name: infisical-dev-api
build:
context: ./backend-pg
dockerfile: Dockerfile.dev

View File

@@ -72,7 +72,7 @@ export const completeAccountSignup = async (details: CompleteAccountSignupDTO) =
};
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
const { data } = await apiRequest.post("/api/v3/signup/complete-account/invite", details);
return data;
};

View File

@@ -42,10 +42,10 @@ export const useDeleteIncidentContact = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteIncidentContactDTO>({
mutationFn: async ({ orgId, email }) => {
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
data: { email }
});
mutationFn: async ({ orgId, incidentContactId }) => {
const { data } = await apiRequest.delete(
`/api/v1/organization/${orgId}/incidentContactOrg/${incidentContactId}`
);
return data;
},
onSuccess: (_, { orgId }) => {

View File

@@ -9,7 +9,7 @@ export type IncidentContact = {
export type DeleteIncidentContactDTO = {
orgId: string;
email: string;
incidentContactId: string;
};
export type AddIncidentContactDTO = {

View File

@@ -67,13 +67,11 @@ const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return { permissions: [], membership: null };
const { data } = await apiRequest.get<{
data: {
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
membership: OrgUser;
};
}>(`/api/v1/roles/organization/${orgId}/permissions`);
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
membership: OrgUser;
}>(`/api/ee/v1/organization/${orgId}/permissions`);
return data.data;
return data;
};
export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =>

View File

@@ -82,13 +82,14 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
!Component.requireAuth
) {
// TODO(akhilmhdh): bring back server config later
return (
<QueryClientProvider client={queryClient}>
<NotificationProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
<ServerConfigProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ServerConfigProvider>
</NotificationProvider>
</QueryClientProvider>
);

View File

@@ -95,6 +95,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
}
}
} catch (err) {
console.error(err);
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
@@ -236,7 +237,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : (
<div />
)}
<div className="flex flex-row text-sm text-bunker-400 mt-2">
<div className="mt-2 flex flex-row text-sm text-bunker-400">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Forgot password? Recover your account

View File

@@ -35,12 +35,12 @@ export const OrgIncidentContactsTable = () => {
const onRemoveIncidentContact = async () => {
try {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
const incidentContactId = (popUp?.removeContact?.data as { id: string })?.id;
if (!currentOrg?.id) return;
await mutateAsync({
orgId: currentOrg.id,
email: incidentContactEmail
incidentContactId
});
createNotification({
@@ -80,7 +80,7 @@ export const OrgIncidentContactsTable = () => {
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
{filteredContacts?.map(({ email }) => (
{filteredContacts?.map(({ email, id }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
@@ -92,7 +92,7 @@ export const OrgIncidentContactsTable = () => {
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })}
onClick={() => handlePopUpOpen("removeContact", { email, id })}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />