diff --git a/backend-pg/.dockerignore b/backend-pg/.dockerignore new file mode 100644 index 000000000..b484ea02a --- /dev/null +++ b/backend-pg/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.env +.env.* +.git +.gitignore +Dockerfile +.dockerignore +docker-compose.* +.DS_Store +*.swp +*~ diff --git a/backend-pg/Dockerfile.dev b/backend-pg/Dockerfile.dev new file mode 100644 index 000000000..a279dd381 --- /dev/null +++ b/backend-pg/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package.json +COPY package-lock.json package-lock.json + +RUN npm install + +COPY . . + +ENV HOST=0.0.0.0 + +CMD ["npm", "run", "dev:docker"] diff --git a/backend-pg/nodemon.json b/backend-pg/nodemon.json new file mode 100644 index 000000000..3f79eef77 --- /dev/null +++ b/backend-pg/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": ".ts,.js", + "ignore": [], + "exec": "tsx ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine" +} diff --git a/backend-pg/package-lock.json b/backend-pg/package-lock.json index a89eb39b6..35a5d2383 100644 --- a/backend-pg/package-lock.json +++ b/backend-pg/package-lock.json @@ -15,17 +15,29 @@ "@fastify/rate-limit": "^9.0.0", "@fastify/swagger": "^8.12.0", "@fastify/swagger-ui": "^1.10.1", + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "eslint-config-airbnb-typescript": "^17.1.0", "fastify": "^4.24.3", "fastify-plugin": "^4.5.1", + "handlebars": "^4.7.8", + "jsonwebtoken": "^9.0.2", + "jsrp": "^0.2.4", + "knex": "^3.0.1", + "nodemailer": "^6.9.7", "ora": "^7.0.1", + "pg": "^8.11.3", "pino": "^8.16.2", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.0" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.5", + "@types/jsrp": "^0.2.6", "@types/node": "^20.9.5", + "@types/nodemailer": "^6.4.14", + "@types/prompt-sync": "^4.2.3", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "eslint": "^8.54.0", @@ -35,7 +47,9 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-simple-import-sort": "^10.0.0", + "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", + "prompt-sync": "^4.2.0", "ts-node": "^10.9.1", "tsx": "^4.4.0", "typescript": "^5.3.2" @@ -712,6 +726,25 @@ "node": ">=8" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1007,6 +1040,15 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1017,6 +1059,21 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsrp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/jsrp/-/jsrp-0.2.6.tgz", + "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", @@ -1026,6 +1083,21 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prompt-sync": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", + "integrity": "sha512-Ox77gCSx0YyeakGt/qfOZUSFNSSi+sh3ABoGOiCwiO2KODx492BJnUm9oIXS+AHJtqp12iM4RduY6viTJ9bYwA==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -1301,6 +1373,11 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1345,6 +1422,38 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -1401,11 +1510,54 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1601,6 +1753,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -1610,6 +1775,15 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -1688,6 +1862,19 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -1735,6 +1922,50 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -1776,12 +2007,28 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1792,6 +2039,11 @@ "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1819,6 +2071,18 @@ "node": ">=6.6.0" } }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1935,6 +2199,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1943,6 +2212,14 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1990,6 +2267,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", @@ -2143,6 +2428,14 @@ "@esbuild/win32-x64": "0.18.20" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2579,6 +2872,14 @@ "node": ">=8" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2912,6 +3213,28 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2964,6 +3287,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -2978,6 +3357,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3017,6 +3404,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -3134,6 +3526,26 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3142,6 +3554,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", @@ -3189,6 +3610,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -3247,6 +3699,39 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -3283,6 +3768,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3333,6 +3824,14 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3365,6 +3864,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -3439,6 +3950,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3679,6 +4198,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3739,6 +4263,56 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsrp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz", + "integrity": "sha512-+CjGAhZaj3k2MMXEy+xWYv7xJGnise/SlL1IIvnRuJ1ZiLtNPJJln/dMDCgORQCq1ouXDnW1FBxW5bkBFhK/8g==", + "dependencies": { + "create-hash": "^1.0.0", + "jsbn": "^1.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3747,6 +4321,90 @@ "json-buffer": "3.0.1" } }, + "node_modules/knex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.0.1.tgz", + "integrity": "sha512-ruASxC6xPyDklRdrcDy6a9iqK+R9cGK214aiQa+D9gX2ZnHZKv6o6JC9ZfgxILxVAul4bZ13c3tgOAHSuQ7/9g==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.1", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3783,11 +4441,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", @@ -3814,12 +4512,44 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3884,6 +4614,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mnemonist": { "version": "0.39.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", @@ -3902,6 +4674,117 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemailer": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", + "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", + "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -3929,6 +4812,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -4142,6 +5044,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4190,6 +5097,94 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4267,6 +5262,41 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4316,6 +5346,36 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" }, + "node_modules/prompt-sync": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", + "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==", + "dev": true, + "dependencies": { + "strip-ansi": "^5.0.0" + } + }, + "node_modules/prompt-sync/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/prompt-sync/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4328,6 +5388,12 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4370,6 +5436,14 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/readable-stream": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", @@ -4385,6 +5459,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -4393,6 +5479,17 @@ "node": ">= 12.13.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -4520,6 +5617,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -4706,6 +5812,11 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -4743,6 +5854,18 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4780,6 +5903,18 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4796,6 +5931,14 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -4937,6 +6080,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4973,6 +6128,30 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4986,6 +6165,14 @@ "real-require": "^0.2.0" } }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -5025,6 +6212,38 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5199,6 +6418,18 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5213,6 +6444,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -5247,6 +6484,20 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5294,11 +6545,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/backend-pg/package.json b/backend-pg/package.json index 5ec5e63aa..6499ab5e9 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -6,15 +6,28 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "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": "eslint 'src/**/*.ts'" + "lint": "eslint 'src/**/*.ts'", + "generate:component": "tsx ./scripts/create-backend-file.ts", + "migration:new": "tsx ./scripts/create-migration.ts", + "migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", + "migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", + "migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", + "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", + "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.5", + "@types/jsrp": "^0.2.6", "@types/node": "^20.9.5", + "@types/nodemailer": "^6.4.14", + "@types/prompt-sync": "^4.2.3", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "eslint": "^8.54.0", @@ -24,7 +37,9 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-simple-import-sort": "^10.0.0", + "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", + "prompt-sync": "^4.2.0", "ts-node": "^10.9.1", "tsx": "^4.4.0", "typescript": "^5.3.2" @@ -36,11 +51,18 @@ "@fastify/rate-limit": "^9.0.0", "@fastify/swagger": "^8.12.0", "@fastify/swagger-ui": "^1.10.1", + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "eslint-config-airbnb-typescript": "^17.1.0", "fastify": "^4.24.3", "fastify-plugin": "^4.5.1", + "handlebars": "^4.7.8", + "jsonwebtoken": "^9.0.2", + "jsrp": "^0.2.4", + "knex": "^3.0.1", + "nodemailer": "^6.9.7", "ora": "^7.0.1", + "pg": "^8.11.3", "pino": "^8.16.2", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.0" diff --git a/backend-pg/scripts/create-backend-file.ts b/backend-pg/scripts/create-backend-file.ts new file mode 100644 index 000000000..2ea0a773a --- /dev/null +++ b/backend-pg/scripts/create-backend-file.ts @@ -0,0 +1,81 @@ +import { mkdirSync, writeFileSync } from "fs"; +import path from "path"; +import promptSync from "prompt-sync"; + +const prompt = promptSync({ + sigint: true +}); + +console.log(` +Component List +-------------- +1. Service component +2. Schema file +`); +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`; + + mkdirSync(dir); + + writeFileSync( + path.join(dir, `${componentName}-dal.ts`), + `import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; + +export type ${dalTypeName} = {}; + +export const ${dalName} = (db: TDbClient): ${dalTypeName} => { + + return { }; +}; +` + ); + + writeFileSync( + path.join(dir, `${componentName}-service.ts`), + `import { ${dalTypeName} } from "./${componentName}-dal"; + +type ${serviceTypeName}Dep = { + ${componentName}Dal: ${dalTypeName}; +}; + +export type ${serviceTypeName} = ReturnType; + +export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) => { + return {}; +}; +` + ); + writeFileSync(path.join(dir, `${componentName}-types.ts`), ""); +} else if (componentType === 2) { + const componentName = prompt("Type component name in lowercase with space seperated: "); + const dashcase = componentName.split(" ").join("-"); + const pascalCase = componentName + .split(" ") + .reduce( + (prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, + "" + ); + writeFileSync( + path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`), + ` +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ${pascalCase}Schema = z.object({}); + +export type T${pascalCase} = z.infer; +export type T${pascalCase}Insert = Omit; +export type T${pascalCase}Update = Partial>; +` + ); +} diff --git a/backend-pg/scripts/create-migration.ts b/backend-pg/scripts/create-migration.ts new file mode 100644 index 000000000..9a80f290e --- /dev/null +++ b/backend-pg/scripts/create-migration.ts @@ -0,0 +1,15 @@ +import { execSync } from "child_process"; +import path from "path"; +import promptSync from "prompt-sync"; + +const prompt = promptSync(); + +const migrationName = prompt("Enter name for migration: "); + +execSync( + `npx knex migrate:make --knexfile ${path.join( + __dirname, + "../src/db/knexfile.ts" + )} -x ts ${migrationName}`, + { stdio: "inherit" } +); diff --git a/backend-pg/src/@types/fastify-zod.d.ts b/backend-pg/src/@types/fastify-zod.d.ts new file mode 100644 index 000000000..02d28b28d --- /dev/null +++ b/backend-pg/src/@types/fastify-zod.d.ts @@ -0,0 +1,19 @@ +import { + FastifyInstance, + RawReplyDefaultExpression, + RawRequestDefaultExpression, + RawServerDefault +} from "fastify"; +import { Logger } from "pino"; + +import { ZodTypeProvider } from "@app/server/plugins/fastify-zod"; + +declare global { + type FastifyZodProvider = FastifyInstance< + RawServerDefault, + RawRequestDefaultExpression, + RawReplyDefaultExpression, + Readonly, + ZodTypeProvider + >; +} diff --git a/backend-pg/src/@types/fastify.d.ts b/backend-pg/src/@types/fastify.d.ts index 78cd61af4..33b744166 100644 --- a/backend-pg/src/@types/fastify.d.ts +++ b/backend-pg/src/@types/fastify.d.ts @@ -1,19 +1,32 @@ -import { ZodTypeProvider } from "@app/server/plugins/fastify-zod"; +import { TUser } from "@app/db/schemas"; +import { TAuthDalFactory } from "@app/services/auth/auth-dal"; +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 "fastify"; declare module "fastify" { interface FastifyRequest { realIp: string; + // used for mfa session authentication + mfa: { + userId: string; + user: TUser; + }; + } + + interface FastifyInstance { + services: { + login: TAuthLoginFactory; + password: TAuthPasswordFactory; + signup: TAuthSignupFactory; + }; + + // this is exclusive use for middlewares in which we need to inject data + // everywhere else access using service layer + store: { + user: Pick; + }; } } - -declare global { - type FastifyZodProvider = FastifyInstance< - RawServerDefault, - RawRequestDefaultExpression, - RawReplyDefaultExpression, - FastifyBaseLogger, - ZodTypeProvider - >; -} diff --git a/backend-pg/src/@types/knex.d.ts b/backend-pg/src/@types/knex.d.ts new file mode 100644 index 000000000..8b3776fe3 --- /dev/null +++ b/backend-pg/src/@types/knex.d.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +import { + TableName, + TBackupPrivateKey, + TBackupPrivateKeyInsert, + TToken, + TTokenInsert, + TTokenUpdate, + TUser, + TUserEncryptionKey, + TUserEncryptionKeyInsert, + TUserEncryptionKeyUpdate, + TUserInsert, + TUserUpdate +} from "@app/db/schemas"; +import { + TTokenSession, + TTokenSessionInsert, + TTokenSessionUpdate +} from "@app/db/schemas/token-session"; + +declare module "knex/types/tables" { + interface Tables extends { [key in TableName]: Knex.CompositeTableType } { + [TableName.Users]: Knex.CompositeTableType; + [TableName.UserEncryptionKey]: Knex.CompositeTableType< + TUserEncryptionKey, + TUserEncryptionKeyInsert, + TUserEncryptionKeyUpdate + >; + [TableName.AuthTokens]: Knex.CompositeTableType; + [TableName.AuthTokenSession]: Knex.CompositeTableType< + TTokenSession, + TTokenSessionInsert, + TTokenSessionUpdate + >; + [TableName.BackupPrivateKey]: Knex.CompositeTableType< + TBackupPrivateKey, + TBackupPrivateKeyInsert, + TTokenSessionUpdate + >; + } +} diff --git a/backend-pg/src/db/index.ts b/backend-pg/src/db/index.ts new file mode 100644 index 000000000..75992e2c6 --- /dev/null +++ b/backend-pg/src/db/index.ts @@ -0,0 +1,2 @@ +export type { TDbClient } from "./instance"; +export { initDbConnection } from "./instance"; diff --git a/backend-pg/src/db/instance.ts b/backend-pg/src/db/instance.ts new file mode 100644 index 000000000..1eb54c136 --- /dev/null +++ b/backend-pg/src/db/instance.ts @@ -0,0 +1,11 @@ +import knex from "knex"; + +export type TDbClient = ReturnType; +export const initDbConnection = (dbConnectionUri: string) => { + const db = knex({ + client: "pg", + connection: dbConnectionUri + }); + + return db; +}; diff --git a/backend-pg/src/db/knexfile.ts b/backend-pg/src/db/knexfile.ts new file mode 100644 index 000000000..071eb63fb --- /dev/null +++ b/backend-pg/src/db/knexfile.ts @@ -0,0 +1,24 @@ +import dotenv from "dotenv"; +import type { Knex } from "knex"; +import path from "path"; + +// eslint-disable-next-line +import "ts-node/register"; + +// Update with your config settings. +dotenv.config({ + path: path.join(__dirname, "../../.env"), + debug: true +}); +export default { + useNullAsDefault: true, + client: "postgres", + connection: process.env.DB_CONNECTION_URI, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: "infisical_migrations" + } +} as Knex.Config; diff --git a/backend-pg/src/db/migrations/20231128072457_user.ts b/backend-pg/src/db/migrations/20231128072457_user.ts new file mode 100644 index 000000000..9fd26031c --- /dev/null +++ b/backend-pg/src/db/migrations/20231128072457_user.ts @@ -0,0 +1,37 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { + createOnUpdateTrigger, + createUpdateAtTriggerFunction, + dropOnUpdateTrigger, + dropUpdatedAtTriggerFunction +} from "../utils"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.Users); + if (!isTablePresent) { + await knex.schema.createTable(TableName.Users, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("email").notNullable(); + t.specificType("authMethods", "text[]"); + t.boolean("superAdmin").defaultTo(false); + t.string("firstName"); + t.string("lastName"); + t.boolean("isAccepted").defaultTo(false); + t.boolean("isMfaEnabled").defaultTo(false); + t.specificType("mfaMethods", "text[]"); + t.jsonb("devices"); + t.timestamps(true, true, true); + }); + } + // this is a one time function + await createUpdateAtTriggerFunction(knex); + await createOnUpdateTrigger(knex, TableName.Users); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.Users); + await dropOnUpdateTrigger(knex, TableName.Users); + await dropUpdatedAtTriggerFunction(knex); +} diff --git a/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts b/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts new file mode 100644 index 000000000..b9b312456 --- /dev/null +++ b/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.UserEncryptionKey); + if (!isTablePresent) { + await knex.schema.createTable(TableName.UserEncryptionKey, (t) => { + t.increments().primary(); + t.text("clientPublicKey"); + t.text("serverPrivateKey"); + t.text("encryptionVersion").defaultTo(1); + t.text("protectedKey").notNullable(); + t.text("protectedKeyIV").notNullable(); + t.text("protectedKeyTag").notNullable(); + t.text("publicKey").notNullable(); + t.text("encryptedPrivateKey").notNullable(); + t.text("iv").notNullable(); + t.text("tag").notNullable(); + t.text("salt").notNullable(); + t.text("verifier").notNullable(); + // one to one relationship + t.uuid("userId").notNullable().unique(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.UserEncryptionKey); +} diff --git a/backend-pg/src/db/migrations/20231129072939_auth-token.ts b/backend-pg/src/db/migrations/20231129072939_auth-token.ts new file mode 100644 index 000000000..0b186b8b0 --- /dev/null +++ b/backend-pg/src/db/migrations/20231129072939_auth-token.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.AuthTokens); + if (!isTablePresent) { + await knex.schema.createTable(TableName.AuthTokens, (t) => { + t.string("type").notNullable(); + t.string("phoneNumber"); + t.string("tokenHash").notNullable(); + t.integer("triesLeft"); + t.datetime("expiresAt").notNullable(); + // does not need update trigger we will do it manually + t.timestamps(true, true, true); + t.uuid("userId"); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AuthTokens); +} diff --git a/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts b/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts new file mode 100644 index 000000000..4acde142f --- /dev/null +++ b/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.AuthTokenSession); + if (!isTablePresent) { + await knex.schema.createTable(TableName.AuthTokenSession, (t) => { + t.increments(); + t.string("ip").notNullable(); + t.string("userAgent"); + t.integer("refreshVersion").notNullable().defaultTo(1); + t.integer("accessVersion").notNullable().defaultTo(1); + t.datetime("lastUsed").notNullable(); + // does not need update trigger we will do it manually + t.timestamps(true, true, true); + t.uuid("userId").notNullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + } + // this is a one time function + await createOnUpdateTrigger(knex, TableName.AuthTokenSession); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AuthTokenSession); + await dropOnUpdateTrigger(knex, TableName.AuthTokenSession); +} diff --git a/backend-pg/src/db/migrations/20231201151432_backup-key.ts b/backend-pg/src/db/migrations/20231201151432_backup-key.ts new file mode 100644 index 000000000..2f7ff9665 --- /dev/null +++ b/backend-pg/src/db/migrations/20231201151432_backup-key.ts @@ -0,0 +1,26 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesTableExist = await knex.schema.hasTable(TableName.BackupPrivateKey); + if (!doesTableExist) { + await knex.schema.createTable(TableName.BackupPrivateKey, (t) => { + t.increments(); + t.string("encryptedPrivateKey").notNullable(); + t.string("iv").notNullable(); + t.string("tag").notNullable(); + t.string("algorithm").notNullable(); + t.string("keyEncoding").notNullable(); + t.string("salt").notNullable(); + t.string("verifier").notNullable(); + t.timestamps(true, true, true); + t.uuid("userId").notNullable().unique(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.BackupPrivateKey); +} diff --git a/backend-pg/src/db/schemas/backup-private-key.ts b/backend-pg/src/db/schemas/backup-private-key.ts new file mode 100644 index 000000000..73bcc59a1 --- /dev/null +++ b/backend-pg/src/db/schemas/backup-private-key.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +import { SecretEncryptionAlgo, SecretKeyEncoding, TImmutableDBKeys } from "./models"; + +export const BackupPrivateKeySchema = z.object({ + id: z.string(), + userId: z.string(), + encryptedPrivateKey: z.string(), + iv: z.string(), + tag: z.string(), + algorithm: z.nativeEnum(SecretEncryptionAlgo), + keyEncoding: z.nativeEnum(SecretKeyEncoding), + salt: z.string(), + verifier: z.string(), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime() +}); + +export type TBackupPrivateKey = z.infer; +export type TBackupPrivateKeyInsert = Omit; +export type TBackupPrivateKeyUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/index.ts b/backend-pg/src/db/schemas/index.ts new file mode 100644 index 000000000..33c7fea0f --- /dev/null +++ b/backend-pg/src/db/schemas/index.ts @@ -0,0 +1,15 @@ +export { + BackupPrivateKeySchema, + TBackupPrivateKey, + TBackupPrivateKeyInsert, + TBackupPrivateKeyUpdate +} from "./backup-private-key"; +export { TableName } from "./models"; +export { TokenSchema, TToken, TTokenInsert, TTokenUpdate } from "./token"; +export { AuthMethod, TUser, TUserInsert, TUserUpdate, UserSchema } from "./user"; +export { + TUserEncryptionKey, + TUserEncryptionKeyInsert, + TUserEncryptionKeyUpdate, + UserEncryptionKey +} from "./user-encryption-key"; diff --git a/backend-pg/src/db/schemas/models.ts b/backend-pg/src/db/schemas/models.ts new file mode 100644 index 000000000..8c9cc0daa --- /dev/null +++ b/backend-pg/src/db/schemas/models.ts @@ -0,0 +1,19 @@ +export enum TableName { + Users = "users", + UserEncryptionKey = "user_encryption_keys", + AuthTokens = "auth_tokens", + AuthTokenSession = "auth_token_sessions", + BackupPrivateKey = "backup_private_key" +} + +export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; + +export enum SecretEncryptionAlgo { + AES_256_GCM = "aes-256-gcm" +} + +export enum SecretKeyEncoding { + UTF8 = "utf8", + BASE64 = "base64", + HEX = "hex" +} diff --git a/backend-pg/src/db/schemas/token-session.ts b/backend-pg/src/db/schemas/token-session.ts new file mode 100644 index 000000000..196871aa0 --- /dev/null +++ b/backend-pg/src/db/schemas/token-session.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const TokenSessionSchema = z.object({ + id: z.string(), + userId: z.string(), + ip: z.string(), + userAgent: z.string(), + refreshVersion: z.number().default(1), + accessVersion: z.number().default(1), + lastUsed: z.string().datetime() +}); + +export type TTokenSession = z.infer; +export type TTokenSessionInsert = Omit; +export type TTokenSessionUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/token.ts b/backend-pg/src/db/schemas/token.ts new file mode 100644 index 000000000..401d5db87 --- /dev/null +++ b/backend-pg/src/db/schemas/token.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const TokenSchema = z.object({ + id: z.number(), + type: z.enum(["emailConfirmation", "emailMfa", "organizationInvitation", "passwordReset"]), + phoneNumber: z.string().optional(), + tokenHash: z.string(), + triesLeft: z.number().optional(), + expiresAt: z.string().datetime().optional(), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), + userId: z.string().optional() +}); + +export type TToken = z.infer; +export type TTokenInsert = Omit; +export type TTokenUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/user-encryption-key.ts b/backend-pg/src/db/schemas/user-encryption-key.ts new file mode 100644 index 000000000..103ee5bd2 --- /dev/null +++ b/backend-pg/src/db/schemas/user-encryption-key.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +import { PickRequired } from "@app/lib/types"; + +import { TImmutableDBKeys } from "./models"; + +export const UserEncryptionKey = z.object({ + id: z.number(), + userId: z.string(), + serverPrivateKey: z.string().optional().nullable(), + clientPublicKey: z.string().optional().nullable(), + encryptionVersion: z.number().default(1).optional(), + protectedKey: z.string(), + protectedKeyIV: z.string(), + protectedKeyTag: z.string(), + publicKey: z.string(), + encryptedPrivateKey: z.string(), + iv: z.string(), + tag: z.string(), + salt: z.string(), + verifier: z.string() +}); + +export type TUserEncryptionKey = z.infer; +export type TUserEncryptionKeyInsert = Omit, TImmutableDBKeys>; +export type TUserEncryptionKeyUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/user.ts b/backend-pg/src/db/schemas/user.ts new file mode 100644 index 000000000..59428f08d --- /dev/null +++ b/backend-pg/src/db/schemas/user.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export enum AuthMethod { + EMAIL = "email", + GOOGLE = "google", + GITHUB = "github", + GITLAB = "gitlab", + OKTA_SAML = "okta-saml", + AZURE_SAML = "azure-saml", + JUMPCLOUD_SAML = "jumpcloud-saml" +} + +export const UserSchema = z.object({ + id: z.string(), + authMethods: z.nativeEnum(AuthMethod).array().default([AuthMethod.EMAIL]).optional().nullable(), + email: z.string(), + isSuperAdmin: z.boolean().default(false).optional(), + firstName: z.string().optional().nullable(), + lastName: z.string().optional().nullable(), + isMfaEnabled: z.boolean().default(false).optional(), + mfaMethods: z.string().array().default([]).optional().nullable(), + isAccepted: z.boolean().default(false).optional(), + devices: z.string().nullable().optional() +}); + +export const UserDeviceSchema = z + .object({ + ip: z.string(), + userAgent: z.string() + }) + .array() + .default([]); + +export type TUser = z.infer; +export type TUserInsert = Omit; +export type TUserUpdate = Partial>; diff --git a/backend-pg/src/db/utils.ts b/backend-pg/src/db/utils.ts new file mode 100644 index 000000000..9c537f5b5 --- /dev/null +++ b/backend-pg/src/db/utils.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +export const createJunctionTable = ( + knex: Knex, + tableName: string, + table1Name: string, + table2Name: string +) => + knex.schema.createTable(tableName, (table) => { + table.increments(); // Primary key + table.integer(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1 + table.integer(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2 + table.foreign(`${table1Name}Id`).references("id").inTable(table2Name); + table.foreign(`${table2Name}Id`).references("id").inTable(table1Name); + }); + +// one time logic +// this is a postgres function log to set updateAt to present time whenever row gets updated +export const createUpdateAtTriggerFunction = (knex: Knex) => + knex.raw(` +CREATE OR REPLACE FUNCTION on_update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW."updatedAt" = NOW(); +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +`); + +export const dropUpdatedAtTriggerFunction = (knex: Knex) => + knex.raw(` +DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE; +`); + +// we would be using this to apply updatedAt where ever we wanta +// remember to set `timestamps(true,true,true)` before this on schema +export const createOnUpdateTrigger = (knex: Knex, tableName: string) => + knex.raw(` +CREATE TRIGGER ${tableName}_updatedAt +BEFORE UPDATE ON ${tableName} +FOR EACH ROW +EXECUTE PROCEDURE on_update_timestamp(); +`); + +export const dropOnUpdateTrigger = (knex: Knex, tableName: string) => + knex.raw(`DROP TRIGGER IF EXISTS ${tableName}_updatedAt ON ${tableName}`); diff --git a/backend-pg/src/lib/config/env.ts b/backend-pg/src/lib/config/env.ts index 5f7217e03..ed03e6850 100644 --- a/backend-pg/src/lib/config/env.ts +++ b/backend-pg/src/lib/config/env.ts @@ -1,16 +1,39 @@ import { Logger } from "pino"; import { z } from "zod"; +import { zpStr } from "../zod"; + +const zodStrBool = z + .enum(["true", "false"]) + .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: z.string().optional(), - ROOT_ENCRYPTION_KEY: z.string().optional(), - HTTPS_ENABLED: z - .enum(["true", "false"]) - .optional() - .transform((val) => val === "true") + 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")) }); let envCfg: Readonly>; @@ -21,8 +44,20 @@ export const initEnvConfig = (logger: Logger) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { logger.error("Invalid environment variables. Check the error below"); - logger.error(parsedEnv.error); + logger.error(parsedEnv.error.issues); process.exit(-1); } envCfg = Object.freeze(parsedEnv.data); + return envCfg; }; + +export const formatSmtpConfig = () => ({ + host: envCfg.SMTP_HOST, + port: envCfg.SMTP_PORT, + auth: + envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD + ? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD } + : undefined, + secure: envCfg.SMTP_SECURE, + from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>` +}); diff --git a/backend-pg/src/lib/crypto/index.ts b/backend-pg/src/lib/crypto/index.ts new file mode 100644 index 000000000..ae317cfa6 --- /dev/null +++ b/backend-pg/src/lib/crypto/index.ts @@ -0,0 +1 @@ +export { generateSrpServerKey, srpCheckClientProof } from "./srp"; diff --git a/backend-pg/src/lib/crypto/srp.ts b/backend-pg/src/lib/crypto/srp.ts new file mode 100644 index 000000000..b05f3734d --- /dev/null +++ b/backend-pg/src/lib/crypto/srp.ts @@ -0,0 +1,26 @@ +import jsrp from "jsrp"; + +export const generateSrpServerKey = async (salt: string, verifier: string) => { + // eslint-disable-next-line new-cap + const server = new jsrp.server(); + await new Promise((resolve) => { + server.init({ salt, verifier }, () => resolve(null)); + }); + return { pubKey: server.getPublicKey(), privateKey: server.getPrivateKey() }; +}; + +export const srpCheckClientProof = async ( + salt: string, + verifier: string, + serverPrivateKey: string, + clientPublicKey: string, + clientProof: string +) => { + // eslint-disable-next-line new-cap + const server = new jsrp.server(); + await new Promise((resolve) => { + server.init({ salt, verifier, b: serverPrivateKey }, () => resolve(null)); + }); + server.setClientPublicKey(clientPublicKey); + return server.checkClientProof(clientProof); +}; diff --git a/backend-pg/src/lib/errors/index.ts b/backend-pg/src/lib/errors/index.ts new file mode 100644 index 000000000..ab090fa46 --- /dev/null +++ b/backend-pg/src/lib/errors/index.ts @@ -0,0 +1,36 @@ +/* eslint-disable max-classes-per-file */ +export class DatabaseError extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message?: string; name: string; error: unknown }) { + super(message || "Failed to execute db ops"); + this.name = name; + this.error = error; + } +} + +export class UnauthorizedError extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message?: string; name: string; error: unknown }) { + super(message ?? "You are not allowed to access this resourve"); + this.name = name; + this.error = error; + } +} + +export class BadRequestError extends Error { + 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; + } +} diff --git a/backend-pg/src/lib/types/index.ts b/backend-pg/src/lib/types/index.ts new file mode 100644 index 000000000..c70fe1fd7 --- /dev/null +++ b/backend-pg/src/lib/types/index.ts @@ -0,0 +1,5 @@ +export type RequiredKeys = { + [K in keyof T]-?: undefined extends T[K] ? never : K; +}[keyof T]; + +export type PickRequired = Pick>; diff --git a/backend-pg/src/lib/validator/disposable_emails.txt b/backend-pg/src/lib/validator/disposable_emails.txt new file mode 100644 index 000000000..c71323b3a --- /dev/null +++ b/backend-pg/src/lib/validator/disposable_emails.txt @@ -0,0 +1,3519 @@ +0-mail.com +027168.com +0815.ru +0815.ry +0815.su +0845.ru +0box.eu +0clickemail.com +0n0ff.net +0nelce.com +0v.ro +0w.ro +0wnd.net +0wnd.org +0x207.info +1-8.biz +1-tm.com +10-minute-mail.com +1000rebates.stream +100likers.com +105kg.ru +10dk.email +10mail.com +10mail.org +10minut.com.pl +10minut.xyz +10minutemail.be +10minutemail.cf +10minutemail.co.uk +10minutemail.co.za +10minutemail.com +10minutemail.de +10minutemail.ga +10minutemail.gq +10minutemail.ml +10minutemail.net +10minutemail.nl +10minutemail.pro +10minutemail.us +10minutemailbox.com +10minutemails.in +10minutenemail.de +10minutesmail.com +10minutesmail.fr +10minutmail.pl +10x9.com +11163.com +123-m.com +12hosting.net +12houremail.com +12minutemail.com +12minutemail.net +12storage.com +140unichars.com +147.cl +14n.co.uk +15qm.com +1blackmoon.com +1ce.us +1chuan.com +1clck2.com +1fsdfdsfsdf.tk +1mail.ml +1pad.de +1s.fr +1secmail.com +1secmail.net +1secmail.org +1st-forms.com +1to1mail.org +1usemail.com +1webmail.info +1zhuan.com +2012-2016.ru +20email.eu +20email.it +20mail.eu +20mail.in +20mail.it +20minutemail.com +20minutemail.it +20mm.eu +2120001.net +21cn.com +247web.net +24hinbox.com +24hourmail.com +24hourmail.net +2anom.com +2chmail.net +2ether.net +2fdgdfgdfgdf.tk +2odem.com +2prong.com +2wc.info +300book.info +30mail.ir +30minutemail.com +30wave.com +3202.com +36ru.com +3d-painting.com +3l6.com +3mail.ga +3trtretgfrfe.tk +4-n.us +4057.com +418.dk +42o.org +4gfdsgfdgfd.tk +4k5.net +4mail.cf +4mail.ga +4nextmail.com +4nmv.ru +4tb.host +4warding.com +4warding.net +4warding.org +50set.ru +55hosting.net +5ghgfhfghfgh.tk +5gramos.com +5july.org +5mail.cf +5mail.ga +5minutemail.net +5oz.ru +5tb.in +5x25.com +5ymail.com +60minutemail.com +672643.net +675hosting.com +675hosting.net +675hosting.org +6hjgjhgkilkj.tk +6ip.us +6mail.cf +6mail.ga +6mail.ml +6paq.com +6somok.ru +6url.com +75hosting.com +75hosting.net +75hosting.org +7days-printing.com +7mail.ga +7mail.ml +7tags.com +80665.com +8127ep.com +8mail.cf +8mail.ga +8mail.ml +99.com +99cows.com +99experts.com +9mail.cf +9me.site +9mot.ru +9ox.net +9q.ro +a-bc.net +a45.in +a7996.com +aa5zy64.com +abacuswe.us +abakiss.com +abcmail.email +abilitywe.us +abovewe.us +absolutewe.us +abundantwe.us +abusemail.de +abuser.eu +abyssmail.com +ac20mail.in +academiccommunity.com +academywe.us +acceleratewe.us +accentwe.us +acceptwe.us +acclaimwe.us +accordwe.us +accreditedwe.us +acentri.com +achievementwe.us +achievewe.us +acornwe.us +acrossgracealley.com +acrylicwe.us +activatewe.us +activitywe.us +acucre.com +acuitywe.us +acumenwe.us +adaptivewe.us +adaptwe.us +add3000.pp.ua +addictingtrailers.com +adeptwe.us +adfskj.com +adios.email +adiq.eu +aditus.info +admiralwe.us +ado888.biz +adobeccepdm.com +adoniswe.us +adpugh.org +adroh.com +adsd.org +adubiz.info +advantagewe.us +advantimo.com +adventurewe.us +adventwe.us +advisorwe.us +advocatewe.us +adwaterandstir.com +aegde.com +aegia.net +aegiscorp.net +aegiswe.us +aelo.es +aeonpsi.com +afarek.com +affiliate-nebenjob.info +affiliatedwe.us +affilikingz.de +affinitywe.us +affluentwe.us +affordablewe.us +afia.pro +afrobacon.com +afterhourswe.us +agedmail.com +agendawe.us +agger.ro +agilewe.us +agorawe.us +agtx.net +aheadwe.us +ahem.email +ahk.jp +ahmedkhlef.com +air2token.com +airmailbox.website +airsi.de +ajaxapp.net +akapost.com +akerd.com +akgq701.com +akmail.in +al-qaeda.us +albionwe.us +alchemywe.us +alfaceti.com +aliaswe.us +alienware13.com +aligamel.com +alisongamel.com +alivance.com +alivewe.us +all-cats.ru +allaccesswe.us +allamericanwe.us +allaroundwe.us +alldirectbuy.com +allegiancewe.us +allegrowe.us +allemojikeyboard.com +allgoodwe.us +alliancewe.us +allinonewe.us +allofthem.net +alloutwe.us +allowed.org +alloywe.us +allprowe.us +allseasonswe.us +allstarwe.us +allthegoodnamesaretaken.org +allurewe.us +almondwe.us +alph.wtf +alpha-web.net +alphaomegawe.us +alpinewe.us +altairwe.us +altitudewe.us +altuswe.us +ama-trade.de +ama-trans.de +amadeuswe.us +amail.club +amail.com +amail1.com +amail4.me +amazon-aws.org +amberwe.us +ambiancewe.us +ambitiouswe.us +amelabs.com +americanawe.us +americasbestwe.us +americaswe.us +amicuswe.us +amilegit.com +amiri.net +amiriindustries.com +amplewe.us +amplifiedwe.us +amplifywe.us +ampsylike.com +analogwe.us +analysiswe.us +analyticalwe.us +analyticswe.us +analyticwe.us +anappfor.com +anappthat.com +andreihusanu.ro +andthen.us +animesos.com +anit.ro +ano-mail.net +anon-mail.de +anonbox.net +anonmail.top +anonmails.de +anonymail.dk +anonymbox.com +anonymized.org +anonymousness.com +anotherdomaincyka.tk +ansibleemail.com +anthony-junkmail.com +antireg.com +antireg.ru +antispam.de +antispam24.de +antispammail.de +anyalias.com +aoeuhtns.com +apfelkorps.de +aphlog.com +apkmd.com +appc.se +appinventor.nl +appixie.com +apps.dj +appzily.com +arduino.hk +ariaz.jetzt +armyspy.com +aron.us +arroisijewellery.com +art-en-ligne.pro +artman-conception.com +arur01.tk +arurgitu.gq +arvato-community.de +aschenbrandt.net +asdasd.nl +asdasd.ru +ashleyandrew.com +ask-mail.com +asorent.com +ass.pp.ua +astonut.tk +astroempires.info +asu.mx +asu.su +at.hm +at0mik.org +atnextmail.com +attnetwork.com +augmentationtechnology.com +ausgefallen.info +auti.st +autorobotica.com +autosouvenir39.ru +autotwollow.com +autowb.com +aver.com +averdov.com +avia-tonic.fr +avls.pt +awatum.de +awdrt.org +awiki.org +awsoo.com +axiz.org +axon7zte.com +axsup.net +ayakamail.cf +azazazatashkent.tk +azcomputerworks.com +azmeil.tk +b1of96u.com +b2bx.net +b2cmail.de +badgerland.eu +badoop.com +badpotato.tk +balaket.com +banit.club +banit.me +bank-opros1.ru +bareed.ws +barryogorman.com +bartdevos.be +basscode.org +bauwerke-online.com +bazaaboom.com +bbbbyyzz.info +bbhost.us +bbitf.com +bbitj.com +bbitq.com +bcaoo.com +bcast.ws +bcb.ro +bccto.me +bdmuzic.pw +beaconmessenger.com +bearsarefuzzy.com +beddly.com +beefmilk.com +belamail.org +belljonestax.com +beluckygame.com +benipaula.org +bepureme.com +beribase.ru +beribaza.ru +berirabotay.ru +best-john-boats.com +bestchoiceusedcar.com +bestlistbase.com +bestoption25.club +bestparadize.com +bestsoundeffects.com +besttempmail.com +betr.co +bgtmail.com +bgx.ro +bheps.com +bidourlnks.com +big1.us +bigprofessor.so +bigstring.com +bigwhoop.co.za +bij.pl +binka.me +binkmail.com +binnary.com +bio-muesli.info +bio-muesli.net +bione.co +bitwhites.top +bitymails.us +blackgoldagency.ru +blackmarket.to +bladesmail.net +blip.ch +blnkt.net +block521.com +blogmyway.org +blogos.net +blogspam.ro +blondemorkin.com +bluedumpling.info +bluewerks.com +bnote.com +boatmail.us +bobmail.info +bobmurchison.com +bofthew.com +bonobo.email +boofx.com +bookthemmore.com +bootybay.de +borged.com +borged.net +borged.org +bot.nu +boun.cr +bouncr.com +boxformail.in +boximail.com +boxmail.lol +boxomail.live +boxtemp.com.br +bptfp.net +brand-app.biz +brandallday.net +brasx.org +breakthru.com +brefmail.com +brennendesreich.de +briggsmarcus.com +broadbandninja.com +bsnow.net +bspamfree.org +bspooky.com +bst-72.com +btb-notes.com +btc.email +btcmail.pw +btcmod.com +btizet.pl +buccalmassage.ru +budaya-tionghoa.com +budayationghoa.com +buffemail.com +bugfoo.com +bugmenever.com +bugmenot.com +bukhariansiddur.com +bulrushpress.com +bum.net +bumpymail.com +bunchofidiots.com +bund.us +bundes-li.ga +bunsenhoneydew.com +burnthespam.info +burstmail.info +businessbackend.com +businesssuccessislifesuccess.com +buspad.org +bussitussi.com +buymoreplays.com +buyordie.info +buyusdomain.com +buyusedlibrarybooks.org +buzzcluby.com +byebyemail.com +byespm.com +byom.de +c51vsgq.com +cachedot.net +californiafitnessdeals.com +cam4you.cc +camping-grill.info +candymail.de +cane.pw +capitalistdilemma.com +car101.pro +carbtc.net +cars2.club +carsencyclopedia.com +cartelera.org +caseedu.tk +cashflow35.com +casualdx.com +cavi.mx +cbair.com +cbes.net +cc.liamria +ccmail.uk +cdfaq.com +cdpa.cc +ceed.se +cek.pm +cellurl.com +centermail.com +centermail.net +cetpass.com +cfo2go.ro +chacuo.net +chaichuang.com +chalupaurybnicku.cz +chammy.info +chasefreedomactivate.com +chatich.com +cheaphub.net +cheatmail.de +chenbot.email +chibakenma.ml +chickenkiller.com +chielo.com +childsavetrust.org +chilkat.com +chinamkm.com +chithinh.com +chitthi.in +choco.la +chogmail.com +choicemail1.com +chong-mail.com +chong-mail.net +chong-mail.org +chumpstakingdumps.com +cigar-auctions.com +civikli.com +civx.org +ckaazaza.tk +ckiso.com +cl-cl.org +cl0ne.net +claimab.com +clandest.in +classesmail.com +clearwatermail.info +click-email.com +clickdeal.co +clipmail.eu +clixser.com +clonemoi.tk +cloud-mail.top +cloudns.cx +clout.wiki +clrmail.com +cmail.club +cmail.com +cmail.net +cmail.org +cnamed.com +cndps.com +cnew.ir +cnmsg.net +cnsds.de +co.cc +cobarekyo1.ml +cocoro.uk +cocovpn.com +codeandscotch.com +codivide.com +coffeetimer24.com +coieo.com +coin-host.net +coinlink.club +coldemail.info +compareshippingrates.org +completegolfswing.com +comwest.de +conf.work +consumerriot.com +contbay.com +cooh-2.site +coolandwacky.us +coolimpool.org +coreclip.com +cosmorph.com +courrieltemporaire.com +coza.ro +crankhole.com +crapmail.org +crastination.de +crazespaces.pw +crazymailing.com +cream.pink +crepeau12.com +cringemonster.com +cross-law.ga +cross-law.gq +crossmailjet.com +crossroadsmail.com +crunchcompass.com +crusthost.com +cs.email +csh.ro +cszbl.com +ctmailing.us +ctos.ch +cu.cc +cubiclink.com +cuendita.com +cuirushi.org +cuoly.com +cupbest.com +curlhph.tk +curryworld.de +cust.in +cutout.club +cutradition.com +cuvox.de +cyber-innovation.club +cyber-phone.eu +cylab.org +d1yun.com +d3p.dk +daabox.com +dab.ro +dacoolest.com +daemsteam.com +daibond.info +daily-email.com +daintly.com +damai.webcam +dammexe.net +damnthespam.com +dandikmail.com +darkharvestfilms.com +daryxfox.net +dasdasdascyka.tk +dash-pads.com +dataarca.com +datarca.com +datazo.ca +datenschutz.ru +datum2.com +davidkoh.net +davidlcreative.com +dawin.com +daymail.life +daymailonline.com +dayrep.com +dbunker.com +dcctb.com +dcemail.com +ddcrew.com +de-a.org +dea-21olympic.com +deadaddress.com +deadchildren.org +deadfake.cf +deadfake.ga +deadfake.ml +deadfake.tk +deadspam.com +deagot.com +dealja.com +dealrek.com +deekayen.us +defomail.com +degradedfun.net +deinbox.com +delayload.com +delayload.net +delikkt.de +delivrmail.com +demen.ml +dengekibunko.ga +dengekibunko.gq +dengekibunko.ml +der-kombi.de +derkombi.de +derluxuswagen.de +desoz.com +despam.it +despammed.com +dev-null.cf +dev-null.ga +dev-null.gq +dev-null.ml +developermail.com +devnullmail.com +deyom.com +dharmatel.net +dhm.ro +dhy.cc +dialogus.com +diapaulpainting.com +dicopto.com +digdig.org +digital-message.com +digitalesbusiness.info +digitalmail.info +digitalmariachis.com +digitalsanctuary.com +dildosfromspace.com +dim-coin.com +dingbone.com +diolang.com +directmail24.net +disaq.com +disbox.net +disbox.org +discard.cf +discard.email +discard.ga +discard.gq +discard.ml +discard.tk +discardmail.com +discardmail.de +discos4.com +disign-concept.eu +disign-revelation.com +dispo.in +dispomail.eu +disposable-e.ml +disposable-email.ml +disposable.cf +disposable.ga +disposable.ml +disposable.site +disposableaddress.com +disposableemailaddresses.com +disposableinbox.com +disposablemails.com +dispose.it +disposeamail.com +disposemail.com +disposemymail.com +dispostable.com +divad.ga +divermail.com +divismail.ru +diwaq.com +dlemail.ru +dmarc.ro +dndent.com +dnses.ro +doanart.com +dob.jp +dodgeit.com +dodgemail.de +dodgit.com +dodgit.org +dodsi.com +doiea.com +dolphinnet.net +domforfb1.tk +domforfb18.tk +domforfb19.tk +domforfb2.tk +domforfb23.tk +domforfb27.tk +domforfb29.tk +domforfb3.tk +domforfb4.tk +domforfb5.tk +domforfb6.tk +domforfb7.tk +domforfb8.tk +domforfb9.tk +domozmail.com +donemail.ru +dongqing365.com +dontreg.com +dontsendmespam.de +doojazz.com +doquier.tk +dotman.de +dotmsg.com +dotslashrage.com +doublemail.de +douchelounge.com +dozvon-spb.ru +dp76.com +dr69.site +drdrb.com +drdrb.net +dred.ru +drevo.si +drivetagdev.com +drmail.in +droolingfanboy.de +dropcake.de +dropjar.com +droplar.com +dropmail.me +dropsin.net +dsgvo.ru +dsiay.com +dspwebservices.com +duam.net +duck2.club +dudmail.com +duk33.com +dukedish.com +dump-email.info +dumpandjunk.com +dumpmail.de +dumpyemail.com +durandinterstellar.com +duskmail.com +dwse.edu.pl +dyceroprojects.com +dz17.net +e-mail.com +e-mail.org +e-marketstore.ru +e-tomarigi.com +e3z.de +e4ward.com +eanok.com +easy-trash-mail.com +easynetwork.info +easytrashmail.com +eatmea2z.club +eay.jp +ebbob.com +ebeschlussbuch.de +ecallheandi.com +ecolo-online.fr +edgex.ru +edinburgh-airporthotels.com +edv.to +ee1.pl +ee2.pl +eeedv.de +eelmail.com +efxs.ca +egzones.com +einmalmail.de +einrot.com +einrot.de +eintagsmail.de +elearningjournal.org +electro.mn +elitevipatlantamodels.com +elki-mkzn.ru +email-fake.cf +email-fake.com +email-fake.ga +email-fake.gq +email-fake.ml +email-fake.tk +email-jetable.fr +email-lab.com +email-temp.com +email.edu.pl +email.net +email1.pro +email60.com +emailage.cf +emailage.ga +emailage.gq +emailage.ml +emailage.tk +emailate.com +emailcu.icu +emaildienst.de +emaildrop.io +emailfake.com +emailfake.ml +emailfreedom.ml +emailgenerator.de +emailgo.de +emailias.com +emailigo.de +emailinfive.com +emailisvalid.com +emaillime.com +emailmiser.com +emailna.co +emailnax.com +emailo.pro +emailondeck.com +emailportal.info +emailproxsy.com +emailresort.com +emails.ga +emailsecurer.com +emailsensei.com +emailsingularity.net +emailspam.cf +emailspam.ga +emailspam.gq +emailspam.ml +emailspam.tk +emailsy.info +emailtech.info +emailtemporanea.com +emailtemporanea.net +emailtemporar.ro +emailtemporario.com.br +emailthe.net +emailtmp.com +emailto.de +emailure.net +emailwarden.com +emailxfer.com +emailz.cf +emailz.ga +emailz.gq +emailz.ml +emeil.in +emeil.ir +emeraldwebmail.com +emil.com +emkei.cf +emkei.ga +emkei.gq +emkei.ml +emkei.tk +eml.pp.ua +emlhub.com +emlpro.com +emltmp.com +empireanime.ga +emstjzh.com +emz.net +enayu.com +enterto.com +envy17.com +eoffice.top +eoopy.com +epb.ro +ephemail.net +ephemeral.email +eposta.buzz +eposta.work +eqiluxspam.ga +ereplyzy.com +ericjohnson.ml +ero-tube.org +esadverse.com +esbano-ru.ru +esc.la +escapehatchapp.com +esemay.com +esgeneri.com +esiix.com +esprity.com +estate-invest.fr +eth2btc.info +ether123.net +ethereum1.top +ethersports.org +ethersportz.info +etotvibor.ru +etranquil.com +etranquil.net +etranquil.org +euaqa.com +evanfox.info +eveav.com +evilcomputer.com +evopo.com +evyush.com +exdonuts.com +existiert.net +exitstageleft.net +explodemail.com +express.net.ua +extracurricularsociety.com +extremail.ru +eyepaste.com +ez.lv +ezehe.com +ezfill.com +ezstest.com +f4k.es +f5.si +facebook-email.cf +facebook-email.ga +facebook-email.ml +facebookmail.gq +facebookmail.ml +fackme.gq +fadingemail.com +faecesmail.me +fag.wf +failbone.com +faithkills.com +fake-box.com +fake-email.pp.ua +fake-mail.cf +fake-mail.ga +fake-mail.ml +fakedemail.com +fakeinbox.cf +fakeinbox.com +fakeinbox.ga +fakeinbox.info +fakeinbox.ml +fakeinbox.tk +fakeinformation.com +fakemail.fr +fakemail.io +fakemailgenerator.com +fakemailz.com +fallinhay.com +fammix.com +fanclub.pm +fangoh.com +fansworldwide.de +fantasymail.de +farrse.co.uk +fast-email.info +fast-mail.fr +fastacura.com +fastchevy.com +fastchrysler.com +fasternet.biz +fastkawasaki.com +fastmazda.com +fastmitsubishi.com +fastnissan.com +fastsubaru.com +fastsuzuki.com +fasttoyota.com +fastyamaha.com +fatflap.com +fbma.tk +fddns.ml +fdfdsfds.com +femailtor.com +fer-gabon.org +fermaxxi.ru +fettometern.com +fexbox.org +fexbox.ru +fexpost.com +fextemp.com +ficken.de +fictionsite.com +fightallspam.com +figjs.com +figshot.com +figurescoin.com +fiifke.de +filbert4u.com +filberts4u.com +film-blog.biz +filzmail.com +findemail.info +findu.pl +finews.biz +fir.hk +firemailbox.club +fitnesrezink.ru +fivemail.de +fixmail.tk +fizmail.com +fleckens.hu +flemail.ru +flowu.com +flu.cc +fluidsoft.us +flurred.com +fly-ts.de +flyinggeek.net +flyspam.com +foobarbot.net +footard.com +foreastate.com +forecastertests.com +foreskin.cf +foreskin.ga +foreskin.gq +foreskin.ml +foreskin.tk +forgetmail.com +fornow.eu +forspam.net +forward.cat +fosil.pro +foxja.com +foxtrotter.info +fr.cr +fr.nf +fr33mail.info +fragolina2.tk +frapmail.com +frappina.tk +free-email.cf +free-email.ga +free-temp.net +freebabysittercam.com +freeblackbootytube.com +freecat.net +freedom4you.info +freedompop.us +freefattymovies.com +freehotmail.net +freeinbox.email +freelance-france.eu +freeletter.me +freemail.ms +freemails.cf +freemails.ga +freemails.ml +freemeil.ga +freemeil.gq +freemeil.ml +freeml.net +freeplumpervideos.com +freerubli.ru +freeschoolgirlvids.com +freesistercam.com +freeteenbums.com +freundin.ru +friendlymail.co.uk +front14.org +frwdmail.com +ftp.sh +ftpinc.ca +fuckedupload.com +fuckingduh.com +fuckme69.club +fucknloveme.top +fuckxxme.top +fudgerub.com +fuirio.com +fukaru.com +fukurou.ch +fullangle.org +fulvie.com +fun64.com +funnycodesnippets.com +funnymail.de +furzauflunge.de +futuramind.com +fuwamofu.com +fuwari.be +fux0ringduh.com +fxnxs.com +fyii.de +g14l71lb.com +g1xmail.top +g2xmail.top +g3xmail.top +g4hdrop.us +gafy.net +gage.ga +galaxy.tv +gally.jp +gamail.top +gamegregious.com +gamgling.com +garasikita.pw +garbagecollector.org +garbagemail.org +gardenscape.ca +garizo.com +garliclife.com +garrymccooey.com +gav0.com +gawab.com +gbcmail.win +gbmail.top +gcmail.top +gdmail.top +gedmail.win +geekforex.com +geew.ru +gehensiemirnichtaufdensack.de +geldwaschmaschine.de +gelitik.in +genderfuck.net +geronra.com +geschent.biz +get-mail.cf +get-mail.ga +get-mail.ml +get-mail.tk +get.pp.ua +get1mail.com +get2mail.fr +getairmail.cf +getairmail.com +getairmail.ga +getairmail.gq +getairmail.ml +getairmail.tk +geteit.com +getfun.men +getmails.eu +getnada.com +getnowtoday.cf +getonemail.com +getonemail.net +getover.de +getsimpleemail.com +gett.icu +gexik.com +ggmal.ml +ghosttexter.de +giacmosuaviet.info +giaiphapmuasam.com +giantmail.de +gifto12.com +ginzi.be +ginzi.co.uk +ginzi.es +ginzi.net +ginzy.co.uk +ginzy.eu +girlmail.win +girlsindetention.com +girlsundertheinfluence.com +gishpuppy.com +giveh2o.info +givememail.club +givmail.com +glitch.sx +globaltouron.com +glubex.com +glucosegrin.com +gmal.com +gmatch.org +gmial.com +gmx1mail.top +gmxmail.top +gmxmail.win +gnctr-calgary.com +go2usa.info +go2vpn.net +goemailgo.com +golemico.com +gomail.in +goonby.com +goplaygame.ru +gorillaswithdirtyarmpits.com +goround.info +gosuslugi-spravka.ru +gothere.biz +gotmail.com +gotmail.net +gotmail.org +gowikibooks.com +gowikicampus.com +gowikicars.com +gowikifilms.com +gowikigames.com +gowikimusic.com +gowikinetwork.com +gowikitravel.com +gowikitv.com +grandmamail.com +grandmasmail.com +great-host.in +greencafe24.com +greendike.com +greenhousemail.com +greensloth.com +greggamel.com +greggamel.net +gregorsky.zone +gregorygamel.com +gregorygamel.net +grish.de +griuc.schule +grn.cc +groupbuff.com +grr.la +grugrug.ru +gruz-m.ru +gs-arc.org +gsredcross.org +gsrv.co.uk +gsxstring.ga +gudanglowongan.com +guerillamail.biz +guerillamail.com +guerillamail.de +guerillamail.info +guerillamail.net +guerillamail.org +guerillamailblock.com +guerrillamail.biz +guerrillamail.com +guerrillamail.de +guerrillamail.info +guerrillamail.net +guerrillamail.org +guerrillamailblock.com +gufum.com +gustr.com +gxemail.men +gynzi.co.uk +gynzi.es +gynzy.at +gynzy.es +gynzy.eu +gynzy.gr +gynzy.info +gynzy.lt +gynzy.mobi +gynzy.pl +gynzy.ro +gynzy.sk +gzb.ro +h8s.org +habitue.net +hacccc.com +hackersquad.tk +hackthatbit.ch +hahawrong.com +haida-edu.cn +hairs24.ru +haltospam.com +hamham.uk +hangxomcuatoilatotoro.ml +happydomik.ru +harakirimail.com +haribu.com +hartbot.de +hasanmail.ml +hat-geld.de +hatespam.org +hawrong.com +haydoo.com +hazelnut4u.com +hazelnuts4u.com +hazmatshipping.org +hccmail.win +headstrong.de +heathenhammer.com +heathenhero.com +hecat.es +heisei.be +hellodream.mobi +helloricky.com +helpinghandtaxcenter.org +helpjobs.ru +heros3.com +herp.in +herpderp.nl +hezll.com +hi5.si +hiddentragedy.com +hidebox.org +hidebusiness.xyz +hidemail.de +hidemail.pro +hidemail.us +hidzz.com +highbros.org +hiltonvr.com +himail.online +hmail.us +hmamail.com +hmh.ro +hoanggiaanh.com +hoanglong.tech +hochsitze.com +hola.org +holl.ga +honeys.be +honor-8.com +hopemail.biz +hornyalwary.top +host1s.com +hostcalls.com +hostguru.top +hostingmail.me +hostlaba.com +hot-mail.cf +hot-mail.ga +hot-mail.gq +hot-mail.ml +hot-mail.tk +hotmai.com +hotmailproduct.com +hotmial.com +hotpop.com +hotprice.co +hotsoup.be +housat.com +hpc.tw +hs.vc +ht.cx +huangniu8.com +hukkmu.tk +hulapla.de +humaility.com +hungpackage.com +hushmail.cf +huskion.net +hvastudiesucces.nl +hwsye.net +i2pmail.org +i6.cloudns.cc +iaoss.com +ibnuh.bz +icantbelieveineedtoexplainthisshit.com +icemail.club +ichigo.me +icx.in +icx.ro +idx4.com +idxue.com +ieatspam.eu +ieatspam.info +ieh-mail.de +iencm.com +iffymedia.com +ige.es +igg.biz +ignoremail.com +ihateyoualot.info +ihazspam.ca +iheartspam.org +ikbenspamvrij.nl +illistnoise.com +ilovespam.com +imail1.net +imails.info +imailt.com +imgof.com +imgv.de +immo-gerance.info +imstations.com +imul.info +in-ulm.de +in2reach.com +inactivemachine.com +inbax.tk +inbound.plus +inbox.si +inbox2.info +inboxalias.com +inboxbear.com +inboxclean.com +inboxclean.org +inboxdesign.me +inboxed.im +inboxed.pw +inboxkitten.com +inboxproxy.com +inboxstore.me +inclusiveprogress.com +incognitomail.com +incognitomail.net +incognitomail.org +incq.com +ind.st +indieclad.com +indirect.ws +indomaed.pw +indomina.cf +indoserver.stream +indosukses.press +ineec.net +infocom.zp.ua +inggo.org +inkomail.com +inmynetwork.tk +inoutmail.de +inoutmail.eu +inoutmail.info +inoutmail.net +inpwa.com +insanumingeniumhomebrew.com +insorg-mail.info +instaddr.ch +instance-email.com +instant-mail.de +instantblingmail.info +instantemailaddress.com +instantmail.fr +internet-v-stavropole.ru +internetoftags.com +interstats.org +intersteller.com +intopwa.com +intopwa.net +intopwa.org +investore.co +iozak.com +ip4.pp.ua +ip6.li +ip6.pp.ua +ipoo.org +ippandansei.tk +ipsur.org +irabops.com +irc.so +irish2me.com +irishspringrealty.com +iroid.com +ironiebehindert.de +irssi.tv +is.af +isdaq.com +ishop2k.com +isosq.com +istii.ro +isukrainestillacountry.com +it7.ovh +italy-mail.com +itcompu.com +itfast.net +itunesgiftcodegenerator.com +iubridge.com +iuemail.men +iwi.net +ixaks.com +ixx.io +j-p.us +jafps.com +jajxz.com +janproz.com +jaqis.com +jdmadventures.com +jdz.ro +je-recycle.info +jellow.ml +jellyrolls.com +jeoce.com +jet-renovation.fr +jetable.com +jetable.net +jetable.org +jetable.pp.ua +jiooq.com +jmail.ovh +jmail.ro +jnxjn.com +jobbikszimpatizans.hu +jobbrett.com +jobposts.net +jobs-to-be-done.net +joelpet.com +joetestalot.com +jopho.com +joseihorumon.info +josse.ltd +jourrapide.com +jpco.org +jsrsolutions.com +jumonji.tk +jungkamushukum.com +junk.to +junk1e.com +junkmail.ga +junkmail.gq +just-email.com +justemail.ml +juyouxi.com +jwork.ru +kademen.com +kadokawa.cf +kadokawa.ga +kadokawa.gq +kadokawa.ml +kadokawa.tk +kaengu.ru +kagi.be +kakadua.net +kalapi.org +kamen-market.ru +kamsg.com +kaovo.com +kappala.info +kara-turk.net +karatraman.ml +kariplan.com +karta-kykyruza.ru +kartvelo.com +kasmail.com +kaspop.com +katztube.com +kazelink.ml +kbox.li +kcrw.de +keepmymail.com +keinhirn.de +keipino.de +kekita.com +kellychibale-researchgroup-uct.com +kemptvillebaseball.com +kennedy808.com +kiani.com +killmail.com +killmail.net +kimsdisk.com +kingsq.ga +kino-100.ru +kiois.com +kismail.ru +kisstwink.com +kitnastar.com +kjkszpjcompany.com +kkmail.be +kksm.be +klassmaster.com +klassmaster.net +klick-tipp.us +klipschx12.com +kloap.com +kludgemush.com +klzlk.com +kmail.li +kmhow.com +knol-power.nl +kobrandly.com +kommunity.biz +kon42.com +konultant-jurist.ru +kook.ml +kopagas.com +kopaka.net +korona-nedvizhimosti.ru +koshu.ru +kosmetik-obatkuat.com +kostenlosemailadresse.de +koszmail.pl +kpay.be +kpooa.com +kpost.be +krd.ag +krsw.tk +kruay.com +krypton.tk +ksmtrck.tk +kuhrap.com +kulmeo.com +kulturbetrieb.info +kurzepost.de +kutakbisajauhjauh.gq +kvhrr.com +kvhrs.com +kvhrw.com +kwift.net +kwilco.net +kyal.pl +kyois.com +kzccv.com +l-c-a.us +l33r.eu +l6factors.com +labetteraverouge.at +labworld.org +lacedmail.com +lackmail.net +lackmail.ru +lacto.info +lags.us +lain.ch +lak.pp.ua +lakelivingstonrealestate.com +lakqs.com +lamasticots.com +landmail.co +laoeq.com +larisia.com +larland.com +last-chance.pro +lastmail.co +lastmail.com +lawlita.com +lazyinbox.com +lazyinbox.us +ldaho.biz +ldop.com +ldtp.com +le-tim.ru +lee.mx +leeching.net +leetmail.co +legalrc.loan +lellno.gq +lenovog4.com +lerbhe.com +letmeinonthis.com +letthemeatspam.com +lez.se +lgxscreen.com +lhsdv.com +liamcyrus.com +lifebyfood.com +lifetimefriends.info +lifetotech.com +ligsb.com +lillemap.net +lilo.me +lindenbaumjapan.com +link2mail.net +linkedintuts2016.pw +linshiyouxiang.net +linuxmail.so +litedrop.com +liveradio.tk +lkgn.se +llogin.ru +loadby.us +loan101.pro +loaoa.com +loapq.com +locanto1.club +locantofuck.top +locantowsite.club +locomodev.net +login-email.cf +login-email.ga +login-email.ml +login-email.tk +logular.com +loh.pp.ua +loin.in +lolfreak.net +lolmail.biz +lookugly.com +lordsofts.com +lortemail.dk +losemymail.com +lovemeet.faith +lovemeleaveme.com +lpfmgmtltd.com +lr7.us +lr78.com +lroid.com +lru.me +ls-server.ru +lsyx24.com +luckymail.org +lukecarriere.com +lukemail.info +lukop.dk +luv2.us +lyfestylecreditsolutions.com +lyft.live +lyricspad.net +lzoaq.com +m21.cc +m4ilweb.info +maboard.com +mac-24.com +macr2.com +macromaid.com +macromice.info +magamail.com +maggotymeat.ga +magicbox.ro +magim.be +magspam.net +maidlow.info +mail-card.net +mail-easy.fr +mail-filter.com +mail-help.net +mail-hosting.co +mail-hub.info +mail-now.top +mail-owl.com +mail-share.com +mail-temporaire.com +mail-temporaire.fr +mail-tester.com +mail.by +mail.wtf +mail0.ga +mail1.top +mail114.net +mail1a.de +mail1web.org +mail21.cc +mail22.club +mail2rss.org +mail333.com +mail4trash.com +mail666.ru +mail7.io +mail707.com +mail72.com +mailapp.top +mailback.com +mailbidon.com +mailbiz.biz +mailblocks.com +mailbox.in.ua +mailbox52.ga +mailbox80.biz +mailbox82.biz +mailbox87.de +mailbox92.biz +mailboxy.fun +mailbucket.org +mailcat.biz +mailcatch.com +mailchop.com +mailcker.com +maildax.me +mailde.de +mailde.info +maildrop.cc +maildrop.cf +maildrop.ga +maildrop.gq +maildrop.ml +maildu.de +maildx.com +maileater.com +mailed.in +mailed.ro +maileimer.de +maileme101.com +mailexpire.com +mailf5.com +mailfa.tk +mailfall.com +mailfirst.icu +mailforspam.com +mailfree.ga +mailfree.gq +mailfree.ml +mailfreeonline.com +mailfs.com +mailguard.me +mailgutter.com +mailhazard.com +mailhazard.us +mailhex.com +mailhub.pro +mailhz.me +mailimate.com +mailin8r.com +mailinatar.com +mailinater.com +mailinator.co.uk +mailinator.com +mailinator.gq +mailinator.info +mailinator.net +mailinator.org +mailinator.us +mailinator0.com +mailinator1.com +mailinator2.com +mailinator2.net +mailinator3.com +mailinator4.com +mailinator5.com +mailinator6.com +mailinator7.com +mailinator8.com +mailinator9.com +mailincubator.com +mailismagic.com +mailita.tk +mailjunk.cf +mailjunk.ga +mailjunk.gq +mailjunk.ml +mailjunk.tk +mailmate.com +mailme.gq +mailme.ir +mailme.lv +mailme24.com +mailmetrash.com +mailmoat.com +mailmoth.com +mailms.com +mailna.biz +mailna.co +mailna.in +mailna.me +mailnator.com +mailnesia.com +mailnull.com +mailonaut.com +mailorc.com +mailorg.org +mailosaur.net +mailox.fun +mailpick.biz +mailpluss.com +mailpooch.com +mailpoof.com +mailpress.gq +mailproxsy.com +mailquack.com +mailrock.biz +mailsac.com +mailscrap.com +mailseal.de +mailshell.com +mailshiv.com +mailsiphon.com +mailslapping.com +mailslite.com +mailsucker.net +mailt.net +mailt.top +mailtechx.com +mailtemp.info +mailtemporaire.com +mailtemporaire.fr +mailto.plus +mailtome.de +mailtothis.com +mailtraps.com +mailtrash.net +mailtrix.net +mailtv.net +mailtv.tv +mailuniverse.co.uk +mailzi.ru +mailzilla.com +mailzilla.org +mainerfolg.info +makemenaughty.club +makemetheking.com +malahov.de +malayalamdtp.com +mama3.org +mamulenok.ru +mandraghen.cf +manifestgenerator.com +mannawo.com +mansiondev.com +manybrain.com +mark-compressoren.ru +marketlink.info +markmurfin.com +mask03.ru +masonline.info +maswae.world +matamuasu.ga +matchpol.net +matra.site +max-mail.org +mbox.re +mbx.cc +mcache.net +mciek.com +mdhc.tk +meantinc.com +mebelnu.info +mechanicalresumes.com +medkabinet-uzi.ru +meepsheep.eu +meidecn.com +meinspamschutz.de +meltedbrownies.com +meltmail.com +memsg.site +mentonit.net +mepost.pw +merry.pink +messagebeamer.de +messwiththebestdielikethe.rest +metadownload.org +metaintern.net +metalunits.com +mezimages.net +mfsa.info +mfsa.ru +mhzayt.online +miaferrari.com +miauj.com +midcoastcustoms.com +midcoastcustoms.net +midcoastsolutions.com +midcoastsolutions.net +midiharmonica.com +midlertidig.com +midlertidig.net +midlertidig.org +mierdamail.com +migmail.net +migmail.pl +migumail.com +mihep.com +mijnhva.nl +ministry-of-silly-walks.de +minsmail.com +mintemail.com +mirai.re +misterpinball.de +miucce.com +mji.ro +mjj.edu.ge +mjukglass.nu +mkpfilm.com +ml8.ca +mm.my +mm5.se +mnode.me +moakt.cc +moakt.co +moakt.com +moakt.ws +mobileninja.co.uk +mobilevpn.top +moburl.com +mockmyid.com +moeri.org +mofu.be +mohmal.com +mohmal.im +mohmal.in +mohmal.tech +moimoi.re +molms.com +momentics.ru +monachat.tk +monadi.ml +moneypipe.net +monumentmail.com +moonwake.com +moot.es +moreawesomethanyou.com +moreorcs.com +morriesworld.ml +morsin.com +moruzza.com +motique.de +mountainregionallibrary.net +mox.pp.ua +moy-elektrik.ru +moza.pl +mozej.com +mp-j.ga +mr24.co +mrvpm.net +mrvpt.com +msgos.com +mspeciosa.com +msrc.ml +mswork.ru +msxd.com +mt2009.com +mt2014.com +mt2015.com +mtmdev.com +muathegame.com +muchomail.com +mucincanon.com +muehlacker.tk +muell.icu +muell.monster +muell.xyz +muellemail.com +muellmail.com +munoubengoshi.gq +musiccode.me +mutant.me +mvrht.com +mvrht.net +mwarner.org +mxclip.com +mxfuel.com +my-pomsies.ru +my-teddyy.ru +my10minutemail.com +mybitti.de +mycleaninbox.net +mycorneroftheinter.net +myde.ml +mydefipet.live +mydemo.equipment +myecho.es +myemailboxy.com +mygeoweb.info +myindohome.services +myinterserver.ml +mykickassideas.com +mymail-in.net +mymail90.com +mymailoasis.com +mynetstore.de +myopang.com +mypacks.net +mypartyclip.de +myphantomemail.com +mysamp.de +myspaceinc.com +myspaceinc.net +myspaceinc.org +myspacepimpedup.com +myspamless.com +mystvpn.com +mysugartime.ru +mytemp.email +mytempemail.com +mytempmail.com +mytrashmail.com +mywarnernet.net +mywrld.site +mywrld.top +myzx.com +mzico.com +n1nja.org +na-cat.com +nabuma.com +nada.email +nada.ltd +nagi.be +nakedtruth.biz +nanonym.ch +naslazhdai.ru +nationalgardeningclub.com +nawmin.info +nbzmr.com +negated.com +neko2.net +nekochan.fr +neomailbox.com +neotlozhniy-zaim.ru +nepwk.com +nervmich.net +nervtmich.net +net1mail.com +netcom.ws +netmails.com +netmails.net +netricity.nl +netris.net +netviewer-france.com +netzidiot.de +nevermail.de +newbpotato.tk +newfilm24.ru +newideasfornewpeople.info +newmail.top +next.ovh +nextmail.info +nextstopvalhalla.com +nezdiro.org +nezid.com +nezumi.be +nezzart.com +nfast.net +nguyenusedcars.com +nh3.ro +nice-4u.com +nicknassar.com +nincsmail.com +nincsmail.hu +niseko.be +niwl.net +nm7.cc +nmail.cf +nnh.com +nnot.net +nnoway.ru +no-spam.ws +no-ux.com +noblepioneer.com +nobugmail.com +nobulk.com +nobuma.com +noclickemail.com +nodezine.com +nogmailspam.info +noicd.com +nokiamail.com +nolemail.ga +nomail.cf +nomail.ga +nomail.pw +nomail2me.com +nomorespamemails.com +nonspam.eu +nonspammer.de +nonze.ro +noref.in +norseforce.com +norwegischlernen.info +nospam4.us +nospamfor.us +nospamthanks.info +nothingtoseehere.ca +notif.me +notmailinator.com +notrnailinator.com +notsharingmy.info +now.im +nowhere.org +nowmymail.com +nowmymail.net +nproxi.com +nthrl.com +ntlhelp.net +nubescontrol.com +nullbox.info +nurfuerspam.de +nut.cc +nutpa.net +nuts2trade.com +nvhrw.com +nwldx.com +nwytg.com +nwytg.net +ny7.me +nypato.com +nyrmusic.com +o2stk.org +o7i.net +oalsp.com +obfusko.com +objectmail.com +obobbo.com +oborudovanieizturcii.ru +obxpestcontrol.com +octovie.com +odaymail.com +odem.com +odnorazovoe.ru +oepia.com +oerpub.org +offshore-proxies.net +ohaaa.de +ohi.tw +oida.icu +oing.cf +okclprojects.com +okinawa.li +okrent.us +okzk.com +olimp-case.ru +olypmall.ru +omail.pro +omnievents.org +omtecha.com +one-mail.top +one-time.email +one2mail.info +onekisspresave.com +onemail.host +oneoffemail.com +oneoffmail.com +onetm.jp +onewaymail.com +onlatedotcom.info +online.ms +onlineidea.info +onqin.com +ontyne.biz +oohioo.com +oolus.com +oonies-shoprus.ru +oopi.org +oosln.com +opayq.com +openavz.com +opendns.ro +opentrash.com +opmmedia.ga +opp24.com +optimaweb.me +opwebw.com +oranek.com +ordinaryamerican.net +oreidresume.com +orgmbx.cc +oroki.de +oshietechan.link +otherinbox.com +ourklips.com +ourpreviewdomain.com +outlawspam.com +outmail.win +ovomail.co +ovpn.to +owleyes.ch +owlpic.com +ownsyou.de +oxopoha.com +ozyl.de +p-banlis.ru +p33.org +p71ce1m.com +pa9e.com +pachilly.com +packiu.com +pagamenti.tk +paharpurmim.ga +pakadebu.ga +pamaweb.com +pancakemail.com +papierkorb.me +paplease.com +para2019.ru +parlimentpetitioner.tk +pastebitch.com +patonce.com +pavilionx2.com +payperex2.com +payspun.com +pe.hu +pecinan.com +pecinan.net +pecinan.org +penisgoes.in +penoto.tk +pepbot.com +peterdethier.com +petloca.com +petrzilka.net +pewpewpewpew.pw +pfui.ru +phone-elkey.ru +photo-impact.eu +photomark.net +pi.vu +piaa.me +pig.pp.ua +pii.at +piki.si +pimpedupmyspace.com +pinehill-seattle.org +pingir.com +pipemail.space +pisls.com +pitaniezdorovie.ru +pivo-bar.ru +pixiil.com +pjjkp.com +placebomail10.com +pleasenoham.org +plexfirm.com +plexolan.de +plhk.ru +ploae.com +plw.me +poehali-otdihat.ru +pojok.ml +pokemail.net +pokiemobile.com +polarkingxx.ml +politikerclub.de +polyfaust.net +pooae.com +poofy.org +pookmail.com +poopiebutt.club +popcornfarm7.com +popcornfly.com +popesodomy.com +popgx.com +porjoton.com +porsh.net +posdz.com +posta.store +postacin.com +postonline.me +poutineyourface.com +powered.name +powerencry.com +powlearn.com +pp7rvv.com +ppetw.com +pptrvv.com +pqoia.com +pratikmail.com +pratikmail.net +pratikmail.org +prazdnik-37.ru +predatorrat.cf +predatorrat.ga +predatorrat.gq +predatorrat.ml +predatorrat.tk +premium-mail.fr +primabananen.net +prin.be +privacy.net +privatdemail.net +privy-mail.com +privy-mail.de +privymail.de +pro-tag.org +pro5g.com +procrackers.com +profast.top +projectcl.com +promailt.com +proprietativalcea.ro +propscore.com +protempmail.com +proxymail.eu +proxyparking.com +prtnx.com +prtshr.com +prtz.eu +psh.me +psles.com +psnator.com +psoxs.com +puglieisi.com +puji.pro +punkass.com +puppetmail.de +purcell.email +purelogistics.org +put2.net +puttanamaiala.tk +putthisinyourspamdatabase.com +pwrby.com +qasti.com +qbfree.us +qc.to +qibl.at +qiott.com +qipmail.net +qiq.us +qisdo.com +qisoa.com +qmrbe.com +qoika.com +qopow.com +qq.my +qsl.ro +qtum-ico.com +quadrafit.com +quick-mail.cc +quickemail.info +quickinbox.com +quickmail.nl +quicksend.ch +ququb.com +qvy.me +qwickmail.com +r4nd0m.de +ra3.us +rabin.ca +rabiot.reisen +rackabzar.com +raetp9.com +rainbowly.ml +raketenmann.de +rancidhome.net +randomail.io +randomail.net +rapt.be +raqid.com +rax.la +raxtest.com +razemail.com +razuz.com +rbb.org +rcasd.com +rcpt.at +rdklcrv.xyz +re-gister.com +reality-concept.club +reallymymail.com +realtyalerts.ca +rebates.stream +receiveee.com +recipeforfailure.com +recode.me +reconmail.com +recyclemail.dk +redfeathercrow.com +reftoken.net +regbypass.com +regspaces.tk +reimondo.com +rejectmail.com +rejo.technology +reliable-mail.com +remail.cf +remail.ga +remarkable.rocks +remote.li +reptilegenetics.com +resgedvgfed.tk +revolvingdoorhoax.org +rfc822.org +rhyta.com +richfinances.pw +riddermark.de +rifkian.ga +rippb.com +risingsuntouch.com +riski.cf +rklips.com +rkomo.com +rm2rf.com +rma.ec +rmqkr.net +rnailinator.com +ro.lt +robertspcrepair.com +robot-mail.com +rollindo.agency +ronnierage.net +rootfest.net +rosebearmylove.ru +rotaniliam.com +rover.info +rowe-solutions.com +royal.net +royaldoodles.org +royalmarket.life +royandk.com +rppkn.com +rsvhr.com +rtrtr.com +rtskiya.xyz +rudymail.ml +rumgel.com +runi.ca +rupayamail.com +ruru.be +rustydoor.com +rvb.ro +ryteto.me +s0ny.net +s33db0x.com +sabrestlouis.com +sackboii.com +saeoil.com +safaat.cf +safermail.info +safersignup.de +safetymail.info +safetypost.de +saharanightstempe.com +salmeow.tk +samsclass.info +sandcars.net +sandelf.de +sandwhichvideo.com +sanfinder.com +sanim.net +sanstr.com +sast.ro +satisfyme.club +satukosong.com +sausen.com +saynotospams.com +scatmail.com +scay.net +schachrol.com +schafmail.de +schmeissweg.tk +schrott-email.de +scrsot.com +sd3.in +sdvft.com +sdvgeft.com +sdvrecft.com +secmail.pw +secretemail.de +secure-mail.biz +secure-mail.cc +secured-link.net +securehost.com.es +seekapps.com +seekjobs4u.com +sejaa.lv +selfdestructingmail.com +selfdestructingmail.org +send22u.info +sendfree.org +sendingspecialflyers.com +sendnow.win +sendspamhere.com +senseless-entertainment.com +server.ms +services391.com +sexforswingers.com +sexical.com +sexyalwasmi.top +shadap.org +shalar.net +sharedmailbox.org +sharklasers.com +sheryli.com +shhmail.com +shhuut.org +shieldedmail.com +shieldemail.com +shiftmail.com +shipfromto.com +shiphazmat.org +shipping-regulations.com +shippingterms.org +shitaway.tk +shitmail.de +shitmail.me +shitmail.org +shmeriously.com +shopxda.com +shortmail.net +shotmail.ru +showslow.de +shrib.com +shut.name +shut.ws +siberpay.com +sidelka-mytischi.ru +siftportal.ru +sify.com +sika3.com +sikux.com +siliwangi.ga +silvercoin.life +sim-simka.ru +simaenaga.com +simpleitsecurity.info +sin.cl +sinaite.net +sinema.ml +sinfiltro.cl +singlespride.com +sinnlos-mail.de +sino.tw +siteposter.net +sizzlemctwizzle.com +sjuaq.com +skeefmail.com +skrx.tk +sky-inbox.com +sky-ts.de +skyrt.de +slapsfromlastnight.com +slaskpost.se +slave-auctions.net +slippery.email +slipry.net +slopsbox.com +slothmail.net +slushmail.com +sluteen.com +sly.io +smallker.tk +smapfree24.com +smapfree24.de +smapfree24.eu +smapfree24.info +smapfree24.org +smartnator.com +smarttalent.pw +smashmail.de +smellfear.com +smellrear.com +smellypotato.tk +smtp99.com +smwg.info +snakemail.com +snapwet.com +sneakmail.de +snece.com +social-mailer.tk +socialfurry.org +sofia.re +sofimail.com +sofort-mail.de +sofortmail.de +sofrge.com +softkey-office.ru +softpls.asia +sogetthis.com +sohai.ml +sohus.cn +soioa.com +soisz.com +solar-impact.pro +solvemail.info +solventtrap.wiki +songsign.com +sonshi.cf +soodmail.com +soodomail.com +soodonims.com +soombo.com +soon.it +spacebazzar.ru +spam-be-gone.com +spam.care +spam.la +spam.org.es +spam.su +spam4.me +spamail.de +spamarrest.com +spamavert.com +spambob.com +spambob.net +spambob.org +spambog.com +spambog.de +spambog.net +spambog.ru +spambooger.com +spambox.info +spambox.me +spambox.org +spambox.us +spamcero.com +spamcon.org +spamcorptastic.com +spamcowboy.com +spamcowboy.net +spamcowboy.org +spamday.com +spamdecoy.net +spamex.com +spamfighter.cf +spamfighter.ga +spamfighter.gq +spamfighter.ml +spamfighter.tk +spamfree.eu +spamfree24.com +spamfree24.de +spamfree24.eu +spamfree24.info +spamfree24.net +spamfree24.org +spamgoes.in +spamherelots.com +spamhereplease.com +spamhole.com +spamify.com +spaminator.de +spamkill.info +spaml.com +spaml.de +spamlot.net +spammer.fail +spammotel.com +spammy.host +spamobox.com +spamoff.de +spamsalad.in +spamslicer.com +spamsphere.com +spamspot.com +spamstack.net +spamthis.co.uk +spamthis.network +spamthisplease.com +spamtrail.com +spamtrap.ro +spamtroll.net +spamwc.cf +spamwc.ga +spamwc.gq +spamwc.ml +speedgaus.net +sperma.cf +spikio.com +spindl-e.com +spoofmail.de +spr.io +spritzzone.de +spruzme.com +spybox.de +spymail.com +squizzy.de +squizzy.net +sroff.com +sry.li +ssoia.com +stanfordujjain.com +starlight-breaker.net +starpower.space +startfu.com +startkeys.com +statdvr.com +stathost.net +statiix.com +stayhome.li +steam-area.ru +steambot.net +stexsy.com +stinkefinger.net +stop-my-spam.cf +stop-my-spam.com +stop-my-spam.ga +stop-my-spam.ml +stop-my-spam.pp.ua +stop-my-spam.tk +stopspam.app +storiqax.top +storj99.com +storj99.top +streetwisemail.com +stromox.com +stuckmail.com +stuffmail.de +stumpfwerk.com +stylist-volos.ru +submic.com +suburbanthug.com +suckmyd.com +sueshaw.com +suexamplesb.com +suioe.com +super-auswahl.de +supergreatmail.com +supermailer.jp +superplatyna.com +superrito.com +supersave.net +superstachel.de +superyp.com +suremail.info +sute.jp +svip520.cn +svk.jp +svxr.org +sweetpotato.ml +sweetxxx.de +swift-mail.net +swift10minutemail.com +syinxun.com +sylvannet.com +symphonyresume.com +syosetu.gq +syujob.accountants +szerz.com +tafmail.com +tafoi.gr +taglead.com +tagmymedia.com +tagyourself.com +talkinator.com +tanukis.org +tapchicuoihoi.com +taphear.com +tapi.re +tarzanmail.cf +tastrg.com +taukah.com +tb-on-line.net +tcwlm.com +tcwlx.com +tdtda.com +tech69.com +techblast.ch +techemail.com +techgroup.me +technoproxy.ru +teerest.com +teewars.org +tefl.ro +telecomix.pl +teleg.eu +teleworm.com +teleworm.us +tellos.xyz +teml.net +temp-link.net +temp-mail.com +temp-mail.de +temp-mail.org +temp-mail.pp.ua +temp-mail.ru +temp-mails.com +tempail.com +tempalias.com +tempe-mail.com +tempemail.biz +tempemail.co.za +tempemail.com +tempemail.net +tempinbox.co.uk +tempinbox.com +tempmail.cn +tempmail.co +tempmail.de +tempmail.eu +tempmail.it +tempmail.pp.ua +tempmail.us +tempmail.ws +tempmail2.com +tempmaildemo.com +tempmailer.com +tempmailer.de +tempmailer.net +tempmailo.com +tempomail.fr +tempomail.org +temporarily.de +temporarioemail.com.br +temporary-mail.net +temporaryemail.net +temporaryemail.us +temporaryforwarding.com +temporaryinbox.com +temporarymailaddress.com +tempr.email +tempsky.com +tempthe.net +tempymail.com +tensi.org +ternaklele.ga +testore.co +testudine.com +thanksnospam.info +thankyou2010.com +thatim.info +thc.st +theaviors.com +thebearshark.com +thecarinformation.com +thechildrensfocus.com +thecity.biz +thecloudindex.com +thediamants.org +thedirhq.info +theeyeoftruth.com +thejoker5.com +thelightningmail.net +thelimestones.com +thembones.com.au +themegreview.com +themostemail.com +thereddoors.online +theroyalweb.club +thescrappermovie.com +theteastory.info +thex.ro +thichanthit.com +thietbivanphong.asia +thisisnotmyrealemail.com +thismail.net +thisurl.website +thnikka.com +thoas.ru +thraml.com +thrma.com +throam.com +thrott.com +throwam.com +throwawayemailaddress.com +throwawaymail.com +throwawaymail.pp.ua +throya.com +thrubay.com +thunderbolt.science +thunkinator.org +thxmate.com +tiapz.com +tic.ec +tilien.com +timgiarevn.com +timkassouf.com +tinoza.org +tinyurl24.com +tipsb.com +tittbit.in +tiv.cc +tizi.com +tkitc.de +tlpn.org +tmail.com +tmail.ws +tmailinator.com +tmails.net +tmmbt.net +tmpbox.net +tmpemails.com +tmpeml.com +tmpeml.info +tmpjr.me +tmpmail.net +tmpmail.org +tmpx.sa.com +toddsbighug.com +tofeat.com +toiea.com +tokem.co +tokenmail.de +tonaeto.com +tonne.to +tonymanso.com +toomail.biz +toon.ml +top-shop-tovar.ru +top101.de +top1mail.ru +top1post.ru +topinrock.cf +topmail2.com +topmail2.net +topofertasdehoy.com +topranklist.de +toprumours.com +tormail.org +toss.pw +tosunkaya.com +totallynotfake.net +totalvista.com +totesmail.com +totoan.info +tourcc.com +tp-qa-mail.com +tpwlb.com +tqoai.com +tqosi.com +tradermail.info +tranceversal.com +trash-amil.com +trash-mail.at +trash-mail.cf +trash-mail.com +trash-mail.de +trash-mail.ga +trash-mail.gq +trash-mail.ml +trash-mail.tk +trash-me.com +trash2009.com +trash2010.com +trash2011.com +trashcanmail.com +trashdevil.com +trashdevil.de +trashemail.de +trashemails.de +trashinbox.com +trashmail.at +trashmail.com +trashmail.de +trashmail.gq +trashmail.io +trashmail.me +trashmail.net +trashmail.org +trashmail.ws +trashmailer.com +trashmailgenerator.de +trashmails.com +trashymail.com +trashymail.net +trasz.com +trayna.com +trbvm.com +trbvn.com +trbvo.com +trend-maker.ru +trgfu.com +trgovinanaveliko.info +trialmail.de +trickmail.net +trillianpro.com +triots.com +trixtrux1.ru +trollproject.com +tropicalbass.info +trungtamtoeic.com +truthfinderlogin.com +tryalert.com +tryninja.io +tryzoe.com +ttirv.org +ttszuo.xyz +tualias.com +tuofs.com +turoid.com +turual.com +turuma.com +tutuapp.bid +tvchd.com +tverya.com +twinmail.de +twkly.ml +twocowmail.net +twoweirdtricks.com +twzhhq.online +txen.de +txtadvertise.com +tyhe.ro +tyldd.com +tympe.net +uacro.com +uber-mail.com +ubismail.net +ubm.md +ucche.us +ucupdong.ml +uemail99.com +ufacturing.com +uggsrock.com +uguuchantele.com +uhe2.com +uhhu.ru +uiu.us +ujijima1129.gq +uk.to +ultra.fyi +ultrada.ru +uma3.be +umail.net +undo.it +unicodeworld.com +unids.com +unimark.org +unit7lahaina.com +unmail.ru +uooos.com +upliftnow.com +uplipht.com +uploadnolimit.com +upozowac.info +urfunktion.se +urhen.com +uroid.com +us.af +us.to +usa.cc +usako.net +usbc.be +used-product.fr +ushijima1129.cf +ushijima1129.ga +ushijima1129.gq +ushijima1129.ml +ushijima1129.tk +utiket.us +uu.gl +uu2.ovh +uuf.me +uwork4.us +uyhip.com +vaasfc4.tk +vaati.org +valemail.net +valhalladev.com +vankin.de +vctel.com +vda.ro +vddaz.com +vdig.com +veanlo.com +vemomail.win +venompen.com +veo.kr +ver0.cf +ver0.ga +ver0.gq +ver0.ml +ver0.tk +vercelli.cf +vercelli.ga +vercelli.gq +vercelli.ml +verdejo.com +vermutlich.net +veryday.ch +veryday.eu +veryday.info +veryrealemail.com +vesa.pw +vevs.de +vfemail.net +via.tokyo.jp +vickaentb.tk +victime.ninja +victoriantwins.com +vidchart.com +viditag.com +viewcastmedia.com +viewcastmedia.net +viewcastmedia.org +vikingsonly.com +vinernet.com +vintomaper.com +vipepe.com +vipmail.name +vipmail.pw +vipxm.net +viralplays.com +virtualemail.info +visal007.tk +visal168.cf +visal168.ga +visal168.gq +visal168.ml +visal168.tk +vixletdev.com +vixtricks.com +vkcode.ru +vmailing.info +vmani.com +vmpanda.com +vnedu.me +voidbay.com +volaj.com +voltaer.com +vomoto.com +vorga.org +votiputox.org +voxelcore.com +vpn.st +vps30.com +vps911.net +vradportal.com +vremonte24-store.ru +vrmtr.com +vsimcard.com +vssms.com +vtxmail.us +vubby.com +vuiy.pw +vusra.com +vztc.com +w-asertun.ru +w3internet.co.uk +wakingupesther.com +walala.org +walkmail.net +walkmail.ru +wallm.com +wanko.be +watch-harry-potter.com +watchever.biz +watchfull.net +watchironman3onlinefreefullmovie.com +wazabi.club +wbdev.tech +wbml.net +web-contact.info +web-ideal.fr +web-inc.net +web-mail.pp.ua +web2mailco.com +webcontact-france.eu +webemail.me +webhook.site +webm4il.info +webmail24.top +webtrip.ch +webuser.in +wee.my +wef.gr +weg-werf-email.de +wegwerf-email-addressen.de +wegwerf-email-adressen.de +wegwerf-email.at +wegwerf-email.de +wegwerf-email.net +wegwerf-emails.de +wegwerfadresse.de +wegwerfemail.com +wegwerfemail.de +wegwerfemail.info +wegwerfemail.net +wegwerfemail.org +wegwerfemailadresse.com +wegwerfmail.de +wegwerfmail.info +wegwerfmail.net +wegwerfmail.org +wegwerpmailadres.nl +wegwrfmail.de +wegwrfmail.net +wegwrfmail.org +wekawa.com +welikecookies.com +wellsfargocomcardholders.com +wemel.top +wetrainbayarea.com +wetrainbayarea.org +wfgdfhj.tk +wg0.com +wh4f.org +whatiaas.com +whatifanalytics.com +whatpaas.com +whatsaas.com +whiffles.org +whopy.com +whyspam.me +wibblesmith.com +wickmail.net +widaryanto.info +widget.gg +wierie.tk +wifimaple.com +wifioak.com +wikidocuslava.ru +wilemail.com +willhackforfood.biz +willselfdestruct.com +wimsg.com +winemaven.info +wins.com.br +wlist.ro +wmail.cf +wmail.club +wokcy.com +wolfmail.ml +wolfsmail.tk +wollan.info +worldspace.link +wpdork.com +wpg.im +wralawfirm.com +writeme.us +wronghead.com +ws.gy +wsym.de +wudet.men +wuespdj.xyz +wupics.com +wuuvo.com +wuzup.net +wuzupmail.net +wwjmp.com +wwwnew.eu +wxnw.net +x24.com +xagloo.co +xagloo.com +xbaby69.top +xcode.ro +xcodes.net +xcompress.com +xcoxc.com +xcpy.com +xemaps.com +xemne.com +xents.com +xjoi.com +xkx.me +xl.cx +xmail.com +xmailer.be +xmaily.com +xn--9kq967o.com +xn--d-bga.net +xojxe.com +xost.us +xoxox.cc +xperiae5.com +xrap.de +xrho.com +xvx.us +xwaretech.com +xwaretech.info +xwaretech.net +xww.ro +xxhamsterxx.ga +xxi2.com +xxlocanto.us +xxolocanto.us +xxqx3802.com +xy9ce.tk +xyzfree.net +xzsok.com +yabai-oppai.tk +yahmail.top +yahooproduct.net +yamail.win +yanet.me +yannmail.win +yapped.net +yaqp.com +yarnpedia.ga +ycare.de +ycn.ro +ye.vc +yedi.org +yeezus.ru +yep.it +yermail.net +yhg.biz +ynmrealty.com +yodx.ro +yogamaven.com +yoggm.com +yomail.info +yoo.ro +yopmail.com +yopmail.fr +yopmail.gq +yopmail.net +yopmail.pp.ua +yordanmail.cf +you-spam.com +yougotgoated.com +youmail.ga +youmailr.com +youneedmore.info +youpymail.com +yourdomain.com +youremail.cf +yourewronghereswhy.com +yourlms.biz +yourspamgoesto.space +yourtube.ml +yroid.com +yspend.com +ytpayy.com +yugasandrika.com +yui.it +yuoia.com +yuurok.com +yxzx.net +yyolf.net +z-o-e-v-a.ru +z0d.eu +z1p.biz +z86.ru +zain.site +zainmax.net +zaktouni.fr +zarabotokdoma11.ru +zasod.com +zaym-zaym.ru +zcrcd.com +zdenka.net +ze.tc +zebins.com +zebins.eu +zehnminuten.de +zehnminutenmail.de +zepp.dk +zetmail.com +zfymail.com +zhaoqian.ninja +zhaoyuanedu.cn +zhcne.com +zhewei88.com +zhorachu.com +zik.dj +zipcad.com +zipo1.gq +zippymail.info +zipsendtest.com +zoaxe.com +zoemail.com +zoemail.net +zoemail.org +zoetropes.org +zombie-hive.com +zomg.info +zsero.com +zumpul.com +zv68.com +zxcv.com +zxcvbnm.com +zymuying.com +zzi.us +zzrgg.com +zzz.com diff --git a/backend-pg/src/lib/validator/index.ts b/backend-pg/src/lib/validator/index.ts new file mode 100644 index 000000000..6bc415680 --- /dev/null +++ b/backend-pg/src/lib/validator/index.ts @@ -0,0 +1 @@ +export { isDisposableEmail } from "./validate-email"; diff --git a/backend-pg/src/lib/validator/validate-email.ts b/backend-pg/src/lib/validator/validate-email.ts new file mode 100644 index 000000000..ec3ef3976 --- /dev/null +++ b/backend-pg/src/lib/validator/validate-email.ts @@ -0,0 +1,10 @@ +import fs from "fs/promises"; +import path from "path"; + +export const isDisposableEmail = async (email: string) => { + const emailDomain = email.split("@")[1]; + const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8"); + + if (disposableEmails.split("\n").includes(emailDomain)) return true; + return false; +}; diff --git a/backend-pg/src/lib/zod/index.ts b/backend-pg/src/lib/zod/index.ts new file mode 100644 index 000000000..3b04f033e --- /dev/null +++ b/backend-pg/src/lib/zod/index.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +// this is a patched zod string to remove empty string to undefined +export const zpStr = ( + schema: T, + opt: { stripNull: boolean } = { stripNull: true } +) => + z.preprocess((val) => { + if (opt.stripNull && val === null) return undefined; + if (typeof val !== "string") return val; + return val.trim() || undefined; + }, schema); diff --git a/backend-pg/src/server/app.ts b/backend-pg/src/server/app.ts index 079b25157..66f43073b 100644 --- a/backend-pg/src/server/app.ts +++ b/backend-pg/src/server/app.ts @@ -1,6 +1,5 @@ import dotenv from "dotenv"; import fasitfy from "fastify"; -import { z } from "zod"; import type { FastifyCookieOptions } from "@fastify/cookie"; import cookie from "@fastify/cookie"; import type { FastifyCorsOptions } from "@fastify/cors"; @@ -9,20 +8,24 @@ import helmet from "@fastify/helmet"; import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit"; -import { initEnvConfig } from "@lib/config/env"; +import { initDbConnection } from "@app/db"; +import { smtpServiceFactory } from "@app/services/smtp/smtp-service"; + +import { formatSmtpConfig, initEnvConfig } from "@lib/config/env"; import { initLogger } from "@lib/logger"; import { globalRateLimiterCfg } from "./config/rateLimiter"; import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod"; import { fastifyIp } from "./plugins/ip"; import { fastifySwagger } from "./plugins/swagger"; +import { registerRoutes } from "./routes"; dotenv.config(); // Run the server! const main = async () => { const logger = await initLogger(); - initEnvConfig(logger); + const envCfg = initEnvConfig(logger); const server = fasitfy({ logger, @@ -32,42 +35,31 @@ const main = async () => { server.setValidatorCompiler(validatorCompiler); server.setSerializerCompiler(serializerCompiler); + const db = initDbConnection(envCfg.DB_CONNECTION_URI); + const smtp = smtpServiceFactory(formatSmtpConfig()); + try { - // TODO(akhilmhdh:pg): change this to environment variable with default await server.register(cookie, { - secret: "infisical-cookie-secret" + secret: envCfg.COOKIE_SECRET_SIGN_KEY }); await server.register(cors, { credentials: true, - origin: "http://localhost:3000" + origin: true }); // pull ip based on various proxy headers await server.register(fastifyIp); - // Rate limiters and security headers - await server.register(ratelimiter, globalRateLimiterCfg); - await server.register(helmet); - await server.register(fastifySwagger); - // Declare a route - server.route({ - method: "GET", - url: "/", - schema: { - response: { - 200: z.object({ hello: z.string() }) - } - }, - handler: () => ({ - hello: "world" - }) - }); + // Rate limiters and security headers + await server.register(ratelimiter, globalRateLimiterCfg); + await server.register(helmet, { contentSecurityPolicy: false }); + await server.register(registerRoutes, { prefix: "/api", smtp, db }); await server.ready(); server.swagger(); - await server.listen({ port: 8000 }); + await server.listen({ port: envCfg.PORT, host: envCfg.HOST }); } catch (err) { server.log.error(err); process.exit(1); diff --git a/backend-pg/src/server/plugins/swagger.ts b/backend-pg/src/server/plugins/swagger.ts index 008994da6..6b8295a87 100644 --- a/backend-pg/src/server/plugins/swagger.ts +++ b/backend-pg/src/server/plugins/swagger.ts @@ -4,6 +4,7 @@ import swaggerUI from "@fastify/swagger-ui"; import { jsonSchemaTransform } from "./fastify-zod"; +// TODO(akhilmhdh-pg): change the localhost port later export const fastifySwagger = fp(async (fastify) => { await fastify.register(swagger, { transform: jsonSchemaTransform, @@ -15,12 +16,12 @@ export const fastifySwagger = fp(async (fastify) => { }, servers: [ { - url: "https://app.infisical.com", - description: "Production server" + url: "http://localhost:4000", + description: "Local server" }, { - url: "http://localhost:8000", - description: "Local server" + url: "https://app.infisical.com", + description: "Production server" } ], components: { diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts new file mode 100644 index 000000000..3488843ad --- /dev/null +++ b/backend-pg/src/server/routes/index.ts @@ -0,0 +1,44 @@ +import { Knex } from "knex"; + +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 { TSmtpService } from "@app/services/smtp/smtp-service"; +import { tokenDalFactory } from "@app/services/token/token-dal"; +import { tokenServiceFactory } from "@app/services/token/token-service"; + +import { registerV1Routes } from "./v1"; +import { registerV2Routes } from "./v2"; +import { registerV3Routes } from "./v3"; + +export const registerRoutes = async ( + server: FastifyZodProvider, + { db, smtp }: { db: Knex; smtp: TSmtpService } +) => { + // db layers + const authDal = authDalFactory(db); + const authTokenDal = tokenDalFactory(db); + + // 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 }); + + // inject all services + server.decorate("services", { + login: loginService, + password: passwordService, + signup: signupService + } as FastifyZodProvider["services"]); + + server.decorate("store", { + user: authDal + } as FastifyZodProvider["store"]); + + // register routes for v1 + await server.register(registerV1Routes, { prefix: "/v1" }); + await server.register(registerV2Routes, { prefix: "/v2" }); + await server.register(registerV3Routes, { prefix: "/v3" }); +}; diff --git a/backend-pg/src/server/routes/v1/index.ts b/backend-pg/src/server/routes/v1/index.ts new file mode 100644 index 000000000..086fee790 --- /dev/null +++ b/backend-pg/src/server/routes/v1/index.ts @@ -0,0 +1,5 @@ +import { registerPasswordRouter } from "./password-router"; + +export const registerV1Routes = async (server: FastifyZodProvider) => { + await server.register(registerPasswordRouter, { prefix: "/password" }); +}; diff --git a/backend-pg/src/server/routes/v1/password-router.ts b/backend-pg/src/server/routes/v1/password-router.ts new file mode 100644 index 000000000..d506f9814 --- /dev/null +++ b/backend-pg/src/server/routes/v1/password-router.ts @@ -0,0 +1,115 @@ +import { z } from "zod"; + +import { BackupPrivateKeySchema } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; + +export const registerPasswordRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/srp1", + schema: { + body: z.object({ + clientPublicKey: z.string().trim() + }), + response: { + 200: z.object({ + serverPublicKey: z.string(), + salt: z.string() + }) + } + }, + handler: async (req) => { + const { salt, serverPublicKey } = await server.services.password.generateServerPubKey( + req.auth.userId, + req.body.clientPublicKey + ); + return { salt, serverPublicKey }; + } + }); + + server.route({ + method: "POST", + url: "/change-password", + schema: { + body: z.object({ + clientProof: z.string().trim(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: 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() + }) + } + }, + handler: async (req, res) => { + const appCfg = getConfig(); + await server.services.password.changePassword({ ...req.body, userId: req.auth.userId }); + + res.cookie("jid", appCfg.COOKIE_SECRET_SIGN_KEY, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + return { message: "Successfully changed password" }; + } + }); + + server.route({ + method: "POST", + url: "/backup-private-key", + schema: { + body: z.object({ + clientProof: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + iv: z.string().trim(), + tag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string(), + backupPrivateKey: BackupPrivateKeySchema + }) + } + }, + handler: async (req) => { + const backupPrivateKey = await server.services.password.createBackupPrivateKey({ + ...req.body, + userId: req.auth.userId + }); + if (!backupPrivateKey) throw new Error("Failed to create backup key"); + + return { message: "Successfully updated backup private key", backupPrivateKey }; + } + }); + + server.route({ + method: "GET", + url: "/backup-private-key", + schema: { + response: { + 200: z.object({ + message: z.string(), + backupPrivateKey: BackupPrivateKeySchema + }) + } + }, + handler: async (req) => { + const backupPrivateKey = await server.services.password.getBackupPrivateKeyOfUser( + req.auth.userId + ); + if (!backupPrivateKey) throw new Error("Failed to find backup key"); + + return { message: "Successfully updated backup private key", backupPrivateKey }; + } + }); +}; diff --git a/backend-pg/src/server/routes/v2/index.ts b/backend-pg/src/server/routes/v2/index.ts new file mode 100644 index 000000000..66bd9d695 --- /dev/null +++ b/backend-pg/src/server/routes/v2/index.ts @@ -0,0 +1,5 @@ +import { registerMfaRouter } from "./mfa-router"; + +export const registerV2Routes = async (server: FastifyZodProvider) => { + await server.register(registerMfaRouter, { prefix: "/auth" }); +}; diff --git a/backend-pg/src/server/routes/v2/mfa-router.ts b/backend-pg/src/server/routes/v2/mfa-router.ts new file mode 100644 index 000000000..b5fbfadcb --- /dev/null +++ b/backend-pg/src/server/routes/v2/mfa-router.ts @@ -0,0 +1,89 @@ +import jwt, { JwtPayload } from "jsonwebtoken"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { AuthTokenType } from "@app/services/auth/auth-signup-type"; + +export const registerMfaRouter = async (server: FastifyZodProvider) => { + const cfg = getConfig(); + + server.addHook("preParsing", async (req, res) => { + const authorizationHeader = req.headers.authorization; + + if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) { + res.status(401).send({ error: "Missing bearer token" }); + return; + } + const token = authorizationHeader.split(" ")[1]; + if (!token) res.status(401).send({ error: "Missing bearer token" }); + + const decodedToken = jwt.verify(token, cfg.JWT_AUTH_SECRET) as JwtPayload; + if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN) + throw new Error("Unauthorized access"); + + const user = await server.store.user.getUserById(decodedToken.userId); + if (!user) throw new Error("User not found"); + req.mfa.userId = user.id; + req.mfa.user = user; + }); + + server.route({ + url: "/mfa/send", + method: "POST", + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.login.resendMfaToken(req.mfa.userId); + return { message: "Successfully send new mfa code" }; + } + }); + + server.route({ + url: "/mfa/verify", + method: "POST", + schema: { + body: z.object({ + mfaToken: z.string().trim() + }), + response: { + 200: z.object({ + encryptionVersion: z.number().default(1).optional(), + protectedKey: z.string(), + protectedKeyIV: z.string(), + protectedKeyTag: z.string(), + publicKey: z.string(), + encryptedPrivateKey: z.string(), + iv: z.string(), + tag: z.string(), + 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, token } = await server.services.login.verifyMfaToken({ + userAgent, + ip: req.realIp, + userId: req.mfa.userId, + mfaToken: req.body.mfaToken + }); + + res.setCookie("jid", token.refresh, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { token: token.access, ...user }; + } + }); +}; diff --git a/backend-pg/src/server/routes/v3/index.ts b/backend-pg/src/server/routes/v3/index.ts new file mode 100644 index 000000000..982dee111 --- /dev/null +++ b/backend-pg/src/server/routes/v3/index.ts @@ -0,0 +1,7 @@ +import { registerLoginRouter } from "./login-router"; +import { registerSignupRouter } from "./signup-router"; + +export const registerV3Routes = async (server: FastifyZodProvider) => { + await server.register(registerSignupRouter, { prefix: "/signup" }); + await server.register(registerLoginRouter, { prefix: "/auth" }); +}; diff --git a/backend-pg/src/server/routes/v3/login-router.ts b/backend-pg/src/server/routes/v3/login-router.ts new file mode 100644 index 000000000..83fa5f89e --- /dev/null +++ b/backend-pg/src/server/routes/v3/login-router.ts @@ -0,0 +1,97 @@ +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; + +export const registerLoginRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/login1", + schema: { + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientPublicKey: z.string().trim() + }), + response: { + 200: z.object({ + serverPublicKey: z.string(), + salt: z.string() + }) + } + }, + handler: async (req) => { + const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({ + email: req.body.email, + clientPublicKey: req.body.clientPublicKey, + providerAuthToken: req.body.providerAuthToken + }); + + return { serverPublicKey, salt }; + } + }); + + server.route({ + method: "POST", + url: "/login2", + schema: { + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientProof: z.string().trim() + }), + response: { + 200: z.discriminatedUnion("mfaEnabled", [ + z.object({ mfaEnabled: z.literal(true), token: z.string() }), + z.object({ + mfaEnabled: z.literal(false), + encryptionVersion: z.number().default(1).optional(), + protectedKey: z.string(), + protectedKeyIV: z.string(), + protectedKeyTag: z.string(), + publicKey: z.string(), + encryptedPrivateKey: z.string(), + iv: z.string(), + tag: z.string(), + 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 data = await server.services.login.loginExchangeClientProof({ + email: req.body.email, + ip: req.realIp, + userAgent, + clientProof: req.body.clientProof + }); + + if (data.isMfaEnabled) { + return { mfaEnabled: true, token: data.token } as const; // for discriminated union + } + + res.setCookie("jid", data.token.refresh, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { + mfaEnabled: false, + encryptionVersion: data.user.encryptionVersion, + token: data.token.access, + publicKey: data.user.publicKey, + encryptedPrivateKey: data.user.encryptedPrivateKey, + iv: data.user.iv, + tag: data.user.tag, + protectedKey: data.user.protectedKey, + protectedKeyIV: data.user.protectedKeyIV, + protectedKeyTag: data.user.protectedKeyTag + } as const; + } + }); +}; diff --git a/backend-pg/src/server/routes/v3/signup-router.ts b/backend-pg/src/server/routes/v3/signup-router.ts new file mode 100644 index 000000000..ff6ec2036 --- /dev/null +++ b/backend-pg/src/server/routes/v3/signup-router.ts @@ -0,0 +1,103 @@ +import { z } from "zod"; + +import { UserSchema } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; + +export const registerSignupRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/email/signup", + method: "POST", + schema: { + body: z.object({ + email: z.string().email().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.signup.beginEmailSignupProcess(req.body.email); + return { message: `Sent an email verification code to ${req.body.email}` }; + } + }); + + server.route({ + url: "/email/verify", + method: "POST", + schema: { + body: z.object({ + email: z.string().email().trim(), + code: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string(), + token: z.string(), + user: UserSchema + }) + } + }, + handler: async (req) => { + const { token, user } = await server.services.signup.verifyEmailSignup( + req.body.email, + req.body.code + ); + return { message: "Successfuly verified email", token, user }; + } + }); + + server.route({ + url: "/complete-account/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(), + organizationName: z.string().trim(), + providerAuthToken: z.string().trim().optional().nullish(), + attributionSource: z.string().trim().optional() + }), + response: { + 200: z.object({ + message: z.string(), + user: UserSchema, + 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.completeEmailAccountSignup({ + ...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 }; + } + }); +}; diff --git a/backend-pg/src/services/auth/auth-dal.ts b/backend-pg/src/services/auth/auth-dal.ts new file mode 100644 index 000000000..eb539f911 --- /dev/null +++ b/backend-pg/src/services/auth/auth-dal.ts @@ -0,0 +1,131 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TBackupPrivateKey, TUser, TUserEncryptionKey } from "@app/db/schemas"; + +export type TAuthDalFactory = ReturnType; + +export const authDalFactory = (db: TDbClient) => { + // getters + const getUserByEmail = async (email: string): Promise => + db(TableName.Users).where({ email }).select("*").first(); + + const getUserById = async (userId: string): Promise => + 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 = {} + ): Promise => { + const [user] = await db(TableName.Users) + .insert({ email, ...data }) + .returning("*"); + return user; + }; + + const updateUser = async ( + email: string, + data: Partial = {} + ): Promise => { + const [user] = await db(TableName.Users) + .where({ email }) + .update({ ...data }) + .returning("*"); + return user; + }; + + const updateUserById = async ( + id: string, + data: Partial = {}, + tx?: Knex + ): Promise => { + const [user] = await (tx ? tx(TableName.Users) : db(TableName.Users)) + .where({ id }) + .update({ ...data }) + .returning("*"); + return user; + }; + + const updateUserEncryptionByUserId = async ( + userId: string, + data: Partial = {}, + tx?: Knex + ): Promise => { + 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, + 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 TUserEncryptionKey) + .onConflict("userId") + .merge() + .returning("*"); + return userEnc; + }; + + const upsertBackupKey = async ( + userId: string, + data: Partial, + tx?: Knex + ): Promise => { + const [backupKey] = await (tx ? tx(TableName.BackupPrivateKey) : db(TableName.BackupPrivateKey)) + .insert({ userId, ...data, updatedAt: new Date().toUTCString() } as TBackupPrivateKey) + .onConflict("userId") + .merge() + .returning("*"); + return backupKey; + }; + + return { + transaction: async (cb: (tx: Knex) => T) => + db.transaction(async (trx) => { + const res = await cb(trx); + return res; + }), + getUserByEmail, + getUserById, + getUserEncKeyByEmail, + getUserEncKeyByUserId, + getBackupPrivateKeyByUserId, + createUser, + updateUser, + updateUserById, + updateUserEncryptionByUserId, + upsertUserEncryptionKey, + upsertBackupKey + }; +}; diff --git a/backend-pg/src/services/auth/auth-login-service.ts b/backend-pg/src/services/auth/auth-login-service.ts new file mode 100644 index 000000000..b1850e789 --- /dev/null +++ b/backend-pg/src/services/auth/auth-login-service.ts @@ -0,0 +1,249 @@ +import jwt from "jsonwebtoken"; + +import { AuthMethod, TUser } from "@app/db/schemas"; +import { UserDeviceSchema } from "@app/db/schemas/user"; +import { getConfig } from "@app/lib/config/env"; +import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; + +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TTokenServiceFactory } from "../token/token-service"; +import { TokenType } from "../token/token-types"; +import { TAuthDalFactory } from "./auth-dal"; +import { + TLoginClientProofDTO, + TLoginGenServerPublicKeyDTO, + TVerifyMfaTokenDTO +} from "./auth-login-type"; +import { AuthTokenType } from "./auth-signup-type"; + +const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuthToken?: string) => { + if (!providerAuthToken) return false; + const decodedToken = jwt.verify(providerAuthToken, jwtSecret) as jwt.JwtPayload; + + if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) return false; + if (decodedToken.email !== email) return false; + return true; +}; + +type TAuthLoginServiceFactoryDep = { + authDal: TAuthDalFactory; + tokenService: TTokenServiceFactory; + smtpService: TSmtpService; +}; + +export type TAuthLoginFactory = ReturnType; +export const authLoginServiceFactory = ({ + authDal, + tokenService, + smtpService +}: TAuthLoginServiceFactoryDep) => { + /* + * Private + * Not exported. This is to update user device list + * If new device is found. Will be saved and a mail will be send + */ + const updateUserDeviceSession = async (user: TUser, ip: string, userAgent: string) => { + const devices = await UserDeviceSchema.parseAsync(JSON.parse(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 smtpService.sendMail({ + template: SmtpTemplates.NewDeviceJoin, + subjectLine: "Successful login from new device", + recipients: [user.email], + substitutions: { + email: user.email, + timestamp: new Date().toString(), + ip, + userAgent + } + }); + } + }; + + /* + * Private + * Send mfa code via email + * */ + const sendUserMfaCode = async (user: TUser) => { + const code = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_MFA, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.EmailMfa, + subjectLine: "Infisical MFA code", + recipients: [user.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 + */ + const generateUserTokens = async (user: TUser, ip: string, userAgent: string) => { + const cfg = getConfig(); + await updateUserDeviceSession(user, ip, userAgent); + const tokenSession = await tokenService.getUserTokenSession({ + userAgent, + ip, + userId: user.id + }); + if (!tokenSession) throw new Error("Failed to create token"); + const accessToken = jwt.sign( + { + authTokenType: AuthTokenType.ACCESS_TOKEN, + userId: user.id, + tokenVersionId: tokenSession.id, + accessVersion: tokenSession.accessVersion + }, + cfg.JWT_AUTH_SECRET, + { expiresIn: cfg.JWT_AUTH_LIFETIME } + ); + + const refreshToken = jwt.sign( + { + authTokenType: AuthTokenType.REFRESH_TOKEN, + userId: user.id, + tokenVersionId: tokenSession.id, + refreshVersion: tokenSession.refreshVersion + }, + cfg.JWT_AUTH_SECRET, + { expiresIn: cfg.JWT_REFRESH_LIFETIME } + ); + + return { access: accessToken, refresh: refreshToken }; + }; + + /* + * Step 1 of login. To get server public key in exchange of client public key + */ + const loginGenServerPublicKey = async ({ + email, + providerAuthToken, + clientPublicKey + }: TLoginGenServerPublicKeyDTO) => { + const user = await authDal.getUserEncKeyByEmail(email); + if (!user || (user && !user.isAccepted)) { + throw new Error("Failed to find user"); + } + const cfg = getConfig(); + + if ( + !user.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, { + clientPublicKey, + serverPrivateKey: serverSrpKey.privateKey + }); + if (!userEncKeys) throw new Error("Failed to update encryption key"); + return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey }; + }; + + /* + * Step 2 of login. Pass the client proof and with multi factor setup handle the required steps + */ + const loginExchangeClientProof = async ({ + email, + clientProof, + providerAuthToken, + ip, + userAgent + }: TLoginClientProofDTO) => { + const user = await authDal.getUserEncKeyByEmail(email); + if (!user) throw new Error("Failed to find user"); + const cfg = getConfig(); + + if ( + !user.authMethods?.includes(AuthMethod.EMAIL) && + !isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken) + ) { + throw new Error("Invalid authorization request"); + } + + if (!user.serverPrivateKey || !user.clientPublicKey) + throw new Error("Failed to authenticate. Try again?"); + const isValidClientProof = await srpCheckClientProof( + user.salt, + user.verifier, + user.serverPrivateKey, + user.clientPublicKey, + clientProof + ); + if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); + + await authDal.updateUserEncryptionByUserId(user.id, { + serverPrivateKey: null, + clientPublicKey: null + }); + // send multi factor auth token if they it enabled + if (user.isMfaEnabled) { + const mfaToken = jwt.sign( + { authTokenType: AuthTokenType.MFA_TOKEN, userId: user.id }, + cfg.JWT_AUTH_SECRET, + { expiresIn: cfg.JWT_MFA_LIFETIME } + ); + await sendUserMfaCode(user); + + return { isMfaEnabled: true, token: mfaToken } as const; + } + + const token = await generateUserTokens(user, ip, userAgent); + return { token, isMfaEnabled: false, user } as const; + }; + + /* + * Multi factor authentication re-send code, Get user id from token + * saved in frontend + */ + const resendMfaToken = async (userId: string) => { + const user = await authDal.getUserById(userId); + if (!user) return; + await sendUserMfaCode(user); + }; + + /* + * Multi factor authentication verification of code + * Third step of login in which user completes with mfa + * */ + const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent }: TVerifyMfaTokenDTO) => { + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_MFA, + userId, + code: mfaToken + }); + const user = await authDal.getUserEncKeyByUserId(userId); + if (!user) throw new Error("Failed to authenticate user"); + + const token = await generateUserTokens(user, ip, userAgent); + return { token, user }; + }; + + /* + * logout user by incrementing the version by 1 meaning any old session will become invalid + * as there number is behind + * */ + const logout = async (userId: string, sessionId: string) => { + await tokenService.clearTokenSessionById(userId, sessionId); + }; + + return { + loginGenServerPublicKey, + loginExchangeClientProof, + logout, + resendMfaToken, + verifyMfaToken + }; +}; diff --git a/backend-pg/src/services/auth/auth-login-type.ts b/backend-pg/src/services/auth/auth-login-type.ts new file mode 100644 index 000000000..e17e98fe1 --- /dev/null +++ b/backend-pg/src/services/auth/auth-login-type.ts @@ -0,0 +1,20 @@ +export type TLoginGenServerPublicKeyDTO = { + email: string; + clientPublicKey: string; + providerAuthToken?: string; +}; + +export type TLoginClientProofDTO = { + email: string; + clientProof: string; + providerAuthToken?: string; + ip: string; + userAgent: string; +}; + +export type TVerifyMfaTokenDTO = { + userId: string; + mfaToken: string; + ip: string; + userAgent: string; +}; diff --git a/backend-pg/src/services/auth/auth-password-service.ts b/backend-pg/src/services/auth/auth-password-service.ts new file mode 100644 index 000000000..ea41198a6 --- /dev/null +++ b/backend-pg/src/services/auth/auth-password-service.ts @@ -0,0 +1,257 @@ +import jwt from "jsonwebtoken"; + +import { getConfig } from "@app/lib/config/env"; +import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; + +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TTokenServiceFactory } from "../token/token-service"; +import { TokenType } from "../token/token-types"; +import { TAuthDalFactory } from "./auth-dal"; +import { + TChangePasswordDTO, + TCreateBackupPrivateKeyDTO, + TResetPasswordViaBackupKeyDTO +} from "./auth-password-type"; +import { AuthTokenType } from "./auth-signup-type"; + +type TAuthPasswordServiceFactoryDep = { + authDal: TAuthDalFactory; + tokenService: TTokenServiceFactory; + smtpService: TSmtpService; +}; + +export type TAuthPasswordFactory = ReturnType; +export const authPaswordServiceFactory = ({ + authDal, + tokenService, + smtpService +}: TAuthPasswordServiceFactoryDep) => { + /* + * Pre setup for pass change with srp protocol + * 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 serverSrpKey = await generateSrpServerKey(user.salt, user.verifier); + const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, { + clientPublicKey, + serverPrivateKey: serverSrpKey.privateKey + }); + if (!userEncKeys) throw new Error("Failed to update encryption key"); + return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey }; + }; + + /* + * Change password to new pass + * */ + const changePassword = async ({ + userId, + clientProof, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier, + tokenVersionId + }: TChangePasswordDTO) => { + const userEnc = await authDal.getUserEncKeyByUserId(userId); + if (!userEnc) throw new Error("Failed to find user"); + + await authDal.updateUserEncryptionByUserId(userEnc.userId, { + serverPrivateKey: null, + clientPublicKey: null + }); + if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) + throw new Error("Failed to authenticate. Try again?"); + const isValidClientProof = await srpCheckClientProof( + userEnc.salt, + userEnc.verifier, + userEnc.serverPrivateKey, + userEnc.clientPublicKey, + clientProof + ); + if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); + + await authDal.updateUserEncryptionByUserId(userId, { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier, + serverPrivateKey: null, + clientPublicKey: null + }); + + if (tokenVersionId) { + await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId); + } + }; + + /* + * Email password reset flow via email. Step 1 send email + */ + const sendPasswordResetEmail = async (email: string) => { + const user = await authDal.getUserByEmail(email); + // ignore as user is not found to avoid an outside entity to identify infisical registered accounts + if (!user || (user && !user.isAccepted)) return; + + const cfg = getConfig(); + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); + }; + + /* + * Step 2 of reset password. Verify the token and inject a temp token to reset password + * */ + const verifyPasswordResetEmail = async (email: string, code: string) => { + const cfg = getConfig(); + const user = await authDal.getUserByEmail(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"); + } + + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id, + code + }); + + const token = jwt.sign( + { + authTokenType: AuthTokenType.SIGNUP_TOKEN, + userId: user.id + }, + cfg.JWT_AUTH_SECRET, + { expiresIn: cfg.JWT_SIGNUP_LIFETIME } + ); + + return { token, user }; + }; + /* + * Reset password of a user via backup key + * */ + const resetPasswordByBackupKey = async ( + userId: string, + { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag + }: TResetPasswordViaBackupKeyDTO + ) => { + await authDal.updateUserEncryptionByUserId(userId, { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier + }); + }; + + /* + * backup key creation to give user's their access back when lost their password + * this also needs to do the generateServerPubKey function to be executed first + * then only client proof can be verified + * */ + const createBackupPrivateKey = async ({ + clientProof, + encryptedPrivateKey, + salt, + verifier, + iv, + tag, + userId + }: TCreateBackupPrivateKeyDTO) => { + const user = await authDal.getUserEncKeyByUserId(userId); + if (!user || (user && !user.isAccepted)) { + throw new Error("Failed to find user"); + } + + if (!user.clientPublicKey || !user.serverPrivateKey) + throw new Error("failed to create backup key"); + const isValidClientProff = await srpCheckClientProof( + user.salt, + user.verifier, + user.serverPrivateKey, + user.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, { + encryptedPrivateKey, + iv, + tag, + salt, + verifier + }); + + await authDal.updateUserEncryptionByUserId( + user.id, + { + serverPrivateKey: null, + clientPublicKey: null + }, + tx + ); + return backupKey; + }); + + return backup; + }; + + /* + * Return user back up + * */ + const getBackupPrivateKeyOfUser = async (userId: string) => { + const user = await authDal.getUserEncKeyByUserId(userId); + if (!user || (user && !user.isAccepted)) { + throw new Error("Failed to find user"); + } + const backupKey = await authDal.getBackupPrivateKeyByUserId(userId); + if (!backupKey) throw new Error("Failed to find user backup key"); + + return backupKey; + }; + + return { + generateServerPubKey, + changePassword, + resetPasswordByBackupKey, + sendPasswordResetEmail, + verifyPasswordResetEmail, + createBackupPrivateKey, + getBackupPrivateKeyOfUser + }; +}; diff --git a/backend-pg/src/services/auth/auth-password-type.ts b/backend-pg/src/services/auth/auth-password-type.ts new file mode 100644 index 000000000..cf2aac08d --- /dev/null +++ b/backend-pg/src/services/auth/auth-password-type.ts @@ -0,0 +1,35 @@ +export type TChangePasswordDTO = { + userId: string; + clientProof: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + tokenVersionId?: string; +}; + +export type TResetPasswordViaBackupKeyDTO = { + userId: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; +}; + +export type TCreateBackupPrivateKeyDTO = { + userId: string; + clientProof: string; + encryptedPrivateKey: string; + salt: string; + iv: string; + tag: string; + verifier: string; +}; diff --git a/backend-pg/src/services/auth/auth-signup-service.ts b/backend-pg/src/services/auth/auth-signup-service.ts new file mode 100644 index 000000000..ef2fafa3b --- /dev/null +++ b/backend-pg/src/services/auth/auth-signup-service.ts @@ -0,0 +1,169 @@ +import { AuthMethod } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; +import { isDisposableEmail } from "@app/lib/validator"; + +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TTokenServiceFactory } from "../token/token-service"; +import { TokenType } from "../token/token-types"; +import { TAuthDalFactory } from "./auth-dal"; +import { AuthTokenType, TCompleteAccountSignupDTO } from "./auth-signup-type"; + +type TAuthSignupDep = { + authDal: TAuthDalFactory; + tokenService: TTokenServiceFactory; + smtpService: TSmtpService; +}; + +export type TAuthSignupFactory = ReturnType; +export const authSignupServiceFactory = ({ + authDal, + tokenService, + smtpService +}: TAuthSignupDep) => { + // first step of signup. create user and send email + const beginEmailSignupProcess = async (email: string) => { + const isEmailInvalid = await isDisposableEmail(email); + if (isEmailInvalid) { + throw new Error("Provided a disposable email"); + } + + let user = await authDal.getUserByEmail(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] }); + } + if (!user) throw new Error("Failed to create user"); + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_CONFIRMATION, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.EmailVerification, + subjectLine: "Infisical confirmation code", + recipients: [email], + substitutions: { + code: token + } + }); + }; + + const verifyEmailSignup = async (email: string, code: string) => { + const user = await authDal.getUserByEmail(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"); + } + const appCfg = getConfig(); + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_CONFIRMATION, + userId: user.id, + code + }); + + // generate jwt token this is a temporary token + const jwtToken = tokenService.createJwtToken( + { + authTokenType: AuthTokenType.SIGNUP_TOKEN, + userId: user.id.toString() + }, + appCfg.JWT_AUTH_SECRET, + { expiresIn: appCfg.JWT_SIGNUP_LIFETIME } + ); + + return { user, token: jwtToken }; + }; + + const completeEmailAccountSignup = async ({ + email, + firstName, + lastName, + // providerAuthToken, + salt, + verifier, + publicKey, + protectedKey, + protectedKeyIV, + protectedKeyTag, + // organizationName, + // attributionSource, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + ip, + userAgent + }: TCompleteAccountSignupDTO) => { + const user = await authDal.getUserByEmail(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 + ); + if (!us) throw new Error("User not found"); + const userEncKey = await authDal.upsertUserEncryptionKey( + us.id, + { + salt, + verifier, + publicKey, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }, + tx + ); + return { info: us, key: userEncKey }; + }); + + // TODO(akhilmhdh-pg): add default org memberships + 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 = tokenService.createJwtToken( + { + authTokenType: AuthTokenType.ACCESS_TOKEN, + userId: updateduser.info.id, + tokenVersionId: tokenSession.id, + accessVersion: tokenSession.accessVersion + }, + appCfg.JWT_AUTH_SECRET, + { expiresIn: appCfg.JWT_SIGNUP_LIFETIME } + ); + + const refreshToken = tokenService.createJwtToken( + { + authTokenType: AuthTokenType.REFRESH_TOKEN, + userId: updateduser.info.id, + tokenVersionId: tokenSession.id, + refreshVersion: tokenSession.refreshVersion + }, + appCfg.JWT_AUTH_SECRET, + { expiresIn: appCfg.JWT_SIGNUP_LIFETIME } + ); + + return { user: updateduser.info, accessToken, refreshToken }; + }; + + return { + beginEmailSignupProcess, + verifyEmailSignup, + completeEmailAccountSignup + }; +}; diff --git a/backend-pg/src/services/auth/auth-signup-type.ts b/backend-pg/src/services/auth/auth-signup-type.ts new file mode 100644 index 000000000..ddfa15336 --- /dev/null +++ b/backend-pg/src/services/auth/auth-signup-type.ts @@ -0,0 +1,38 @@ +export enum AuthTokenType { + ACCESS_TOKEN = "accessToken", + REFRESH_TOKEN = "refreshToken", + SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim + MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim + PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim + API_KEY = "apiKey", + SERVICE_ACCESS_TOKEN = "serviceAccessToken", + SERVICE_REFRESH_TOKEN = "serviceRefreshToken" +} + +export enum AuthMode { + JWT = "jwt", + SERVICE_TOKEN = "serviceToken", + SERVICE_ACCESS_TOKEN = "serviceAccessToken", + API_KEY = "apiKey", + API_KEY_V2 = "apiKeyV2" +} + +export type TCompleteAccountSignupDTO = { + email: string; + firstName: string; + lastName?: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + organizationName: string; + providerAuthToken?: string | null; + attributionSource?: string | undefined; + ip: string; + userAgent: string; +}; diff --git a/backend-pg/src/services/smtp/smtp-service.ts b/backend-pg/src/services/smtp/smtp-service.ts new file mode 100644 index 000000000..a433c2025 --- /dev/null +++ b/backend-pg/src/services/smtp/smtp-service.ts @@ -0,0 +1,78 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import handlebars from "handlebars"; +import { createTransport } from "nodemailer"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; + +import { logger } from "@app/lib/logger"; + +export type TSmtpConfig = SMTPTransport.Options; +export type TSmtpSendMail = { + template: SmtpTemplates; + subjectLine: string; + recipients: string[]; + substitutions: unknown; +}; +export type TSmtpService = ReturnType; + +export enum SmtpTemplates { + EmailVerification = "emailVerification.handlebars", + EmailMfa = "emailMfa.handlebars", + HistoricalSecretList = "historicalSecretLeakIncident.handlebars", + NewDeviceJoin = "newDevice.handlebars", + OrgInvite = "organizationInvitation.handlebars", + ResetPassword = "passwordReset.handlebars", + SecretLeakIncident = "secretLeakIncident.handlebars", + WorkspaceInvite = "workspaceInvitation.handlebars" +} + +export enum SmtpHost { + Sendgrid = "smtp.sendgrid.net", + Mailgun = "smtp.mailgun.org", + SocketLabs = "smtp.sockerlabs.com", + Zohomail = "smtp.zoho.com", + Gmail = "smtp.gmail.com", + Office365 = "smtp.office365.com" +} + +const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => { + if (!secure) return { secure: false }; + if (!host) return { secure: true }; + + if (host === SmtpHost.Sendgrid) { + return { requireTLS: true }; + } + if (host.includes("amazonaws.com")) { + return { tls: { ciphers: "TLSv1.2" } }; + } + return { requireTLS: true, tls: { ciphers: "TLSv1.2" } }; +}; + +export const smtpServiceFactory = (cfg: TSmtpConfig) => { + const smtp = createTransport({ ...cfg, ...getTlsOption(cfg.host, cfg.secure) }); + const isSmtpOn = Boolean(cfg.host); + + const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { + const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8"); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); + if (isSmtpOn) { + await smtp.sendMail({ + from: cfg.from, + to: recipients.join(", "), + subject: subjectLine, + html: htmlToSend + }); + } else { + logger.info("SMTP is not configured. Outputting it in terminal"); + logger.info({ + from: cfg.from, + to: recipients.join(", "), + subject: subjectLine, + html: htmlToSend + }); + } + }; + + return { sendMail }; +}; diff --git a/backend-pg/src/services/smtp/templates/emailMfa.handlebars b/backend-pg/src/services/smtp/templates/emailMfa.handlebars new file mode 100644 index 000000000..489c9dd30 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/emailMfa.handlebars @@ -0,0 +1,19 @@ + + + + + + + MFA Code + + + +

Infisical

+

Sign in attempt requires further verification

+

Your MFA code is below — enter it where you started signing in to Infisical.

+

{{code}}

+

The MFA code will be valid for 2 minutes.

+

Not you? Contact Infisical or your administrator immediately.

+ + + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/emailVerification.handlebars b/backend-pg/src/services/smtp/templates/emailVerification.handlebars new file mode 100644 index 000000000..fc738d202 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/emailVerification.handlebars @@ -0,0 +1,17 @@ + + + + + + + Code + + + +

Confirm your email address

+

Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.

+

{{code}}

+

Questions about setting up Infisical? Email us at support@infisical.com

+ + + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/historicalSecretLeakIncident.handlebars b/backend-pg/src/services/smtp/templates/historicalSecretLeakIncident.handlebars new file mode 100644 index 000000000..3cb517a57 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/historicalSecretLeakIncident.handlebars @@ -0,0 +1,21 @@ + + + + + + + Incident alert: secrets potentially leaked + + + +

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

+

View leaked secrets

+ +

If these are production secrets, please rotate them immediately.

+ +

Once you have taken action, be sure to update the status of the risk in your Infisical + dashboard.

+ + + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/newDevice.handlebars b/backend-pg/src/services/smtp/templates/newDevice.handlebars new file mode 100644 index 000000000..654bb1ba3 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/newDevice.handlebars @@ -0,0 +1,19 @@ + + + + + + + Successful login for {{email}} from new device + + + +

Infisical

+

We're verifying a recent login for {{email}}:

+

Timestamp: {{timestamp}}

+

IP address: {{ip}}

+

User agent: {{userAgent}}

+

If you believe that this login is suspicious, please contact Infisical or reset your password immediately.

+ + + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/organizationInvitation.handlebars b/backend-pg/src/services/smtp/templates/organizationInvitation.handlebars new file mode 100644 index 000000000..b281786f4 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/organizationInvitation.handlebars @@ -0,0 +1,16 @@ + + + + + + + Organization Invitation + + +

Join your organization on Infisical

+

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}

+ Join now +

What is Infisical?

+

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/passwordReset.handlebars b/backend-pg/src/services/smtp/templates/passwordReset.handlebars new file mode 100644 index 000000000..3b136e859 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/passwordReset.handlebars @@ -0,0 +1,14 @@ + + + + + + Account Recovery + + +

Reset your password

+

Someone requested a password reset.

+ Reset password +

If you didn't initiate this request, please contact us immediately at team@infisical.com

+ + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/secretLeakIncident.handlebars b/backend-pg/src/services/smtp/templates/secretLeakIncident.handlebars new file mode 100644 index 000000000..1bf2d3175 --- /dev/null +++ b/backend-pg/src/services/smtp/templates/secretLeakIncident.handlebars @@ -0,0 +1,25 @@ + + + + + + + Incident alert: secret leaked + + + +

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

+

View leaked secrets

+

You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed + by {{pusher_name}} ({{pusher_email}}). If + these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment + in the given programming. This will prevent future notifications from being sent out for those secret(s).

+ +

If these are production secrets, please rotate them immediately.

+ +

Once you have taken action, be sure to update the status of the risk in your Infisical + dashboard.

+ + + \ No newline at end of file diff --git a/backend-pg/src/services/smtp/templates/workspaceInvitation.handlebars b/backend-pg/src/services/smtp/templates/workspaceInvitation.handlebars new file mode 100644 index 000000000..60556555c --- /dev/null +++ b/backend-pg/src/services/smtp/templates/workspaceInvitation.handlebars @@ -0,0 +1,15 @@ + + + + + + Project Invitation + + +

Join your team on Infisical

+

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}

+ Join now +

What is Infisical?

+

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + \ No newline at end of file diff --git a/backend-pg/src/services/token/token-dal.ts b/backend-pg/src/services/token/token-dal.ts new file mode 100644 index 000000000..823508610 --- /dev/null +++ b/backend-pg/src/services/token/token-dal.ts @@ -0,0 +1,98 @@ +import { TDbClient } from "@app/db"; +import { TableName, TToken } from "@app/db/schemas"; +import { TTokenSession } from "@app/db/schemas/token-session"; + +import { + TDeleteTokenForUserDalDTO, + TGetTokenForUserDalDTO, + TUpsertTokenForUserDalDTO +} from "./token-types"; + +export type TTokenDalConfig = {}; + +export type TTokenDalFactory = ReturnType; + +export const tokenDalFactory = (db: TDbClient) => { + const upsertTokenForUser = async ({ + tokenHash, + expiresAt, + userId, + type, + triesLeft + }: TUpsertTokenForUserDalDTO): Promise => { + const token = await db.transaction(async (tx) => { + await tx(TableName.AuthTokens).where({ userId, type }).delete().returning("*"); + const [newToken] = await tx(TableName.AuthTokens) + .insert({ tokenHash, expiresAt: expiresAt.toUTCString(), type, userId, triesLeft }) + .returning("*"); + return newToken; + }); + return token; + }; + + const getTokenForUser = async ({ + userId, + type + }: TGetTokenForUserDalDTO): Promise => + db(TableName.AuthTokens).where({ userId, type }).first(); + + const deleteTokenForUser = async ({ + userId, + type + }: TDeleteTokenForUserDalDTO): Promise => + db(TableName.AuthTokens).where({ userId, type }).delete().returning("*"); + + const decrementTriesField = async ({ + userId, + type + }: TDeleteTokenForUserDalDTO): Promise => { + await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1); + }; + + const getTokenSession = async ( + userId: string, + ip: string, + userAgent: string + ): Promise => + db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first(); + + const insertTokenSession = async ( + userId: string, + ip: string, + userAgent: string + ): Promise => { + const [session] = await db(TableName.AuthTokenSession) + .insert({ + userId, + ip, + userAgent, + accessVersion: 1, + refreshVersion: 1, + lastUsed: new Date().toUTCString() + }) + .returning("*"); + return session; + }; + + const incrementVersion = async ( + userId: string, + sessionId: string + ): Promise => { + const [session] = await db(TableName.AuthTokenSession) + .where({ userId, id: sessionId }) + .increment("accessVersion", 1) + .increment("refreshVersion", 1) + .returning("*"); + return session; + }; + + return { + upsertTokenForUser, + getTokenForUser, + deleteTokenForUser, + decrementTriesField, + getTokenSession, + insertTokenSession, + incrementVersion + }; +}; diff --git a/backend-pg/src/services/token/token-service.ts b/backend-pg/src/services/token/token-service.ts new file mode 100644 index 000000000..1b7c80c43 --- /dev/null +++ b/backend-pg/src/services/token/token-service.ts @@ -0,0 +1,134 @@ +import crypto from "node:crypto"; +import bcrypt from "bcrypt"; +import jwt, { SignOptions } from "jsonwebtoken"; + +import { TToken } from "@app/db/schemas"; +import { TTokenSession } from "@app/db/schemas/token-session"; +import { getConfig } from "@app/lib/config/env"; + +import { TTokenDalFactory } from "./token-dal"; +import { + TCreateTokenForUserDTO, + TIssueAuthTokenDTO, + TokenType, + TValidateTokenForUserDTO +} from "./token-types"; + +type TTokenServiceFactoryDep = { + tokenDal: TTokenDalFactory; + // adjust the expiry from env through here +}; +export type TTokenServiceFactory = ReturnType; + +export const getTokenConfig = (tokenType: TokenType) => { + // generate random token based on specified token use-case + // type [type] + switch (tokenType) { + case TokenType.TOKEN_EMAIL_CONFIRMATION: { + // generate random 6-digit code + const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1)); + const expiresAt = new Date(new Date().getTime() + 86400000); + return { token, expiresAt }; + } + case TokenType.TOKEN_EMAIL_MFA: { + // generate random 6-digit code + const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1)); + const triesLeft = 5; + const expiresAt = new Date(new Date().getTime() + 300000); + return { token, triesLeft, expiresAt }; + } + case TokenType.TOKEN_EMAIL_ORG_INVITATION: { + // generate random hex + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(new Date().getTime() + 259200000); + return { token, expiresAt }; + } + case TokenType.TOKEN_EMAIL_PASSWORD_RESET: { + // generate random hex + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(new Date().getTime() + 86400000); + return { token, expiresAt }; + } + default: { + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(); + return { token, expiresAt }; + } + } +}; + +export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => { + const createTokenForUser = async ({ type, userId }: 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 + }); + return token; + }; + + const validateTokenForUser = async ({ + type, + userId, + code + }: TValidateTokenForUserDTO): Promise => { + const token = await tokenDal.getTokenForUser({ type, userId }); + // 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 }); + throw new Error("Token expired. Please try again"); + } + + const isValidToken = await bcrypt.compare(code, token.tokenHash); + if (!isValidToken) { + if (token?.triesLeft) { + if (token.triesLeft === 1) { + await tokenDal.deleteTokenForUser({ type, userId }); + } else { + await tokenDal.decrementTriesField({ type, userId }); + } + } + throw new Error("Invalid token"); + } + + const deletedToken = await tokenDal.deleteTokenForUser({ type, userId }); + return deletedToken?.[0]; + }; + + const getUserTokenSession = async ({ + userId, + ip, + userAgent + }: TIssueAuthTokenDTO): Promise => { + let session = await tokenDal.getTokenSession(userId, ip, userAgent); + if (!session) { + session = await tokenDal.insertTokenSession(userId, ip, userAgent); + } + return session; + }; + + const clearTokenSessionById = async ( + userId: string, + sessionId: string + ): Promise => tokenDal.incrementVersion(userId, sessionId); + + const createJwtToken = ( + payload: string | Buffer | object, + secret: string, + options?: SignOptions + ) => jwt.sign(payload, secret, options); + + return { + createTokenForUser, + validateTokenForUser, + createJwtToken, + getUserTokenSession, + clearTokenSessionById + }; +}; diff --git a/backend-pg/src/services/token/token-types.ts b/backend-pg/src/services/token/token-types.ts new file mode 100644 index 000000000..454dd7f76 --- /dev/null +++ b/backend-pg/src/services/token/token-types.ts @@ -0,0 +1,41 @@ +export enum TokenType { + TOKEN_EMAIL_CONFIRMATION = "emailConfirmation", + TOKEN_EMAIL_MFA = "emailMfa", + TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", + TOKEN_EMAIL_PASSWORD_RESET = "passwordReset" +} + +export type TCreateTokenForUserDTO = { + type: TokenType; + userId: string; +}; + +export type TValidateTokenForUserDTO = { + type: TokenType; + code: string; + userId: string; +}; + +export type TUpsertTokenForUserDalDTO = { + type: TokenType; + expiresAt: Date; + userId: string; + tokenHash: string; + triesLeft?: number; +}; + +export type TGetTokenForUserDalDTO = { + userId: string; + type: TokenType; +}; + +export type TDeleteTokenForUserDalDTO = { + userId: string; + type: TokenType; +}; + +export type TIssueAuthTokenDTO = { + userId: string; + ip: string; + userAgent: string; +}; diff --git a/docker-compose.pg.yml b/docker-compose.pg.yml new file mode 100644 index 000000000..8f8fe9cc4 --- /dev/null +++ b/docker-compose.pg.yml @@ -0,0 +1,58 @@ +version: "3.9" + +services: + nginx: + container_name: infisical-dev-nginx + image: nginx + restart: always + ports: + - 8080:80 + volumes: + - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + - frontend + + db: + image: postgres:14-alpine + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: infisical + POSTGRES_USER: infisical + POSTGRES_DB: infisical + + backend: + build: + context: ./backend-pg + dockerfile: Dockerfile.dev + depends_on: + - db + env_file: + - .env + environment: + - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + volumes: + - ./backend-pg/src:/app/src + + frontend: + container_name: infisical-dev-frontend + restart: unless-stopped + depends_on: + - backend + build: + context: ./frontend + dockerfile: Dockerfile.dev + volumes: + - ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files + - ./frontend/public:/app/public + env_file: .env + environment: + - NEXT_PUBLIC_ENV=development + - INFISICAL_TELEMETRY_ENABLED=false + +volumes: + postgres-data: + driver: local diff --git a/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx b/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx index 291765e3b..aab7e4c56 100644 --- a/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx +++ b/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx @@ -72,9 +72,9 @@ export const ServerConfigProvider = ({ children }: Props): JSX.Element => { export const useServerConfig = () => { const ctx = useContext(ServerConfigContext); - if (!ctx) { - throw new Error("useServerConfig has to be used within "); - } + // if (!ctx) { + // throw new Error("useServerConfig has to be used within "); + // } - return ctx; + return ctx || { config: { allowSignUp: true } }; }; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index a26297730..697fe69eb 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -121,7 +121,7 @@ export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { export const useSendVerificationEmail = () => { return useMutation({ mutationFn: async ({ email }: { email: string }) => { - const { data } = await apiRequest.post("/api/v1/signup/email/signup", { + const { data } = await apiRequest.post("/api/v3/signup/email/signup", { email }); @@ -133,7 +133,7 @@ export const useSendVerificationEmail = () => { export const useVerifyEmailVerificationCode = () => { return useMutation({ mutationFn: async ({ email, code }: { email: string; code: string }) => { - const { data } = await apiRequest.post("/api/v1/signup/email/verify", { + const { data } = await apiRequest.post("/api/v3/signup/email/verify", { email, code }); diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 82829e410..715e7b9c0 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -82,14 +82,13 @@ 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 ( - - - - - + + + );