diff --git a/.env.example b/.env.example index 2025622dc0..3871841c08 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,7 @@ SITE_URL=http://localhost:8080 # Required to send emails # By default, SMTP_HOST is set to smtp.gmail.com SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 SMTP_NAME=Team SMTP_USERNAME=team@infisical.com SMTP_PASSWORD= diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..0b3d59a189 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ + +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..fa81e63deb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "none", + "singleQuote": true, + "printWidth": 80, + "useTabs": false +} diff --git a/README.md b/README.md index e4f5999099..bd0442c938 100644 --- a/README.md +++ b/README.md @@ -311,4 +311,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub - + diff --git a/backend/.eslintrc b/backend/.eslintrc index a0c373ecb7..7fe1989012 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -1,18 +1,13 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "no-console": 2, - "prettier/prettier": 2 - } -} \ No newline at end of file + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "no-console": 2 + } +} diff --git a/backend/.prettierrc b/backend/.prettierrc deleted file mode 100644 index a5a98113eb..0000000000 --- a/backend/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "trailingComma": "none", - "singleQuote": true, - "printWidth": 80, - "useTabs": true - } diff --git a/backend/package-lock.json b/backend/package-lock.json index 19a42c3a10..d4c3d36a6b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -28,7 +28,7 @@ "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "rimraf": "^3.0.2", "stripe": "^10.7.0", "tweetnacl": "^1.0.3", @@ -50,7 +50,6 @@ "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.1", "install": "^0.13.0", "jest": "^29.3.1", "nodemon": "^2.0.19", @@ -2594,19 +2593,6 @@ "@maxmind/geoip2-node": "^3.4.0" } }, - "node_modules/@sentry/core": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz", - "integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==", - "dependencies": { - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@sentry/node": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -2704,26 +2690,6 @@ "node": ">=8" } }, - "node_modules/@sentry/types": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz", - "integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz", - "integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==", - "dependencies": { - "@sentry/types": "7.17.4", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -4035,9 +4001,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -5146,21 +5112,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", - "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9703,11 +9654,11 @@ } }, "node_modules/query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "dependencies": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -13113,16 +13064,6 @@ "@maxmind/geoip2-node": "^3.4.0" } }, - "@sentry/core": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz", - "integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==", - "requires": { - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", - "tslib": "^1.9.3" - } - }, "@sentry/node": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -13200,20 +13141,6 @@ } } }, - "@sentry/types": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz", - "integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==" - }, - "@sentry/utils": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz", - "integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==", - "requires": { - "@sentry/types": "7.17.4", - "tslib": "^1.9.3" - } - }, "@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -14219,9 +14146,9 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "dedent": { "version": "0.7.0", @@ -15034,12 +14961,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "husky": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", - "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", - "dev": true - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -18324,11 +18245,11 @@ } }, "query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "requires": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" diff --git a/backend/package.json b/backend/package.json index 73cf9311f4..7e0dc7ad9b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,7 @@ "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "rimraf": "^3.0.2", "stripe": "^10.7.0", "tweetnacl": "^1.0.3", @@ -35,7 +35,8 @@ "build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build", "lint": "eslint . --ext .ts", "lint-and-fix": "eslint . --ext .ts --fix", - "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write" + "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", + "lint-staged": "lint-staged" }, "repository": { "type": "git", @@ -63,7 +64,6 @@ "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.1", "install": "^0.13.0", "jest": "^29.3.1", "nodemon": "^2.0.19", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index fd8f9c55b4..5575ceb44a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -13,12 +13,15 @@ const NODE_ENV = process.env.NODE_ENV! || 'production'; const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!; const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; -const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; +const POSTHOG_PROJECT_API_KEY = + process.env.POSTHOG_PROJECT_API_KEY! || + 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const PUBLIC_KEY = process.env.PUBLIC_KEY!; const SENTRY_DSN = process.env.SENTRY_DSN!; const SITE_URL = process.env.SITE_URL!; const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com'; +const SMTP_PORT = process.env.SMTP_PORT! || 587; const SMTP_NAME = process.env.SMTP_NAME!; const SMTP_USERNAME = process.env.SMTP_USERNAME!; const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; @@ -28,38 +31,39 @@ const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!; const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; -const TELEMETRY_ENABLED = (process.env.TELEMETRY_ENABLED! !== 'false') && true; +const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; export { - PORT, - EMAIL_TOKEN_LIFETIME, - ENCRYPTION_KEY, - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET, - JWT_SERVICE_SECRET, - JWT_SIGNUP_LIFETIME, - JWT_SIGNUP_SECRET, - MONGO_URL, - NODE_ENV, - OAUTH_CLIENT_SECRET_HEROKU, - OAUTH_TOKEN_URL_HEROKU, - POSTHOG_HOST, - POSTHOG_PROJECT_API_KEY, - PRIVATE_KEY, - PUBLIC_KEY, - SENTRY_DSN, - SITE_URL, - SMTP_HOST, - SMTP_NAME, - SMTP_USERNAME, - SMTP_PASSWORD, - STRIPE_PRODUCT_CARD_AUTH, - STRIPE_PRODUCT_PRO, - STRIPE_PRODUCT_STARTER, - STRIPE_PUBLISHABLE_KEY, - STRIPE_SECRET_KEY, - STRIPE_WEBHOOK_SECRET, - TELEMETRY_ENABLED + PORT, + EMAIL_TOKEN_LIFETIME, + ENCRYPTION_KEY, + JWT_AUTH_LIFETIME, + JWT_AUTH_SECRET, + JWT_REFRESH_LIFETIME, + JWT_REFRESH_SECRET, + JWT_SERVICE_SECRET, + JWT_SIGNUP_LIFETIME, + JWT_SIGNUP_SECRET, + MONGO_URL, + NODE_ENV, + OAUTH_CLIENT_SECRET_HEROKU, + OAUTH_TOKEN_URL_HEROKU, + POSTHOG_HOST, + POSTHOG_PROJECT_API_KEY, + PRIVATE_KEY, + PUBLIC_KEY, + SENTRY_DSN, + SITE_URL, + SMTP_HOST, + SMTP_PORT, + SMTP_NAME, + SMTP_USERNAME, + SMTP_PASSWORD, + STRIPE_PRODUCT_CARD_AUTH, + STRIPE_PRODUCT_PRO, + STRIPE_PRODUCT_STARTER, + STRIPE_PUBLISHABLE_KEY, + STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET, + TELEMETRY_ENABLED }; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 9fbd58e935..eb537f2b98 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; @@ -5,17 +6,17 @@ import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); import { User } from '../models'; import { createToken, issueTokens, clearTokens } from '../helpers/auth'; -import { - NODE_ENV, - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_SECRET +import { + NODE_ENV, + JWT_AUTH_LIFETIME, + JWT_AUTH_SECRET, + JWT_REFRESH_SECRET } from '../config'; declare module 'jsonwebtoken' { - export interface UserIDJwtPayload extends jwt.JwtPayload { - userId: string; - } + export interface UserIDJwtPayload extends jwt.JwtPayload { + userId: string; + } } const clientPublicKeys: any = {}; @@ -27,47 +28,45 @@ const clientPublicKeys: any = {}; * @returns */ export const login1 = async (req: Request, res: Response) => { - try { - const { - email, - clientPublicKey - }: { email: string; clientPublicKey: string } = req.body; - - const user = await User.findOne({ - email - }).select('+salt +verifier'); - + try { + const { + email, + clientPublicKey + }: { email: string; clientPublicKey: string } = req.body; - if (!user) throw new Error('Failed to find user'); + const user = await User.findOne({ + email + }).select('+salt +verifier'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier - }, - () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); - clientPublicKeys[email] = { - clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt) - }; - + if (!user) throw new Error('Failed to find user'); - return res.status(200).send({ - serverPublicKey, - salt: user.salt - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to start authentication process' - }); - } + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + clientPublicKeys[email] = { + clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }; + + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to start authentication process' + }); + } }; /** @@ -78,59 +77,59 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - try { - const { email, clientProof } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); + try { + const { email, clientProof } = req.body; + const user = await User.findOne({ + email + }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: clientPublicKeys[email].serverBInt - }, - async () => { - server.setClientPublicKey(clientPublicKeys[email].clientPublicKey); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: clientPublicKeys[email].serverBInt + }, + async () => { + server.setClientPublicKey(clientPublicKeys[email].clientPublicKey); - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { - // issue tokens - const tokens = await issueTokens({ userId: user._id.toString() }); - - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/token', - sameSite: "strict", - secure: NODE_ENV === 'production' ? true : false - }); + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + // issue tokens + const tokens = await issueTokens({ userId: user._id.toString() }); - // return (access) token in response - return res.status(200).send({ - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag - }); - } + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/token', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } + // return (access) token in response + return res.status(200).send({ + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + }); + } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } }; /** @@ -140,29 +139,29 @@ export const login2 = async (req: Request, res: Response) => { * @returns */ export const logout = async (req: Request, res: Response) => { - try { - await clearTokens({ - userId: req.user._id.toString() - }); - - // clear httpOnly cookie - res.cookie('jid', '', { - httpOnly: true, - path: '/token', - sameSite: "strict", - secure: NODE_ENV === 'production' ? true : false - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to logout' - }); - } + try { + await clearTokens({ + userId: req.user._id.toString() + }); - return res.status(200).send({ - message: 'Successfully logged out.' - }); + // clear httpOnly cookie + res.cookie('jid', '', { + httpOnly: true, + path: '/token', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to logout' + }); + } + + return res.status(200).send({ + message: 'Successfully logged out.' + }); }; /** @@ -172,9 +171,9 @@ export const logout = async (req: Request, res: Response) => { * @returns */ export const checkAuth = async (req: Request, res: Response) => - res.status(200).send({ - message: 'Authenticated' - }); + res.status(200).send({ + message: 'Authenticated' + }); /** * Return new token by redeeming refresh token @@ -183,42 +182,41 @@ export const checkAuth = async (req: Request, res: Response) => * @returns */ export const getNewToken = async (req: Request, res: Response) => { - try { - const refreshToken = req.cookies.jid; - - if (!refreshToken) { - throw new Error('Failed to find token in request cookies'); - } - - const decodedToken = ( - jwt.verify(refreshToken, JWT_REFRESH_SECRET) - ); - - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + try { + const refreshToken = req.cookies.jid; - if (!user) throw new Error('Failed to authenticate unfound user'); - if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); - - const token = createToken({ - payload: { - userId: decodedToken.userId - }, - expiresIn: JWT_AUTH_LIFETIME, - secret: JWT_AUTH_SECRET - }); - - return res.status(200).send({ - token - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Invalid request' - }); - } + if (!refreshToken) { + throw new Error('Failed to find token in request cookies'); + } + + const decodedToken = ( + jwt.verify(refreshToken, JWT_REFRESH_SECRET) + ); + + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); + + if (!user) throw new Error('Failed to authenticate unfound user'); + if (!user?.publicKey) + throw new Error('Failed to authenticate not fully set up account'); + + const token = createToken({ + payload: { + userId: decodedToken.userId + }, + expiresIn: JWT_AUTH_LIFETIME, + secret: JWT_AUTH_SECRET + }); + + return res.status(200).send({ + token + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Invalid request' + }); + } }; diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/membershipOrgController.ts index bc28049966..a2159bcd05 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/membershipOrgController.ts @@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { try { const { email, code } = req.body; - user = await User.findOne({ email }); + user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ @@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - error: 'Failed email magic link confirmation' + error: 'Failed email magic link verification for organization invitation' }); } diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/passwordController.ts index 86b5355dbc..0109dc6a51 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/passwordController.ts @@ -1,11 +1,121 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; -import { User, BackupPrivateKey } from '../models'; +import { User, Token, BackupPrivateKey } from '../models'; +import { checkEmailVerification } from '../helpers/signup'; +import { createToken } from '../helpers/auth'; +import { sendMail } from '../helpers/nodemailer'; +import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; const clientPublicKeys: any = {}; +/** + * Password reset step 1: Send email verification link to email [email] + * for account recovery. + * @param req + * @param res + * @returns + */ +export const emailPasswordReset = async (req: Request, res: Response) => { + let email: string; + try { + email = req.body.email; + + const user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user has already completed account + + return res.status(403).send({ + error: 'Failed to send email verification for password reset' + }); + } + + const token = crypto.randomBytes(16).toString('hex'); + + await Token.findOneAndUpdate( + { email }, + { + email, + token, + createdAt: new Date() + }, + { upsert: true, new: true } + ); + + await sendMail({ + template: 'passwordReset.handlebars', + subjectLine: 'Infisical password reset', + recipients: [email], + substitutions: { + email, + token, + callback_url: SITE_URL + '/password-reset' + } + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to send email for account recovery' + }); + } + + return res.status(200).send({ + message: `Sent an email for account recovery to ${email}` + }); +} + +/** + * Password reset step 2: Verify email verification link sent to email [email] + * @param req + * @param res + * @returns + */ +export const emailPasswordResetVerify = async (req: Request, res: Response) => { + let user, token; + try { + const { email, code } = req.body; + + user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user doesn't exist with email [email] or + // hasn't even completed their account + return res.status(403).send({ + error: 'Failed email verification for password reset' + }); + } + + await checkEmailVerification({ + email, + code + }); + + // generate temporary password-reset token + token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: JWT_SIGNUP_LIFETIME, + secret: JWT_SIGNUP_SECRET + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed email verification for password reset' + }); + } + + return res.status(200).send({ + message: 'Successfully verified email', + user, + token + }); +} + /** * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * @param req @@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to start change password process' @@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ error: 'Failed to change password. Try again?' @@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { } ); } catch (err) { - Sentry.setUser(null); + Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to update backup private key' }); } }; + +/** + * Return backup private key for user + * @param req + * @param res + * @returns + */ +export const getBackupPrivateKey = async (req: Request, res: Response) => { + let backupPrivateKey; + try { + backupPrivateKey = await BackupPrivateKey.findOne({ + user: req.user._id + }); + + if (!backupPrivateKey) throw new Error('Failed to find backup private key'); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + backupPrivateKey + }); +} + +export const resetPassword = async (req: Request, res: Response) => { + try { + const { + encryptedPrivateKey, + iv, + tag, + salt, + verifier, + } = req.body; + + await User.findByIdAndUpdate( + req.user._id.toString(), + { + encryptedPrivateKey, + iv, + tag, + salt, + verifier + }, + { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email}); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get backup private key' + }); + } + + return res.status(200).send({ + message: 'Successfully reset password' + }); +} \ No newline at end of file diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 7e70f6ac17..1c92af93f9 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -2,21 +2,40 @@ import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import nodemailer from 'nodemailer'; -import { SMTP_HOST, SMTP_NAME, SMTP_USERNAME, SMTP_PASSWORD } from '../config'; +import { + SMTP_HOST, + SMTP_PORT, + SMTP_NAME, + SMTP_USERNAME, + SMTP_PASSWORD +} from '../config'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; +import * as Sentry from '@sentry/node'; +const mailOpts: SMTPConnection.Options = { + host: SMTP_HOST, + port: SMTP_PORT as number +}; +if (SMTP_USERNAME && SMTP_PASSWORD) { + mailOpts.auth = { + user: SMTP_USERNAME, + pass: SMTP_PASSWORD + }; +} // create nodemailer transporter -const transporter = nodemailer.createTransport({ - host: SMTP_HOST, - port: 587, - auth: { - user: SMTP_USERNAME, - pass: SMTP_PASSWORD - } -}); +const transporter = nodemailer.createTransport(mailOpts); transporter - .verify() - .then(() => console.log('SMTP - Successfully connected')) - .catch((err) => console.log('SMTP - Failed to connect')); + .verify() + .then(() => { + Sentry.setUser(null); + Sentry.captureMessage('SMTP - Successfully connected'); + }) + .catch((err) => { + Sentry.setUser(null); + Sentry.captureException( + `SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}` + ); + }); /** * @param {Object} obj @@ -26,33 +45,34 @@ transporter * @param {Object} obj.substitutions - object containing template substitutions */ const sendMail = async ({ - template, - subjectLine, - recipients, - substitutions + template, + subjectLine, + recipients, + substitutions }: { - template: string; - subjectLine: string; - recipients: string[]; - substitutions: any; + template: string; + subjectLine: string; + recipients: string[]; + substitutions: any; }) => { - try { - const html = fs.readFileSync( - path.resolve(__dirname, '../templates/' + template), - 'utf8' - ); - const temp = handlebars.compile(html); - const htmlToSend = temp(substitutions); + try { + const html = fs.readFileSync( + path.resolve(__dirname, '../templates/' + template), + 'utf8' + ); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); - await transporter.sendMail({ - from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`, - to: recipients.join(', '), - subject: subjectLine, - html: htmlToSend - }); - } catch (err) { - console.error(err); - } + await transporter.sendMail({ + from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`, + to: recipients.join(', '), + subject: subjectLine, + html: htmlToSend + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + } }; export { sendMail }; diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 141bf455d5..8a201cb110 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => { // send mail await sendMail({ template: 'emailVerification.handlebars', - subjectLine: 'Infisical workspace invitation', + subjectLine: 'Infisical confirmation code', recipients: [email], substitutions: { code: token diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 43a57fe7e2..6be9c09f75 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -5,28 +5,24 @@ import { requireAuth, validateRequest } from '../middleware'; import { authController } from '../controllers'; import { loginLimiter } from '../helpers/rateLimiter'; +router.post('/token', validateRequest, authController.getNewToken); + router.post( - '/token', - validateRequest, - authController.getNewToken + '/login1', + loginLimiter, + body('email').exists().trim().notEmpty(), + body('clientPublicKey').exists().trim().notEmpty(), + validateRequest, + authController.login1 ); router.post( - '/login1', - loginLimiter, - body('email').exists().trim().notEmpty(), - body('clientPublicKey').exists().trim().notEmpty(), - validateRequest, - authController.login1 -); - -router.post( - '/login2', - loginLimiter, - body('email').exists().trim().notEmpty(), - body('clientProof').exists().trim().notEmpty(), - validateRequest, - authController.login2 + '/login2', + loginLimiter, + body('email').exists().trim().notEmpty(), + body('clientProof').exists().trim().notEmpty(), + validateRequest, + authController.login2 ); router.post('/logout', requireAuth, authController.logout); diff --git a/backend/src/routes/password.ts b/backend/src/routes/password.ts index 5d39eac283..955e532a0b 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/password.ts @@ -1,7 +1,7 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; +import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; import { passwordController } from '../controllers'; import { passwordLimiter } from '../helpers/rateLimiter'; @@ -27,6 +27,33 @@ router.post( passwordController.changePassword ); +// NEW +router.post( + '/email/password-reset', + passwordLimiter, + body('email').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordReset +); + +// NEW +router.post( + '/email/password-reset-verify', + passwordLimiter, + body('email').exists().trim().notEmpty().isEmail(), + body('code').exists().trim().notEmpty(), + validateRequest, + passwordController.emailPasswordResetVerify +); + +// NEW +router.get( + '/backup-private-key', + passwordLimiter, + requireSignupAuth, + passwordController.getBackupPrivateKey +); + router.post( '/backup-private-key', passwordLimiter, @@ -41,4 +68,17 @@ router.post( passwordController.createBackupPrivateKey ); -export default router; +// NEW +router.post( + '/password-reset', + requireSignupAuth, + body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd + body('iv').exists().trim().notEmpty(), // new iv for private key + body('tag').exists().trim().notEmpty(), // new tag for private key + body('salt').exists().trim().notEmpty(), // part of new pwd + body('verifier').exists().trim().notEmpty(), // part of new pwd + validateRequest, + passwordController.resetPassword +); + +export default router; \ No newline at end of file diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars index 6634091887..49cc96f38d 100644 --- a/backend/src/templates/organizationInvitation.handlebars +++ b/backend/src/templates/organizationInvitation.handlebars @@ -4,7 +4,7 @@ - Email Verification + Organization Invitation

Infisical

diff --git a/backend/src/templates/passwordReset.handlebars b/backend/src/templates/passwordReset.handlebars new file mode 100644 index 0000000000..1e629f6644 --- /dev/null +++ b/backend/src/templates/passwordReset.handlebars @@ -0,0 +1,15 @@ + + + + + + Account Recovery + + +

Infisical

+

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/src/templates/workspaceInvitation.handlebars b/backend/src/templates/workspaceInvitation.handlebars index 22f63efc99..252452ce58 100644 --- a/backend/src/templates/workspaceInvitation.handlebars +++ b/backend/src/templates/workspaceInvitation.handlebars @@ -3,7 +3,7 @@ - Email Verification + Project Invitation

Infisical

diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 949a96c7d6..623462d5be 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -20,6 +20,7 @@ services: restart: unless-stopped depends_on: - mongo + - smtp-server build: context: ./backend dockerfile: Dockerfile @@ -33,7 +34,7 @@ services: - NODE_ENV=development networks: - infisical-dev - + frontend: container_name: infisical-dev-frontend restart: unless-stopped @@ -84,6 +85,18 @@ services: networks: - infisical-dev + smtp-server: + container_name: infisical-dev-smtp-server + image: mailhog/mailhog + restart: always + logging: + driver: 'none' # disable saving logs + ports: + - 1025:1025 # SMTP server + - 8025:8025 # Web UI + networks: + - infisical-dev + volumes: mongo-data: driver: local diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 6267a66f0d..1442590236 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Install" +title: 'Install' --- Prerequisite: Set up an account with [Infisical Cloud](https://app.infisical.com) or via a [self-hosted installation](/self-hosting/overview). @@ -13,11 +13,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo Use [brew](https://brew.sh/) package manager ```bash - # install brew install infisical/get-cli/infisical - - # check version - infisical --version ``` ## Updates @@ -31,14 +27,13 @@ The Infisical CLI provides a way to inject environment variables from the platfo Use [Scoop](https://scoop.sh/) package manager ```bash - # install scoop bucket add org https://github.com/Infisical/scoop-infisical.git - scoop install infisical - - # check version - infisical --version ``` + ```bash + scoop install infisical + ``` + ## Updates ```bash @@ -49,33 +44,33 @@ The Infisical CLI provides a way to inject environment variables from the platfo Install prerequisite ```bash - $ sudo apk add --no-cache bash sudo + sudo apk add --no-cache bash sudo ``` Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo apk update && sudo apk add infisical + sudo apk update && sudo apk add infisical ``` Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo yum install infisical + sudo yum install infisical ``` @@ -83,14 +78,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo Add Infisical repository ```bash - $ curl -1sLf \ + curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \ | sudo -E bash ``` Then install CLI ```bash - $ sudo apt-get update && sudo apt-get install -y infisical + sudo apt-get update && sudo apt-get install -y infisical ``` diff --git a/docs/contributing/FAQ.mdx b/docs/contributing/FAQ.mdx index be65f5ca7e..9f4f7aee90 100644 --- a/docs/contributing/FAQ.mdx +++ b/docs/contributing/FAQ.mdx @@ -1,11 +1,13 @@ --- -title: "Frequently Asked Questions" -description: "Have any questions? [Join our Slack community](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g)." +title: 'Frequently Asked Questions' +description: 'Have any questions? [Join our Slack community](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g).' --- ## Problem with SMTP -You can normally populate `SMTP_USERNAME` and `SMTP_PASSWORD` with your usual login and password (you could also create a 'burner' email). Sometimes, there still are problems. +If you opt for actual SMTP server (not the local MailHog), you have to have the right environment variables set. + +You can normally populate `SMTP_USERNAME` and `SMTP_PASSWORD` with your usual login and password (you could also create a 'burner' email). Sometimes, there still are problems. You can go to your Gmail account settings > security and enable “less secure apps”. This would allow Infisical to use your Gmail to send emails. @@ -13,4 +15,4 @@ If it still doesn't work, [this](https://stackoverflow.com/questions/72547853/un ## `MONGO_URL` issues -Your `MONGO_URL` should be something like `mongodb://root:example@mongo:27017/?authSource=admin`. If you want to change it (not recommended), you should make sure that you keep this URL in line with `MONGO_USERNAME=root` and `MONGO_PASSWORD=example`. \ No newline at end of file +Your `MONGO_URL` should be something like `mongodb://root:example@mongo:27017/?authSource=admin`. If you want to change it (not recommended), you should make sure that you keep this URL in line with `MONGO_USERNAME=root` and `MONGO_PASSWORD=example`. diff --git a/docs/contributing/developing.mdx b/docs/contributing/developing.mdx index b6c25861c0..3be1a1b6bf 100644 --- a/docs/contributing/developing.mdx +++ b/docs/contributing/developing.mdx @@ -1,6 +1,6 @@ --- -title: "Developing" -description: "This guide will help you set up and run Infisical in development mode." +title: 'Developing' +description: 'This guide will help you set up and run Infisical in development mode.' --- ## Clone the repo @@ -16,17 +16,65 @@ cd infisical ## Set up environment variables -Tweak the `.env` according to your preferences. Refer to the available [environment variables](/self-hosting/configuration/envars). +Before running the docker-compose we have to generate the .env file with the environment variables, you can create your own file or start with the +`.env.example` as an example guide. + +Mandatory variables in the `.env` file: + +1. Keys and JWT variables + +![image](https://user-images.githubusercontent.com/118568289/206791534-9c9d1431-e83d-49c0-8a54-b373ed0df820.png) + +The `.env.example` has these variables empty, you can self generate the `JWT and ENCRYPTION_KEY` with this [32-byte random hex strings generator](https://www.browserling.com/tools/random-hex). + +For the `PRIVATE_KEY and PUBLIC_KEY` you can use the ones shown in the screenshot: -```bash -cp .env.example .env ``` +PRIVATE_KEY='oGVv5rThrpZ7WLgQW27chY1cXngr4wLQIZnGfSKgHPk=' +PUBLIC_KEY='ldr6JaC7AY+tun3omGLdE4SWpkJbtVBOI54KfUP53Xc=' +``` + +2. Mongo variables and site URL + +![image](https://user-images.githubusercontent.com/118568289/206792171-3376e3c6-c3ac-4d5d-8776-d78ee089b520.png) + +These variables are used to connect the MongoDB and set the URL for the localhost. + +For development, you can use `root` for the `MONGO_USERNAME` and `example` for the `MONGO_PASSWORD` as shown in the screenshot. + +Take into account that if you use your own `MONGO_USERNAME` and `MONGO_PASSWORD`, you also have to change the `MONGO_URL` with the form of `MONGO_USERNAME:MONGO_PASSWORD` after the `//` part of the URL. + +3. Mail SMTP service variables + +![image](https://user-images.githubusercontent.com/118568289/206792653-ba3211d1-1071-43f2-93a7-8b408bbd9e0e.png) + +If you want to receive actual emails (e.g. you want to test how the email message will look like), take note of the following. + +For the `SMTP_USERNAME` variable, you will need an email with 2-steps-verification. + +For the `SMTP_PASSWORD` variable, you will need to [generate an app password](https://support.google.com/mail/answer/185833?hl=en) with the email you used in the `SMTP_USERNAME` variable. + +Otherwise, a local SMTP server (MailHog) is available for testing purposes. Set the following values to use this: + +``` +SMTP_HOST=smtp-server +SMTP_PORT=1025 +SMTP_NAME= +SMTP_USERNAME=team@infisical.com +SMTP_PASSWORD= +``` + +Make sure to leave the `SMTP_PASSWORD` blank so the backend will be able to connect to MailHog + +You can browse `http://localhost:8025/` to browse email messages sent by the backend. + +With these environment variables, you will be ready to run the docker-compose. ## Docker for development ```bash # build and start the services -docker-compose -f docker-compose.dev.yml up --build +docker-compose -f docker-compose.dev.yml up --build --force-recreate ``` Then browse http://localhost:8080 diff --git a/frontend/.eslintrc b/frontend/.eslintrc index b562aaa1fc..d5f35096cf 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -9,13 +9,13 @@ "plugins": ["simple-import-sort", "@typescript-eslint"], "rules": { "react-hooks/exhaustive-deps": "off", - "no-unused-vars": "off", + "no-unused-vars": "warn", + "@typescript-eslint/ban-ts-comment": "warn", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-non-null-assertion": "off", - "simple-import-sort/exports": "warn", "simple-import-sort/imports": [ "warn", diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 222861c341..0000000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false -} diff --git a/frontend/components/RouteGuard.js b/frontend/components/RouteGuard.js index a21c0c86ab..d08972b997 100644 --- a/frontend/components/RouteGuard.js +++ b/frontend/components/RouteGuard.js @@ -1,9 +1,9 @@ -import { useEffect, useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/router"; +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; -import { publicPaths } from "~/const"; -import checkAuth from "~/pages/api/auth/CheckAuth"; +import { publicPaths } from '~/const'; +import checkAuth from '~/pages/api/auth/CheckAuth'; // #TODO: finish spinner only when the data loads fully // #TODO: Redirect somewhere if the page does not exist @@ -22,16 +22,16 @@ export default function RouteGuard({ children }) { // #TODO: add the loading page when not yet authorized. const hideContent = () => setAuthorized(false); // const onError = () => setAuthorized(true) - router.events.on("routeChangeStart", hideContent); + router.events.on('routeChangeStart', hideContent); // router.events.on("routeChangeError", onError); // on route change complete - run auth check - router.events.on("routeChangeComplete", authCheck); + router.events.on('routeChangeComplete', authCheck); // unsubscribe from events in useEffect return function return () => { - router.events.off("routeChangeStart", hideContent); - router.events.off("routeChangeComplete", authCheck); + router.events.off('routeChangeStart', hideContent); + router.events.off('routeChangeComplete', authCheck); // router.events.off("routeChangeError", onError); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -43,7 +43,7 @@ export default function RouteGuard({ children }) { */ async function authCheck(url) { // Make sure that we don't redirect when the user is on the following pages. - const path = "/" + url.split("?")[0].split("/")[1]; + const path = '/' + url.split('?')[0].split('/')[1]; // Check if the user is authenticated const response = await checkAuth(); @@ -51,16 +51,16 @@ export default function RouteGuard({ children }) { if (!publicPaths.includes(path)) { try { if (response.status !== 200) { - router.push("/login"); - console.log("Unauthorized to access."); + router.push('/login'); + console.log('Unauthorized to access.'); setAuthorized(false); } else { setAuthorized(true); - console.log("Authorized to access."); + console.log('Authorized to access.'); } } catch (error) { console.log( - "Error (probably the authCheck route is stuck again...):", + 'Error (probably the authCheck route is stuck again...):', error ); } diff --git a/frontend/components/analytics/posthog.js b/frontend/components/analytics/posthog.js index 44ee4fdb39..8b8e88a226 100644 --- a/frontend/components/analytics/posthog.js +++ b/frontend/components/analytics/posthog.js @@ -1,16 +1,13 @@ -import posthog from "posthog-js"; +import posthog from 'posthog-js'; -import { - ENV, - POSTHOG_API_KEY, - POSTHOG_HOST, -} from "../utilities/config"; +import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; export const initPostHog = () => { - if (typeof window !== "undefined") { - if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { // eslint-disable-line + if (typeof window !== 'undefined') { + // eslint-disable-next-line + if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { posthog.init(POSTHOG_API_KEY, { - api_host: POSTHOG_HOST, + api_host: POSTHOG_HOST }); } } diff --git a/frontend/components/analytics/posthog.ts b/frontend/components/analytics/posthog.ts new file mode 100644 index 0000000000..e0d6e7c09a --- /dev/null +++ b/frontend/components/analytics/posthog.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable no-undef */ +import posthog from 'posthog-js'; + +import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; + +export const initPostHog = () => { + if (typeof window !== 'undefined') { + // @ts-ignore + if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { + posthog.init(POSTHOG_API_KEY, { + api_host: POSTHOG_HOST + }); + } + } + + return posthog; +}; diff --git a/frontend/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 139304ea6a..1415782142 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; -import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from 'react'; +import { useRouter } from 'next/router'; +import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import guidGenerator from "../utilities/randomId"; +import guidGenerator from '../utilities/randomId'; interface InputFieldProps { static?: boolean; @@ -23,7 +23,7 @@ interface InputFieldProps { const InputField = ( props: InputFieldProps & - Pick + Pick ) => { const [passwordVisible, setPasswordVisible] = useState(false); const router = useRouter(); @@ -75,28 +75,28 @@ const InputField = (
props.onChangeHandler(e.target.value)} - type={passwordVisible === false ? props.type : "text"} + type={passwordVisible === false ? props.type : 'text'} placeholder={props.placeholder} value={props.value} required={props.isRequired} className={`${ props.blurred - ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400" - : "" + ? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400' + : '' } ${ - props.error ? "focus:ring-red/50" : "focus:ring-primary/50" + props.error ? 'focus:ring-red/50' : 'focus:ring-primary/50' } relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} name={props.name} spellCheck="false" autoComplete={props.autoComplete} id={props.id} /> - {props.label?.includes("Password") && ( + {props.label?.includes('Password') && (
)} - {row.status == "completed" && myUser !== row.email && ( + {row.status == 'completed' && myUser !== row.email && (
{myUser !== row.email && // row.role != "admin" && - myRole != "member" ? ( + myRole != 'member' ? (
)} - +
{plan.buttonTextSecondary}
@@ -70,9 +88,9 @@ export default function Plan({ plan }) { ) : (

CURRENT PLAN

diff --git a/frontend/components/context/Notifications/Notification.tsx b/frontend/components/context/Notifications/Notification.tsx index e473ddb52b..ad556a6f53 100644 --- a/frontend/components/context/Notifications/Notification.tsx +++ b/frontend/components/context/Notifications/Notification.tsx @@ -1,9 +1,8 @@ -import { useEffect, useRef } from "react"; -import { faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import classnames from "classnames"; +import { useEffect, useRef } from 'react'; +import { faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Notification as NotificationType } from "./NotificationProvider"; +import { Notification as NotificationType } from './NotificationProvider'; interface NotificationProps { notification: Required; @@ -12,7 +11,7 @@ interface NotificationProps { const Notification = ({ notification, - clearNotification, + clearNotification }: NotificationProps) => { const timeout = useRef(); @@ -37,22 +36,29 @@ const Notification = ({ return (
-

{notification.text}

+ {notification.type === 'error' && ( +
+ )} + {notification.type === 'success' && ( +
+ )} + {notification.type === 'info' && ( +
+ )} +

+ {notification.text} +

); diff --git a/frontend/components/context/Notifications/NotificationProvider.tsx b/frontend/components/context/Notifications/NotificationProvider.tsx index b029dc6c61..05f9eee19f 100644 --- a/frontend/components/context/Notifications/NotificationProvider.tsx +++ b/frontend/components/context/Notifications/NotificationProvider.tsx @@ -1,8 +1,8 @@ -import { createContext, ReactNode, useContext, useState } from "react"; +import { createContext, ReactNode, useContext, useState } from 'react'; -import Notifications from "./Notifications"; +import Notifications from './Notifications'; -type NotificationType = "success" | "error" | "info"; +type NotificationType = 'success' | 'error' | 'info'; export type Notification = { text: string; @@ -15,7 +15,7 @@ type NotificationContextState = { }; const NotificationContext = createContext({ - createNotification: () => console.log("createNotification not set!"), + createNotification: () => console.log('createNotification not set!') }); export const useNotificationContext = () => useContext(NotificationContext); @@ -37,8 +37,8 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => { const createNotification = ({ text, - type = "success", - timeoutMs = 2000, + type = 'success', + timeoutMs = 5000 }: Notification) => { const doesNotifExist = notifications.some((notif) => notif.text === text); @@ -54,7 +54,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => { return ( []; @@ -8,14 +8,14 @@ interface NoticationsProps { const Notifications = ({ notifications, - clearNotification, + clearNotification }: NoticationsProps) => { if (!notifications.length) { return null; } return ( -
+
{notifications.map((notif) => ( void; value: string; - type: "varName" | "value"; + type: 'varName' | 'value'; blurred: boolean; duplicates: string[]; } @@ -33,7 +33,7 @@ const DashboardInputField = ({ type, value, blurred, - duplicates, + duplicates }: DashboardInputFieldProps) => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { @@ -43,8 +43,8 @@ const DashboardInputField = ({ ref.current.scrollLeft = e.currentTarget.scrollLeft; }; - if (type === "varName") { - const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ""; + if (type === 'varName') { + const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ''; const hasDuplicates = duplicates?.includes(value); const error = startsWithNumber || hasDuplicates; @@ -52,7 +52,7 @@ const DashboardInputField = ({
@@ -79,7 +79,7 @@ const DashboardInputField = ({ )}
); - } else if (type === "value") { + } else if (type === 'value') { return (
@@ -100,8 +100,8 @@ const DashboardInputField = ({ ref={ref} className={`${ blurred - ? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400" - : "" + ? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400' + : '' } absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`} > {value.split(REGEX).map((word, id) => { @@ -112,7 +112,7 @@ const DashboardInputField = ({ {word.slice(2, word.length - 1)} - {word.slice(word.length - 1, word.length) == "}" ? ( + {word.slice(word.length - 1, word.length) == '}' ? ( {word.slice(word.length - 1, word.length)} @@ -135,7 +135,7 @@ const DashboardInputField = ({ {blurred && (
- {value.split("").map(() => ( + {value.split('').map(() => ( { const handleDragEnter = (e: DragEvent) => { e.preventDefault(); @@ -43,7 +43,7 @@ const DropZone = ({ e.stopPropagation(); // set dropEffect to copy i.e copy of the source item - e.dataTransfer.dropEffect = "copy"; + e.dataTransfer.dropEffect = 'copy'; }; const [loading, setLoading] = useState(false); @@ -54,7 +54,7 @@ const DropZone = ({ setTimeout(() => setLoading(false), 5000); e.preventDefault(); e.stopPropagation(); - e.dataTransfer.dropEffect = "copy"; + e.dataTransfer.dropEffect = 'copy'; const file = e.dataTransfer.files[0]; const reader = new FileReader(); @@ -68,7 +68,7 @@ const DropZone = ({ numCurrentRows + index, key, keyPairs[key as keyof typeof keyPairs], - "shared", + 'shared' ]); setData(newData); setButtonReady(true); @@ -94,15 +94,15 @@ const DropZone = ({ reader.onload = (event) => { if (event.target === null || event.target.result === null) return; const { result } = event.target; - if (typeof result === "string") { + if (typeof result === 'string') { const newData = result - .split("\n") + .split('\n') .map((line: string, index: number) => [ guidGenerator(), numCurrentRows + index, - line.split("=")[0], - line.split("=").slice(1, line.split("=").length).join("="), - "shared", + line.split('=')[0], + line.split('=').slice(1, line.split('=').length).join('='), + 'shared' ]); setData(newData); setButtonReady(true); diff --git a/frontend/components/navigation/NavBarDashboard.tsx b/frontend/components/navigation/NavBarDashboard.tsx index 5a6d63b8e0..1834d81176 100644 --- a/frontend/components/navigation/NavBarDashboard.tsx +++ b/frontend/components/navigation/NavBarDashboard.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react/jsx-key */ -import React, { Fragment, useEffect, useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; +import React, { Fragment, useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons'; +import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons'; import { faAngleDown, faBook, @@ -12,39 +12,39 @@ import { faEnvelope, faGear, faPlus, - faRightFromBracket, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Menu, Transition } from "@headlessui/react"; + faRightFromBracket +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Menu, Transition } from '@headlessui/react'; -import logout from "~/pages/api/auth/Logout"; -import getOrganization from "~/pages/api/organization/GetOrg"; -import getOrganizations from "~/pages/api/organization/getOrgs"; -import getUser from "~/pages/api/user/getUser"; +import logout from '~/pages/api/auth/Logout'; +import getOrganization from '~/pages/api/organization/GetOrg'; +import getOrganizations from '~/pages/api/organization/getOrgs'; +import getUser from '~/pages/api/user/getUser'; -import guidGenerator from "../utilities/randomId"; +import guidGenerator from '../utilities/randomId'; const supportOptions = [ [ , - "Join Slack Forum", - "https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g", + 'Join Slack Forum', + 'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g' ], [ , - "Read Docs", - "https://infisical.com/docs/getting-started/introduction", + 'Read Docs', + 'https://infisical.com/docs/getting-started/introduction' ], [ , - "Open a GitHub Issue", - "https://github.com/Infisical/infisical-cli/issues", + 'Open a GitHub Issue', + 'https://github.com/Infisical/infisical-cli/issues' ], [ , - "Send us an Email", - "mailto:support@infisical.com", - ], + 'Send us an Email', + 'mailto:support@infisical.com' + ] ]; export interface ICurrentOrg { @@ -58,7 +58,7 @@ export interface IUser { } /** - * This is the navigation bar in the main app. + * This is the navigation bar in the main app. * It has two main components: support options and user menu (inlcudes billing, logout, org/user settings) * @returns NavBar */ @@ -75,16 +75,16 @@ export default function Navbar() { const orgsData = await getOrganizations(); setOrgs(orgsData); const currentOrg = await getOrganization({ - orgId: String(localStorage.getItem("orgData.id")), + orgId: String(localStorage.getItem('orgData.id')) }); setCurrentOrg(currentOrg); })(); }, []); const closeApp = async () => { - console.log("Logging out..."); + console.log('Logging out...'); await logout(); - router.push("/login"); + router.push('/login'); }; return ( @@ -163,7 +163,7 @@ export default function Navbar() {
- router.push("/settings/personal/" + router.query.id) + router.push('/settings/personal/' + router.query.id) } className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md" > @@ -173,11 +173,11 @@ export default function Navbar() {

- {" "} + {' '} {user?.firstName} {user?.lastName}

- {" "} + {' '} {user?.email}

@@ -194,7 +194,7 @@ export default function Navbar() {
- router.push("/settings/org/" + router.query.id) + router.push('/settings/org/' + router.query.id) } className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md" > @@ -217,7 +217,7 @@ export default function Navbar() { >
- router.push("/settings/billing/" + router.query.id) + router.push('/settings/billing/' + router.query.id) } className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200" > @@ -235,7 +235,7 @@ export default function Navbar() {
router.push( - "/settings/org/" + router.query.id + "?invite" + '/settings/org/' + router.query.id + '?invite' ) } className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1" @@ -255,13 +255,14 @@ export default function Navbar() {
{orgs .filter( - (org : { _id: string }) => org._id != localStorage.getItem("orgData.id") + (org: { _id: string }) => + org._id != localStorage.getItem('orgData.id') ) - .map((org : { _id: string; name: string; }) => ( + .map((org: { _id: string; name: string }) => (
{ - localStorage.setItem("orgData.id", org._id); + localStorage.setItem('orgData.id', org._id); router.reload(); }} className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md" @@ -286,8 +287,8 @@ export default function Navbar() { onClick={closeApp} className={`${ active - ? "bg-red font-semibold text-white" - : "text-gray-400" + ? 'bg-red font-semibold text-white' + : 'text-gray-400' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} >
diff --git a/frontend/components/utilities/SecurityClient.js b/frontend/components/utilities/SecurityClient.js deleted file mode 100644 index 78eebdebef..0000000000 --- a/frontend/components/utilities/SecurityClient.js +++ /dev/null @@ -1,24 +0,0 @@ -import token from "~/pages/api/auth/Token"; - -export default class SecurityClient { - static #token = ""; - - constructor() {} - - static setToken(token) { - this.#token = token; - } - - static async fetchCall(resource, options) { - let req = new Request(resource, options); - - if (this.#token == "") { - this.setToken(await token()); - } - - if (this.#token) { - req.headers.set("Authorization", "Bearer " + this.#token); - return fetch(req); - } - } -} diff --git a/frontend/components/utilities/SecurityClient.ts b/frontend/components/utilities/SecurityClient.ts new file mode 100644 index 0000000000..ea2664c70b --- /dev/null +++ b/frontend/components/utilities/SecurityClient.ts @@ -0,0 +1,27 @@ +import token from '~/pages/api/auth/Token'; + +export default class SecurityClient { + static #token = ''; + + constructor() {} + + static setToken(token: string) { + this.#token = token; + } + + static async fetchCall( + resource: RequestInfo, + options?: RequestInit | undefined + ) { + const req = new Request(resource, options); + + if (this.#token == '') { + this.setToken(await token()); + } + + if (this.#token) { + req.headers.set('Authorization', 'Bearer ' + this.#token); + return fetch(req); + } + } +} diff --git a/frontend/components/utilities/attemptLogin.js b/frontend/components/utilities/attemptLogin.js index 33c08b6987..228f58727b 100644 --- a/frontend/components/utilities/attemptLogin.js +++ b/frontend/components/utilities/attemptLogin.js @@ -1,17 +1,17 @@ -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import login1 from "~/pages/api/auth/Login1"; -import login2 from "~/pages/api/auth/Login2"; -import getOrganizations from "~/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects"; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; +import login1 from '~/pages/api/auth/Login1'; +import login2 from '~/pages/api/auth/Login2'; +import getOrganizations from '~/pages/api/organization/getOrgs'; +import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; -import pushKeys from "./secrets/pushKeys"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; -import Telemetry from "./telemetry/Telemetry"; +import pushKeys from './secrets/pushKeys'; +import Telemetry from './telemetry/Telemetry'; +import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; +import SecurityClient from './SecurityClient'; -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); const client = new jsrp.client(); /** @@ -37,7 +37,7 @@ const attemptLogin = async ( client.init( { username: email, - password: password, + password: password }, async () => { const clientPublicKey = client.getPublicKey(); @@ -54,54 +54,53 @@ const attemptLogin = async ( await login2(email, clientProof); SecurityClient.setToken(token); - const privateKey = Aes256Gcm.decrypt( - encryptedPrivateKey, + const privateKey = Aes256Gcm.decrypt({ + ciphertext: encryptedPrivateKey, iv, tag, - password + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" + '0' ) - ); + }); saveTokenToLocalStorage({ - token, publicKey, encryptedPrivateKey, iv, tag, - privateKey, + privateKey }); const userOrgs = await getOrganizations(); const userOrgsData = userOrgs.map((org) => org._id); let orgToLogin; - if (userOrgsData.includes(localStorage.getItem("orgData.id"))) { - orgToLogin = localStorage.getItem("orgData.id"); + if (userOrgsData.includes(localStorage.getItem('orgData.id'))) { + orgToLogin = localStorage.getItem('orgData.id'); } else { orgToLogin = userOrgsData[0]; - localStorage.setItem("orgData.id", orgToLogin); + localStorage.setItem('orgData.id', orgToLogin); } let orgUserProjects = await getOrganizationUserProjects({ - orgId: orgToLogin, + orgId: orgToLogin }); orgUserProjects = orgUserProjects?.map((project) => project._id); let projectToLogin; if ( - orgUserProjects.includes(localStorage.getItem("projectData.id")) + orgUserProjects.includes(localStorage.getItem('projectData.id')) ) { - projectToLogin = localStorage.getItem("projectData.id"); + projectToLogin = localStorage.getItem('projectData.id'); } else { try { projectToLogin = orgUserProjects[0]; - localStorage.setItem("projectData.id", projectToLogin); + localStorage.setItem('projectData.id', projectToLogin); } catch (error) { - console.log("ERROR: User likely has no projects. ", error); + console.log('ERROR: User likely has no projects. ', error); } } @@ -110,38 +109,35 @@ const attemptLogin = async ( await pushKeys({ obj: { DATABASE_URL: [ - "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", - "personal", + 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', + 'personal' ], - DB_USERNAME: ["user1234", "personal"], - DB_PASSWORD: ["ah8jak3hk8dhiu4dw7whxwe1l", "personal"], - TWILIO_AUTH_TOKEN: [ - "hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG", - "shared", - ], - WEBSITE_URL: ["http://localhost:3000", "shared"], - STRIPE_SECRET_KEY: ["sk_test_7348oyho4hfq398HIUOH78", "shared"], + DB_USERNAME: ['user1234', 'personal'], + DB_PASSWORD: ['example_password', 'personal'], + TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'], + WEBSITE_URL: ['http://localhost:3000', 'shared'], + STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared'] }, workspaceId: projectToLogin, - env: "Development", + env: 'Development' }); } if (email) { telemetry.identify(email); - telemetry.capture("User Logged In"); + telemetry.capture('User Logged In'); } if (isLogin) { - router.push("/dashboard/"); + router.push('/dashboard/'); } } catch (error) { setErrorLogin(true); - console.log("Login response not available"); + console.log('Login response not available'); } } ); } catch (error) { - console.log("Something went wrong during authentication"); + console.log('Something went wrong during authentication'); } return true; }; diff --git a/frontend/components/utilities/checks/OnboardingCheck.ts b/frontend/components/utilities/checks/OnboardingCheck.ts new file mode 100644 index 0000000000..343efadb52 --- /dev/null +++ b/frontend/components/utilities/checks/OnboardingCheck.ts @@ -0,0 +1,71 @@ +import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; +import checkUserAction from '~/pages/api/userActions/checkUserAction'; + +interface OnboardingCheckProps { + setTotalOnboardingActionsDone?: (value: number) => void; + setHasUserClickedSlack?: (value: boolean) => void; + setHasUserClickedIntro?: (value: boolean) => void; + setHasUserStarred?: (value: boolean) => void; + setHasUserPushedSecrets?: (value: boolean) => void; + setUsersInOrg?: (value: boolean) => void; +} + +/** + * This function checks which onboarding steps a user has already finished. + */ +const onboardingCheck = async ({ + setTotalOnboardingActionsDone, + setHasUserClickedSlack, + setHasUserClickedIntro, + setHasUserStarred, + setHasUserPushedSecrets, + setUsersInOrg +}: OnboardingCheckProps) => { + let countActions = 0; + const userActionSlack = await checkUserAction({ + action: 'slack_cta_clicked' + }); + if (userActionSlack) { + countActions = countActions + 1; + } + setHasUserClickedSlack && + setHasUserClickedSlack(userActionSlack ? true : false); + + const userActionSecrets = await checkUserAction({ + action: 'first_time_secrets_pushed' + }); + if (userActionSecrets) { + countActions = countActions + 1; + } + setHasUserPushedSecrets && + setHasUserPushedSecrets(userActionSecrets ? true : false); + + const userActionIntro = await checkUserAction({ + action: 'intro_cta_clicked' + }); + if (userActionIntro) { + countActions = countActions + 1; + } + setHasUserClickedIntro && + setHasUserClickedIntro(userActionIntro ? true : false); + + const userActionStar = await checkUserAction({ + action: 'star_cta_clicked' + }); + if (userActionStar) { + countActions = countActions + 1; + } + setHasUserStarred && setHasUserStarred(userActionStar ? true : false); + + const orgId = localStorage.getItem('orgData.id'); + const orgUsers = await getOrganizationUsers({ + orgId: orgId ? orgId : '' + }); + if (orgUsers.length > 1) { + countActions = countActions + 1; + } + setUsersInOrg && setUsersInOrg(orgUsers.length > 1); + setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions); +}; + +export default onboardingCheck; diff --git a/frontend/components/utilities/cryptography/aes-256-gcm.js b/frontend/components/utilities/cryptography/aes-256-gcm.js deleted file mode 100644 index 0616813dfd..0000000000 --- a/frontend/components/utilities/cryptography/aes-256-gcm.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @fileoverview Provides easy encryption/decryption methods using AES 256 GCM. - */ - -"use strict"; - -const crypto = require("crypto"); - -const ALGORITHM = "aes-256-gcm"; -const BLOCK_SIZE_BYTES = 16; // 128 bit - -/** - * Provides easy encryption/decryption methods using AES 256 GCM. - */ -class Aes256Gcm { - /** - * No need to run the constructor. The class only has static methods. - */ - constructor() {} - - /** - * Encrypts text with AES 256 GCM. - * @param {string} text - Cleartext to encode. - * @param {string} secret - Shared secret key, must be 32 bytes. - * @returns {object} - */ - static encrypt(text, secret) { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, "utf8", "base64"); - ciphertext += cipher.final("base64"); - return { - ciphertext, - iv: iv.toString("base64"), - tag: cipher.getAuthTag().toString("base64"), - }; - } - - /** - * Decrypts AES 256 CGM encrypted text. - * @param {string} ciphertext - Base64-encoded ciphertext. - * @param {string} iv - The base64-encoded initialization vector. - * @param {string} tag - The base64-encoded authentication tag generated by getAuthTag(). - * @param {string} secret - Shared secret key, must be 32 bytes. - * @returns {string} - */ - static decrypt(ciphertext, iv, tag, secret) { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, "base64") - ); - decipher.setAuthTag(Buffer.from(tag, "base64")); - - let cleartext = decipher.update(ciphertext, "base64", "utf8"); - cleartext += decipher.final("utf8"); - - return cleartext; - } -} - -module.exports = Aes256Gcm; diff --git a/frontend/components/utilities/cryptography/aes-256-gcm.ts b/frontend/components/utilities/cryptography/aes-256-gcm.ts new file mode 100644 index 0000000000..aa4986dc42 --- /dev/null +++ b/frontend/components/utilities/cryptography/aes-256-gcm.ts @@ -0,0 +1,82 @@ +/** + * @fileoverview Provides easy encryption/decryption methods using AES 256 GCM. + */ + +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; // 128 bit + +interface EncryptProps { + text: string; + secret: string; +} + +interface DecryptProps { + ciphertext: string; + iv: string; + tag: string; + secret: string; +} + +interface EncryptOutputProps { + ciphertext: string; + iv: string; + tag: string; +} + +/** + * Provides easy encryption/decryption methods using AES 256 GCM. + */ +class Aes256Gcm { + /** + * No need to run the constructor. The class only has static methods. + */ + constructor() {} + + /** + * Encrypts text with AES 256 GCM. + * @param {object} obj + * @param {string} obj.text - Cleartext to encode. + * @param {string} obj.secret - Shared secret key, must be 32 bytes. + * @returns {object} + */ + // { ciphertext: string; iv: string; tag: string; } + static encrypt({ text, secret }: EncryptProps): EncryptOutputProps { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; + } + + /** + * Decrypts AES 256 CGM encrypted text. + * @param {object} obj + * @param {string} obj.ciphertext - Base64-encoded ciphertext. + * @param {string} obj.iv - The base64-encoded initialization vector. + * @param {string} obj.tag - The base64-encoded authentication tag generated by getAuthTag(). + * @param {string} obj.secret - Shared secret key, must be 32 bytes. + * @returns {string} + */ + static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; + } +} + +export default Aes256Gcm; diff --git a/frontend/components/utilities/cryptography/changePassword.js b/frontend/components/utilities/cryptography/changePassword.js index de0fd9c3a7..c173b3cdfb 100644 --- a/frontend/components/utilities/cryptography/changePassword.js +++ b/frontend/components/utilities/cryptography/changePassword.js @@ -1,11 +1,11 @@ -import changePassword2 from "~/pages/api/auth/ChangePassword2"; -import SRP1 from "~/pages/api/auth/SRP1"; +import changePassword2 from '~/pages/api/auth/ChangePassword2'; +import SRP1 from '~/pages/api/auth/SRP1'; -import Aes256Gcm from "./aes-256-gcm"; +import Aes256Gcm from './aes-256-gcm'; -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); const clientOldPassword = new jsrp.client(); const clientNewPassword = new jsrp.client(); @@ -34,7 +34,7 @@ const changePassword = async ( clientOldPassword.init( { username: email, - password: currentPassword, + password: currentPassword }, async () => { const clientPublicKey = clientOldPassword.getPublicKey(); @@ -42,13 +42,13 @@ const changePassword = async ( let serverPublicKey, salt; try { const res = await SRP1({ - clientPublicKey: clientPublicKey, + clientPublicKey: clientPublicKey }); serverPublicKey = res.serverPublicKey; salt = res.salt; } catch (err) { setCurrentPasswordError(true); - console.log("Wrong current password", err, 1); + console.log('Wrong current password', err, 1); } clientOldPassword.setSalt(salt); @@ -58,27 +58,27 @@ const changePassword = async ( clientNewPassword.init( { username: email, - password: newPassword, + password: newPassword }, async () => { clientNewPassword.createVerifier(async (err, result) => { // The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø) - let { ciphertext, iv, tag } = Aes256Gcm.encrypt( - localStorage.getItem("PRIVATE_KEY"), - newPassword + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: localStorage.getItem('PRIVATE_KEY'), + secret: newPassword .slice(0, 32) .padStart( 32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), - "0" + '0' ) - ); + }); if (ciphertext) { - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); + localStorage.setItem('encryptedPrivateKey', ciphertext); + localStorage.setItem('iv', iv); + localStorage.setItem('tag', tag); let res; try { @@ -88,14 +88,14 @@ const changePassword = async ( tag, salt: result.salt, verifier: result.verifier, - clientProof, + clientProof }); if (res.status == 400) { setCurrentPasswordError(true); } else if (res.status == 200) { setPasswordChanged(true); - setCurrentPassword(""); - setNewPassword(""); + setCurrentPassword(''); + setNewPassword(''); } } catch (err) { setCurrentPasswordError(true); @@ -108,7 +108,7 @@ const changePassword = async ( } ); } catch (error) { - console.log("Something went wrong during changing the password"); + console.log('Something went wrong during changing the password'); } return true; }; diff --git a/frontend/components/utilities/cryptography/crypto.ts b/frontend/components/utilities/cryptography/crypto.ts index a50ee6a0a7..409b447fc5 100644 --- a/frontend/components/utilities/cryptography/crypto.ts +++ b/frontend/components/utilities/cryptography/crypto.ts @@ -1,12 +1,12 @@ -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const aes = require("./aes-256-gcm"); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +import aes from './aes-256-gcm'; type encryptAsymmetricProps = { plaintext: string; publicKey: string; privateKey: string; -} +}; /** * Return assymmetrically encrypted [plaintext] using [publicKey] where @@ -19,7 +19,11 @@ type encryptAsymmetricProps = { * @returns {String} ciphertext - base64-encoded ciphertext * @returns {String} nonce - base64-encoded nonce */ -const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmetricProps): object => { +const encryptAssymmetric = ({ + plaintext, + publicKey, + privateKey +}: encryptAsymmetricProps): object => { const nonce = nacl.randomBytes(24); const ciphertext = nacl.box( nacl.util.decodeUTF8(plaintext), @@ -30,7 +34,7 @@ const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmet return { ciphertext: nacl.util.encodeBase64(ciphertext), - nonce: nacl.util.encodeBase64(nonce), + nonce: nacl.util.encodeBase64(nonce) }; }; @@ -39,7 +43,7 @@ type decryptAsymmetricProps = { nonce: string; publicKey: string; privateKey: string; -} +}; /** * Return assymmetrically decrypted [ciphertext] using [privateKey] where @@ -49,9 +53,13 @@ type decryptAsymmetricProps = { * @param {String} obj.nonce - nonce * @param {String} obj.publicKey - base64-encoded public key of the sender * @param {String} obj.privateKey - base64-encoded private key of the receiver (current user) - * @param {String} plaintext - UTF8 plaintext */ -const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryptAsymmetricProps): string => { +const decryptAssymmetric = ({ + ciphertext, + nonce, + publicKey, + privateKey +}: decryptAsymmetricProps): string => { const plaintext = nacl.box.open( nacl.util.decodeBase64(ciphertext), nacl.util.decodeBase64(nonce), @@ -65,7 +73,7 @@ const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryp type encryptSymmetricProps = { plaintext: string; key: string; -} +}; /** * Return symmetrically encrypted [plaintext] using [key]. @@ -73,15 +81,18 @@ type encryptSymmetricProps = { * @param {String} obj.plaintext - plaintext to encrypt * @param {String} obj.key - 16-byte hex key */ -const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => { +const encryptSymmetric = ({ + plaintext, + key +}: encryptSymmetricProps): object => { let ciphertext, iv, tag; try { - const obj = aes.encrypt(plaintext, key); + const obj = aes.encrypt({ text: plaintext, secret: key }); ciphertext = obj.ciphertext; iv = obj.iv; tag = obj.tag; } catch (err) { - console.log("Failed to perform encryption"); + console.log('Failed to perform encryption'); console.log(err); process.exit(1); } @@ -89,7 +100,7 @@ const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => return { ciphertext, iv, - tag, + tag }; }; @@ -98,7 +109,7 @@ type decryptSymmetricProps = { iv: string; tag: string; key: string; -} +}; /** * Return symmetrically decypted [ciphertext] using [iv], [tag], @@ -110,12 +121,17 @@ type decryptSymmetricProps = { * @param {String} obj.key - 32-byte hex key * */ -const decryptSymmetric = ({ ciphertext, iv, tag, key }: decryptSymmetricProps): string => { +const decryptSymmetric = ({ + ciphertext, + iv, + tag, + key +}: decryptSymmetricProps): string => { let plaintext; try { - plaintext = aes.decrypt(ciphertext, iv, tag, key); + plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key }); } catch (err) { - console.log("Failed to perform decryption"); + console.log('Failed to perform decryption'); process.exit(1); } @@ -126,5 +142,5 @@ export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, - encryptSymmetric, + encryptSymmetric }; diff --git a/frontend/components/utilities/cryptography/issueBackupKey.js b/frontend/components/utilities/cryptography/issueBackupKey.js deleted file mode 100644 index 19d7ebefa8..0000000000 --- a/frontend/components/utilities/cryptography/issueBackupKey.js +++ /dev/null @@ -1,98 +0,0 @@ -import issueBackupPrivateKey from "~/pages/api/auth/IssueBackupPrivateKey"; -import SRP1 from "~/pages/api/auth/SRP1"; - -import generateBackupPDF from "../generateBackupPDF"; -import Aes256Gcm from "./aes-256-gcm"; - -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); -const jsrp = require("jsrp"); -const clientPassword = new jsrp.client(); -const clientKey = new jsrp.client(); -const crypto = require("crypto"); - -/** - * This function loggs in the user (whether it's right after signup, or a normal login) - * @param {*} email - * @param {*} password - * @param {*} setErrorLogin - * @param {*} router - * @param {*} isSignUp - * @returns - */ -const issueBackupKey = async ({ - email, - password, - personalName, - setBackupKeyError, - setBackupKeyIssued, -}) => { - try { - setBackupKeyError(false); - setBackupKeyIssued(false); - clientPassword.init( - { - username: email, - password: password, - }, - async () => { - const clientPublicKey = clientPassword.getPublicKey(); - - let serverPublicKey, salt; - try { - const res = await SRP1({ - clientPublicKey: clientPublicKey, - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setBackupKeyError(true); - console.log("Wrong current password", err, 1); - } - - clientPassword.setSalt(salt); - clientPassword.setServerPublicKey(serverPublicKey); - const clientProof = clientPassword.getProof(); // called M1 - - const generatedKey = crypto.randomBytes(16).toString("hex"); - - clientKey.init( - { - username: email, - password: generatedKey, - }, - async () => { - clientKey.createVerifier(async (err, result) => { - let { ciphertext, iv, tag } = Aes256Gcm.encrypt( - localStorage.getItem("PRIVATE_KEY"), - generatedKey - ); - - const res = await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof, - }); - - if (res.status == 400) { - setBackupKeyError(true); - } else if (res.status == 200) { - generateBackupPDF(personalName, email, generatedKey); - setBackupKeyIssued(true); - } - }); - } - ); - } - ); - } catch (error) { - setBackupKeyError(true); - console.log("Failed to issue a backup key"); - } - return true; -}; - -export default issueBackupKey; diff --git a/frontend/components/utilities/cryptography/issueBackupKey.ts b/frontend/components/utilities/cryptography/issueBackupKey.ts new file mode 100644 index 0000000000..147377b77f --- /dev/null +++ b/frontend/components/utilities/cryptography/issueBackupKey.ts @@ -0,0 +1,113 @@ +import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey'; +import SRP1 from '~/pages/api/auth/SRP1'; + +import generateBackupPDF from '../generateBackupPDF'; +import Aes256Gcm from './aes-256-gcm'; + +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); +const jsrp = require('jsrp'); +const clientPassword = new jsrp.client(); +const clientKey = new jsrp.client(); +const crypto = require('crypto'); + +interface BackupKeyProps { + email: string; + password: string; + personalName: string; + setBackupKeyError: (value: boolean) => void; + setBackupKeyIssued: (value: boolean) => void; +} + +/** + * This function issue a backup key for a user + * @param {obkect} obj + * @param {string} obj.email - email of a user issuing a backup key + * @param {string} obj.password - password of a user issuing a backup key + * @param {string} obj.personalName - name of a user issuing a backup key + * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key + * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly + * @returns + */ +const issueBackupKey = async ({ + email, + password, + personalName, + setBackupKeyError, + setBackupKeyIssued +}: BackupKeyProps) => { + try { + setBackupKeyError(false); + setBackupKeyIssued(false); + clientPassword.init( + { + username: email, + password: password + }, + async () => { + const clientPublicKey = clientPassword.getPublicKey(); + + let serverPublicKey, salt; + try { + const res = await SRP1({ + clientPublicKey: clientPublicKey + }); + serverPublicKey = res.serverPublicKey; + salt = res.salt; + } catch (err) { + setBackupKeyError(true); + console.log('Wrong current password', err, 1); + } + + clientPassword.setSalt(salt); + clientPassword.setServerPublicKey(serverPublicKey); + const clientProof = clientPassword.getProof(); // called M1 + + const generatedKey = crypto.randomBytes(16).toString('hex'); + + clientKey.init( + { + username: email, + password: generatedKey + }, + async () => { + clientKey.createVerifier( + async (err: any, result: { salt: string; verifier: string }) => { + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: String(localStorage.getItem('PRIVATE_KEY')), + secret: generatedKey + }); + + const res = await issueBackupPrivateKey({ + encryptedPrivateKey: ciphertext, + iv, + tag, + salt: result.salt, + verifier: result.verifier, + clientProof + }); + + if (res?.status == 400) { + setBackupKeyError(true); + } else if (res?.status == 200) { + generateBackupPDF({ + personalName, + personalEmail: email, + generatedKey + }); + setBackupKeyIssued(true); + } + } + ); + } + ); + } + ); + } catch (error) { + setBackupKeyError(true); + console.log('Failed to issue a backup key'); + } + return true; +}; + +export default issueBackupKey; diff --git a/frontend/components/utilities/file.js b/frontend/components/utilities/file.ts similarity index 74% rename from frontend/components/utilities/file.js rename to frontend/components/utilities/file.ts index 96d1820eb1..3784405f91 100644 --- a/frontend/components/utilities/file.js +++ b/frontend/components/utilities/file.ts @@ -6,21 +6,21 @@ const LINE = * @param {Buffer} src - source buffer * @returns {String} text - text of buffer */ -function parse(src) { - const obj = {}; +function parse(src: Buffer) { + const obj: Record = {}; // Convert buffer to string let lines = src.toString(); // Convert line breaks to same format - lines = lines.replace(/\r\n?/gm, "\n"); + lines = lines.replace(/\r\n?/gm, '\n'); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; // Default undefined or null to empty string - let value = match[2] || ""; + let value = match[2] || ''; // Remove whitespace value = value.trim(); @@ -29,12 +29,12 @@ function parse(src) { const maybeQuote = value[0]; // Remove surrounding quotes - value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); + value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2'); // Expand newlines if double quoted if (maybeQuote === '"') { - value = value.replace(/\\n/g, "\n"); - value = value.replace(/\\r/g, "\r"); + value = value.replace(/\\n/g, '\n'); + value = value.replace(/\\r/g, '\r'); } // Add to object diff --git a/frontend/components/utilities/generateBackupPDF.js b/frontend/components/utilities/generateBackupPDF.ts similarity index 94% rename from frontend/components/utilities/generateBackupPDF.js rename to frontend/components/utilities/generateBackupPDF.ts index 58e0b6667e..d47bdfe732 100644 --- a/frontend/components/utilities/generateBackupPDF.js +++ b/frontend/components/utilities/generateBackupPDF.ts @@ -1,86 +1,96 @@ -import { jsPDF } from "jspdf"; +import { jsPDF } from 'jspdf'; + +interface PDFProps { + personalName: string; + personalEmail: string; + generatedKey: string; +} /** * This function generate a pdf with a secret key for a user. */ -function generateBackupPDF(personalName, personalEmail, generatedKey) { +function generateBackupPDF({ + personalName, + personalEmail, + generatedKey +}: PDFProps) { const imgData = - "data:image/png;base64,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"; + 'data:image/png;base64,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'; // new jsPDF('p', 'mm', [297, 210]); - var today = new Date(); - var dd = String(today.getDate()).padStart(2, "0"); - var mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0! - var yyyy = today.getFullYear(); + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + const yyyy = today.getFullYear(); - today = mm + "/" + dd + "/" + yyyy; + const todayFormatted = mm + '/' + dd + '/' + yyyy; - var doc = new jsPDF("p", "pt"); - doc.setFillColor(13, 17, 23); - doc.rect(0, 0, 600, 900, "F"); - doc.setTextColor(227, 227, 227); - doc.addImage(imgData, "png", 30, 35, 535, 72); - doc.addFont("helvetica", "normal"); + const doc = new jsPDF('p', 'pt'); + doc.setFillColor(255, 255, 255); + doc.rect(0, 0, 600, 900, 'F'); + doc.setTextColor(23, 23, 23); + doc.addImage(imgData, 'png', 30, 35, 535, 72); + doc.addFont('helvetica', 'normal', ''); doc.setFontSize(12); doc.text( + 'Created for ' + personalName + ' on ' + todayFormatted + '.', 290, 130, - "Created for " + personalName + " on " + today + ".", - "center" + { align: 'center' } ); doc.setFontSize(14); doc.text( + 'In case you get locked out of you Infisical account, you`ll need these account details', 32, - 180, - "In case you get locked out of you Infisical account, you`ll need these account details" + 180 ); - doc.text(32, 200, "to sign in —"); - doc.setFont(undefined, "bold"); + doc.text('to sign in —', 32, 200); + doc.setFont('helvetica', 'bold'); doc.text( + 'including your Secret Key, which we absolutely cannot access or', 110, - 200, - "including your Secret Key, which we absolutely cannot access or" + 200 ); - doc.text(32, 220, "recover for you. "); - doc.setFont(undefined, "normal"); - doc.text(32, 250, "Recommendations:"); + doc.text('recover for you. ', 32, 220); + doc.setFont('helvetica', 'normal'); + doc.text('Recommendations:', 32, 250); doc.text( + '1. We recommend to get your Emergency Kit off your computer and print a copy.', 32, - 280, - "1. We recommend to get your Emergency Kit off your computer and print a copy." + 280 ); doc.text( + '2. Store it somewhere safe (such as with your birth certificate, your will, or on your', 32, - 310, - "2. Store it somewhere safe (such as with your birth certificate, your will, or on your" + 310 ); - doc.text(32, 330, "personal cloud storage)."); - doc.setFillColor(206, 217, 111); - doc.roundedRect(32, 350, 530, 190, 5, 5, "F"); + doc.text('personal cloud storage).', 32, 330); + doc.setFillColor(251, 255, 158); + doc.roundedRect(32, 350, 530, 190, 5, 5, 'F'); doc.setDrawColor(228, 255, 0); doc.setLineWidth(1); - doc.roundedRect(32, 350, 530, 190, 5, 5, "S"); + doc.roundedRect(32, 350, 530, 190, 5, 5, 'S'); doc.setTextColor(43, 43, 43); - doc.setFont(undefined, "bold"); + doc.setFont('helvetica', 'bold'); doc.setFontSize(15); - doc.text(290, 375, "Infisical Account Details", "center"); - doc.setFont(undefined, "normal"); + doc.text('Infisical Account Details', 290, 375, { align: 'center' }); doc.setFontSize(12); - doc.text(50, 420, "SIGN-IN URL"); - doc.text(50, 465, "EMAIL ADDRESS"); - doc.text(50, 510, "SECRET KEY"); - doc.setFillColor(23, 27, 33); - doc.roundedRect(170, 398, 375, 35, 5, 5, "F"); - doc.roundedRect(170, 443, 375, 35, 5, 5, "F"); - doc.roundedRect(170, 488, 375, 35, 5, 5, "F"); - doc.setTextColor(227, 227, 227); + doc.text('SIGN-IN URL', 50, 420); + doc.text('EMAIL ADDRESS', 50, 465); + doc.text('SECRET KEY', 50, 510); + doc.setFont('helvetica', 'normal'); + doc.setFillColor(254, 255, 235); + doc.roundedRect(170, 398, 375, 35, 5, 5, 'F'); + doc.roundedRect(170, 443, 375, 35, 5, 5, 'F'); + doc.roundedRect(170, 488, 375, 35, 5, 5, 'F'); + doc.setTextColor(23, 23, 23); doc.setFontSize(14); - doc.text(180, 420, "https://app.infisical.com/login"); - doc.text(180, 465, personalEmail); - doc.text(180, 510, generatedKey); - doc.text(32, 575, "Need help? Contact us at support@infisical.com"); + doc.text('https://app.infisical.com/login', 180, 420); + doc.text(personalEmail, 180, 465); + doc.text(generatedKey, 180, 510); + doc.text('Need help? Contact us at support@infisical.com', 32, 575); - doc.save("Infisical Emergency Kit.pdf"); + doc.save('Infisical Emergency Kit.pdf'); } export default generateBackupPDF; diff --git a/frontend/components/utilities/randomId.js b/frontend/components/utilities/randomId.ts similarity index 82% rename from frontend/components/utilities/randomId.js rename to frontend/components/utilities/randomId.ts index 7d1c0859bd..8e1c7f9723 100644 --- a/frontend/components/utilities/randomId.js +++ b/frontend/components/utilities/randomId.ts @@ -3,19 +3,19 @@ * @returns */ const guidGenerator = () => { - var S4 = function () { + const S4 = function () { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); }; return ( S4() + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + S4() + S4() diff --git a/frontend/components/utilities/secrets/getSecretsForProject.js b/frontend/components/utilities/secrets/getSecretsForProject.ts similarity index 58% rename from frontend/components/utilities/secrets/getSecretsForProject.js rename to frontend/components/utilities/secrets/getSecretsForProject.ts index a4cce35e0b..0bbcdd0d25 100644 --- a/frontend/components/utilities/secrets/getSecretsForProject.js +++ b/frontend/components/utilities/secrets/getSecretsForProject.ts @@ -1,22 +1,30 @@ -import getSecrets from "~/pages/api/files/GetSecrets"; +import getSecrets from '~/pages/api/files/GetSecrets'; -import { envMapping } from "../../../public/data/frequentConstants"; -import guidGenerator from "../randomId"; +import { envMapping } from '../../../public/data/frequentConstants'; +import guidGenerator from '../randomId'; const { decryptAssymmetric, - decryptSymmetric, -} = require("../cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); + decryptSymmetric +} = require('../cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); + +interface Props { + env: keyof typeof envMapping; + setFileState: any; + setIsKeyAvailable: any; + setData: any; + workspaceId: string; +} const getSecretsForProject = async ({ env, setFileState, setIsKeyAvailable, setData, - workspaceId, -}) => { + workspaceId +}: Props) => { try { let file; try { @@ -24,44 +32,42 @@ const getSecretsForProject = async ({ setFileState(file); } catch (error) { - console.log("ERROR: Not able to access the latest file"); + console.log('ERROR: Not able to access the latest file'); } // This is called isKeyAvilable but what it really means is if a person is able to create new key pairs - setIsKeyAvailable( - !file.key ? (file.secrets.length == 0 ? true : false) : true - ); + setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); - let tempFileState = []; + const tempFileState: { key: string; value: string; type: string }[] = []; if (file.key) { // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ ciphertext: file.key.encryptedKey, nonce: file.key.nonce, publicKey: file.key.sender.publicKey, - privateKey: PRIVATE_KEY, + privateKey: PRIVATE_KEY }); - file.secrets.map((secretPair) => { + file.secrets.map((secretPair: any) => { // decrypt .env file with symmetric key const plainTextKey = decryptSymmetric({ ciphertext: secretPair.secretKey.ciphertext, iv: secretPair.secretKey.iv, tag: secretPair.secretKey.tag, - key, + key }); const plainTextValue = decryptSymmetric({ ciphertext: secretPair.secretValue.ciphertext, iv: secretPair.secretValue.iv, tag: secretPair.secretValue.tag, - key, + key }); tempFileState.push({ key: plainTextKey, value: plainTextValue, - type: secretPair.type, + type: secretPair.type }); }); } @@ -72,9 +78,9 @@ const getSecretsForProject = async ({ return { id: guidGenerator(), pos: index, - key: line["key"], - value: line["value"], - type: line["type"] + key: line['key'], + value: line['value'], + type: line['type'] } }) ); @@ -82,12 +88,12 @@ const getSecretsForProject = async ({ return tempFileState.map((line, index) => [ guidGenerator(), index, - line["key"], - line["value"], - line["type"], + line['key'], + line['value'], + line['type'] ]); } catch (error) { - console.log("Something went wrong during accessing or decripting secrets."); + console.log('Something went wrong during accessing or decripting secrets.'); } return true; }; diff --git a/frontend/components/utilities/secrets/pushKeysIntegration.js b/frontend/components/utilities/secrets/pushKeysIntegration.js deleted file mode 100644 index 5b08748e72..0000000000 --- a/frontend/components/utilities/secrets/pushKeysIntegration.js +++ /dev/null @@ -1,74 +0,0 @@ -import publicKeyInfical from "~/pages/api/auth/publicKeyInfisical"; -import changeHerokuConfigVars from "~/pages/api/integrations/ChangeHerokuConfigVars"; - -const crypto = require("crypto"); -const { - encryptSymmetric, - encryptAssymmetric, -} = require("../cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); - -const pushKeysIntegration = async ({ obj, integrationId }) => { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - let randomBytes = crypto.randomBytes(16).toString("hex"); - - const secrets = Object.keys(obj).map((key) => { - // encrypt key - const { - ciphertext: ciphertextKey, - iv: ivKey, - tag: tagKey, - } = encryptSymmetric({ - plaintext: key, - key: randomBytes, - }); - - // encrypt value - const { - ciphertext: ciphertextValue, - iv: ivValue, - tag: tagValue, - } = encryptSymmetric({ - plaintext: obj[key], - key: randomBytes, - }); - - const visibility = "shared"; - - return { - ciphertextKey, - ivKey, - tagKey, - hashKey: crypto.createHash("sha256").update(key).digest("hex"), - ciphertextValue, - ivValue, - tagValue, - hashValue: crypto.createHash("sha256").update(obj[key]).digest("hex"), - type: visibility, - }; - }); - - // obtain public keys of all receivers (i.e. members in workspace) - let publicKeyInfisical = await publicKeyInfical(); - - publicKeyInfisical = (await publicKeyInfisical.json()).publicKey; - - // assymmetrically encrypt key with each receiver public keys - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: publicKeyInfisical, - privateKey: PRIVATE_KEY, - }); - - const key = { - encryptedKey: ciphertext, - nonce, - }; - - changeHerokuConfigVars({ integrationId, key, secrets }); -}; - -export default pushKeysIntegration; diff --git a/frontend/components/utilities/secrets/pushKeysIntegration.ts b/frontend/components/utilities/secrets/pushKeysIntegration.ts new file mode 100644 index 0000000000..9497789275 --- /dev/null +++ b/frontend/components/utilities/secrets/pushKeysIntegration.ts @@ -0,0 +1,79 @@ +import publicKeyInfical from '~/pages/api/auth/publicKeyInfisical'; +import changeHerokuConfigVars from '~/pages/api/integrations/ChangeHerokuConfigVars'; + +const crypto = require('crypto'); +const { + encryptSymmetric, + encryptAssymmetric +} = require('../cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); + +interface Props { + obj: Record; + integrationId: string; +} + +const pushKeysIntegration = async ({ obj, integrationId }: Props) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + const randomBytes = crypto.randomBytes(16).toString('hex'); + + const secrets = Object.keys(obj).map((key) => { + // encrypt key + const { + ciphertext: ciphertextKey, + iv: ivKey, + tag: tagKey + } = encryptSymmetric({ + plaintext: key, + key: randomBytes + }); + + // encrypt value + const { + ciphertext: ciphertextValue, + iv: ivValue, + tag: tagValue + } = encryptSymmetric({ + plaintext: obj[key], + key: randomBytes + }); + + const visibility = 'shared'; + + return { + ciphertextKey, + ivKey, + tagKey, + hashKey: crypto.createHash('sha256').update(key).digest('hex'), + ciphertextValue, + ivValue, + tagValue, + hashValue: crypto.createHash('sha256').update(obj[key]).digest('hex'), + type: visibility + }; + }); + + // obtain public keys of all receivers (i.e. members in workspace) + const publicKeyInfisical = await publicKeyInfical(); + + const publicKey = (await publicKeyInfisical.json()).publicKey; + + // assymmetrically encrypt key with each receiver public keys + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: randomBytes, + publicKey, + privateKey: PRIVATE_KEY + }); + + const key = { + encryptedKey: ciphertext, + nonce + }; + + changeHerokuConfigVars({ integrationId, key, secrets }); +}; + +export default pushKeysIntegration; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0c87e9fdab..229fd05fca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,7 +29,7 @@ "markdown-it": "^13.0.1", "next": "^12.2.5", "posthog-js": "^1.34.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", @@ -2467,9 +2467,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -5826,11 +5826,11 @@ } }, "node_modules/query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "dependencies": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -9126,9 +9126,9 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "deep-is": { "version": "0.1.4", @@ -11477,11 +11477,11 @@ "dev": true }, "query-string": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", - "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "requires": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" diff --git a/frontend/package.json b/frontend/package.json index ab71c81bf5..8842aa2965 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,7 @@ "markdown-it": "^13.0.1", "next": "^12.2.5", "posthog-js": "^1.34.0", - "query-string": "^7.1.1", + "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", diff --git a/frontend/pages/api/auth/ChangePassword2.js b/frontend/pages/api/auth/ChangePassword2.ts similarity index 50% rename from frontend/pages/api/auth/ChangePassword2.js rename to frontend/pages/api/auth/ChangePassword2.ts index b764e60677..132eeddf43 100644 --- a/frontend/pages/api/auth/ChangePassword2.js +++ b/frontend/pages/api/auth/ChangePassword2.ts @@ -1,4 +1,13 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; + clientProof: string; +} /** * This is the second step of the change password process (pake) @@ -11,12 +20,12 @@ const changePassword2 = ({ tag, salt, verifier, - clientProof, -}) => { - return SecurityClient.fetchCall("/api/v1/password/change-password", { - method: "POST", + clientProof +}: Props) => { + return SecurityClient.fetchCall('/api/v1/password/change-password', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ clientProof: clientProof, @@ -24,13 +33,13 @@ const changePassword2 = ({ iv: iv, tag: tag, salt: salt, - verifier: verifier, - }), + verifier: verifier + }) }).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return res; } else { - console.log("Failed to change the password"); + console.log('Failed to change the password'); } }); }; diff --git a/frontend/pages/api/auth/CheckAuth.js b/frontend/pages/api/auth/CheckAuth.js deleted file mode 100644 index edd3736144..0000000000 --- a/frontend/pages/api/auth/CheckAuth.js +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient.js"; - -/** - * This function is used to check if the user is authenticated. - * To do that, we get their tokens from cookies, and verify if they are good. - * @param {*} req - * @param {*} res - * @returns - */ -const checkAuth = async (req, res) => { - return SecurityClient.fetchCall("/api/v1/auth/checkAuth", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }).then((res) => { - if (res.status == 200) { - return res; - } else { - console.log("Not authorized"); - } - }); -}; - -export default checkAuth; diff --git a/frontend/pages/api/auth/CheckAuth.ts b/frontend/pages/api/auth/CheckAuth.ts new file mode 100644 index 0000000000..2578d7ce9a --- /dev/null +++ b/frontend/pages/api/auth/CheckAuth.ts @@ -0,0 +1,22 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function is used to check if the user is authenticated. + * To do that, we get their tokens from cookies, and verify if they are good. + */ +const checkAuth = async () => { + return SecurityClient.fetchCall('/api/v1/auth/checkAuth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }).then((res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Not authorized'); + } + }); +}; + +export default checkAuth; diff --git a/frontend/pages/api/auth/CheckEmailVerificationCode.js b/frontend/pages/api/auth/CheckEmailVerificationCode.js deleted file mode 100644 index 83a64602a7..0000000000 --- a/frontend/pages/api/auth/CheckEmailVerificationCode.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route check the verification code from the email that user just recieved - * @param {*} email - * @param {*} code - * @returns - */ -const checkEmailVerificationCode = (email, code) => { - return fetch("/api/v1/signup/email/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - code: code, - }), - }); -}; - -export default checkEmailVerificationCode; diff --git a/frontend/pages/api/auth/CheckEmailVerificationCode.ts b/frontend/pages/api/auth/CheckEmailVerificationCode.ts new file mode 100644 index 0000000000..0709592a32 --- /dev/null +++ b/frontend/pages/api/auth/CheckEmailVerificationCode.ts @@ -0,0 +1,26 @@ +interface Props { + email: string; + code: string; +} + +/** + * This route check the verification code from the email that user just recieved + * @param {object} obj + * @param {string} obj.email + * @param {string} obj.code + * @returns + */ +const checkEmailVerificationCode = ({ email, code }: Props) => { + return fetch('/api/v1/signup/email/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + code: code + }) + }); +}; + +export default checkEmailVerificationCode; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignup.js b/frontend/pages/api/auth/CompleteAccountInformationSignup.js deleted file mode 100644 index 21406e381e..0000000000 --- a/frontend/pages/api/auth/CompleteAccountInformationSignup.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {*} email - * @param {*} firstName - * @param {*} lastName - * @param {*} workspace - * @param {*} publicKey - * @param {*} ciphertext - * @param {*} iv - * @param {*} tag - * @param {*} salt - * @param {*} verifier - * @returns - */ -const completeAccountInformationSignup = ({ - email, - firstName, - lastName, - organizationName, - publicKey, - ciphertext, - iv, - tag, - salt, - verifier, - token, -}) => { - return fetch("/api/v1/signup/complete-account/signup", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify({ - email, - firstName, - lastName, - publicKey, - encryptedPrivateKey: ciphertext, - organizationName, - iv, - tag, - salt, - verifier, - }), - }); -}; - -export default completeAccountInformationSignup; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignup.ts b/frontend/pages/api/auth/CompleteAccountInformationSignup.ts new file mode 100644 index 0000000000..8057e776a4 --- /dev/null +++ b/frontend/pages/api/auth/CompleteAccountInformationSignup.ts @@ -0,0 +1,66 @@ +interface Props { + email: string; + firstName: string; + lastName: string; + publicKey: string; + ciphertext: string; + organizationName: string; + iv: string; + tag: string; + salt: string; + verifier: string; + token: string; +} + +/** + * This function is called in the end of the signup process. + * It sends all the necessary nformation to the server. + * @param {object} obj + * @param {string} obj.email - email of the user completing signup + * @param {string} obj.firstName - first name of the user completing signup + * @param {string} obj.lastName - last name of the user completing sign up + * @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization) + * @param {string} obj.publicKey - public key of the user completing signup + * @param {string} obj.ciphertext + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.token - token that confirms a user's identity + * @returns + */ +const completeAccountInformationSignup = ({ + email, + firstName, + lastName, + organizationName, + publicKey, + ciphertext, + iv, + tag, + salt, + verifier, + token +}: Props) => { + return fetch('/api/v1/signup/complete-account/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token + }, + body: JSON.stringify({ + email, + firstName, + lastName, + publicKey, + encryptedPrivateKey: ciphertext, + organizationName, + iv, + tag, + salt, + verifier + }) + }); +}; + +export default completeAccountInformationSignup; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js deleted file mode 100644 index a205e6f59f..0000000000 --- a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {*} email - * @param {*} firstName - * @param {*} lastName - * @param {*} publicKey - * @param {*} ciphertext - * @param {*} iv - * @param {*} tag - * @param {*} salt - * @param {*} verifier - * @returns - */ -const completeAccountInformationSignupInvite = ({ - email, - firstName, - lastName, - publicKey, - ciphertext, - iv, - tag, - salt, - verifier, - token, -}) => { - return fetch("/api/v1/signup/complete-account/invite", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify({ - email: email, - firstName: firstName, - lastName: lastName, - publicKey: publicKey, - encryptedPrivateKey: ciphertext, - iv: iv, - tag: tag, - salt: salt, - verifier: verifier, - }), - }); -}; - -export default completeAccountInformationSignupInvite; diff --git a/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts new file mode 100644 index 0000000000..03c11cb67e --- /dev/null +++ b/frontend/pages/api/auth/CompleteAccountInformationSignupInvite.ts @@ -0,0 +1,62 @@ +interface Props { + email: string; + firstName: string; + lastName: string; + publicKey: string; + ciphertext: string; + iv: string; + tag: string; + salt: string; + verifier: string; + token: string; +} + +/** + * This function is called in the end of the signup process. + * It sends all the necessary nformation to the server. + * @param {object} obj + * @param {string} obj.email - email of the user completing signupinvite flow + * @param {string} obj.firstName - first name of the user completing signupinvite flow + * @param {string} obj.lastName - last name of the user completing signupinvite flow + * @param {string} obj.publicKey - public key of the user completing signupinvite flow + * @param {string} obj.ciphertext + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.token - token that confirms a user's identity + * @returns + */ +const completeAccountInformationSignupInvite = ({ + email, + firstName, + lastName, + publicKey, + ciphertext, + iv, + tag, + salt, + verifier, + token +}: Props) => { + return fetch('/api/v1/signup/complete-account/invite', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token + }, + body: JSON.stringify({ + email: email, + firstName: firstName, + lastName: lastName, + publicKey: publicKey, + encryptedPrivateKey: ciphertext, + iv: iv, + tag: tag, + salt: salt, + verifier: verifier + }) + }); +}; + +export default completeAccountInformationSignupInvite; diff --git a/frontend/pages/api/auth/IssueBackupPrivateKey.js b/frontend/pages/api/auth/IssueBackupPrivateKey.js deleted file mode 100644 index 9a31f7b008..0000000000 --- a/frontend/pages/api/auth/IssueBackupPrivateKey.js +++ /dev/null @@ -1,40 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the route that issues a backup private key that will afterwards be added into a pdf - */ -const issueBackupPrivateKey = ({ - encryptedPrivateKey, - iv, - tag, - salt, - verifier, - clientProof, -}) => { - return SecurityClient.fetchCall( - "/api/v1/password/backup-private-key", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - clientProof: clientProof, - encryptedPrivateKey: encryptedPrivateKey, - iv: iv, - tag: tag, - salt: salt, - verifier: verifier, - }), - } - ).then((res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to issue the backup key"); - return res; - } - }); -}; - -export default issueBackupPrivateKey; diff --git a/frontend/pages/api/auth/IssueBackupPrivateKey.ts b/frontend/pages/api/auth/IssueBackupPrivateKey.ts new file mode 100644 index 0000000000..e24ac510e3 --- /dev/null +++ b/frontend/pages/api/auth/IssueBackupPrivateKey.ts @@ -0,0 +1,52 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; + clientProof: string; +} + +/** + * This is the route that issues a backup private key that will afterwards be added into a pdf + * @param {object} obj + * @param {string} obj.encryptedPrivateKey + * @param {string} obj.iv + * @param {string} obj.tag + * @param {string} obj.salt + * @param {string} obj.verifier + * @param {string} obj.clientProof + * @returns + */ +const issueBackupPrivateKey = ({ + encryptedPrivateKey, + iv, + tag, + salt, + verifier, + clientProof +}: Props) => { + return SecurityClient.fetchCall('/api/v1/password/backup-private-key', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + clientProof: clientProof, + encryptedPrivateKey: encryptedPrivateKey, + iv: iv, + tag: tag, + salt: salt, + verifier: verifier + }) + }).then((res) => { + if (res?.status !== 200) { + console.log('Failed to issue the backup key'); + } + return res; + }); +}; + +export default issueBackupPrivateKey; diff --git a/frontend/pages/api/auth/Logout.ts b/frontend/pages/api/auth/Logout.ts index 4dbcb7bcaa..cd4ff9a61e 100644 --- a/frontend/pages/api/auth/Logout.ts +++ b/frontend/pages/api/auth/Logout.ts @@ -1,29 +1,29 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route logs the user out. Note: the user should authorized to do this. * We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry */ const logout = async () => { - return SecurityClient.fetchCall("/api/v1/auth/logout", { - method: "POST", + return SecurityClient.fetchCall('/api/v1/auth/logout', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, - credentials: "include", + credentials: 'include' }).then((res) => { if (res?.status == 200) { - SecurityClient.setToken(""); + SecurityClient.setToken(''); // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.setItem("publicKey", ""); - localStorage.setItem("encryptedPrivateKey", ""); - localStorage.setItem("iv", ""); - localStorage.setItem("tag", ""); - localStorage.setItem("PRIVATE_KEY", ""); - console.log("User logged out", res); + localStorage.setItem('publicKey', ''); + localStorage.setItem('encryptedPrivateKey', ''); + localStorage.setItem('iv', ''); + localStorage.setItem('tag', ''); + localStorage.setItem('PRIVATE_KEY', ''); + console.log('User logged out', res); return res; } else { - console.log("Failed to log out"); + console.log('Failed to log out'); } }); }; diff --git a/frontend/pages/api/auth/SRP1.js b/frontend/pages/api/auth/SRP1.js deleted file mode 100644 index b0fefb857b..0000000000 --- a/frontend/pages/api/auth/SRP1.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the first step of the change password process (pake) - * @param {*} clientPublicKey - * @returns - */ -const SRP1 = ({ clientPublicKey }) => { - return SecurityClient.fetchCall("/api/v1/password/srp1", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - clientPublicKey, - }), - }).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to do the first step of SRP"); - } - }); -}; - -export default SRP1; diff --git a/frontend/pages/api/auth/SRP1.ts b/frontend/pages/api/auth/SRP1.ts new file mode 100644 index 0000000000..cb6386ff20 --- /dev/null +++ b/frontend/pages/api/auth/SRP1.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + clientPublicKey: string; +} + +/** + * This is the first step of the change password process (pake) + * @param {string} clientPublicKey + * @returns + */ +const SRP1 = ({ clientPublicKey }: Props) => { + return SecurityClient.fetchCall('/api/v1/password/srp1', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + clientPublicKey + }) + }).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to do the first step of SRP'); + } + }); +}; + +export default SRP1; diff --git a/frontend/pages/api/auth/SendVerificationEmail.js b/frontend/pages/api/auth/SendVerificationEmail.ts similarity index 55% rename from frontend/pages/api/auth/SendVerificationEmail.js rename to frontend/pages/api/auth/SendVerificationEmail.ts index ae952852d0..4f3b063c6a 100644 --- a/frontend/pages/api/auth/SendVerificationEmail.js +++ b/frontend/pages/api/auth/SendVerificationEmail.ts @@ -2,15 +2,15 @@ * This route send the verification email to the user's email (contains a 6-digit verification code) * @param {*} email */ -const sendVerificationEmail = (email) => { - fetch("/api/v1/signup/email/signup", { - method: "POST", +const sendVerificationEmail = (email: string) => { + fetch('/api/v1/signup/email/signup', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ - email: email, - }), + email: email + }) }); }; diff --git a/frontend/pages/api/auth/Token.js b/frontend/pages/api/auth/Token.js deleted file mode 100644 index c3e5fd9587..0000000000 --- a/frontend/pages/api/auth/Token.js +++ /dev/null @@ -1,17 +0,0 @@ -const token = async (req, res) => { - return fetch("/api/v1/auth/token", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - }).then(async (res) => { - if (res.status == 200) { - return (await res.json()).token; - } else { - console.log("Getting a new token failed"); - } - }); -}; - -export default token; diff --git a/frontend/pages/api/auth/Token.ts b/frontend/pages/api/auth/Token.ts new file mode 100644 index 0000000000..ed347ba4b8 --- /dev/null +++ b/frontend/pages/api/auth/Token.ts @@ -0,0 +1,17 @@ +const token = async () => { + return fetch('/api/v1/auth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }).then(async (res) => { + if (res.status == 200) { + return (await res.json()).token; + } else { + console.log('Getting a new token failed'); + } + }); +}; + +export default token; diff --git a/frontend/pages/api/auth/VerifySignupInvite.js b/frontend/pages/api/auth/VerifySignupInvite.js deleted file mode 100644 index 2a9ba4dcd9..0000000000 --- a/frontend/pages/api/auth/VerifySignupInvite.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route verifies the signup invite link - * @param {*} email - * @param {*} code - * @returns - */ -const verifySignupInvite = ({ email, code }) => { - return fetch("/api/v1/invite-org/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - code, - }), - }); -}; - -export default verifySignupInvite; diff --git a/frontend/pages/api/auth/VerifySignupInvite.ts b/frontend/pages/api/auth/VerifySignupInvite.ts new file mode 100644 index 0000000000..0b6cc7577f --- /dev/null +++ b/frontend/pages/api/auth/VerifySignupInvite.ts @@ -0,0 +1,26 @@ +interface Props { + email: string; + code: string; +} + +/** + * This route verifies the signup invite link + * @param {object} obj + * @param {string} obj.email - email that a user is trying to verify + * @param {string} obj.code - code that a user received to the abovementioned email + * @returns + */ +const verifySignupInvite = ({ email, code }: Props) => { + return fetch('/api/v1/invite-org/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email, + code + }) + }); +}; + +export default verifySignupInvite; diff --git a/frontend/pages/api/auth/publicKeyInfisical.js b/frontend/pages/api/auth/publicKeyInfisical.js deleted file mode 100644 index 76cad17260..0000000000 --- a/frontend/pages/api/auth/publicKeyInfisical.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This route lets us get the public key of infisical. Th euser doesn't have to be authenticated since this is just the public key. - * @param {*} req - * @param {*} res - * @returns - */ -const publicKeyInfisical = (req, res) => { - return fetch("/api/v1/key/publicKey/infisical", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); -}; - -export default publicKeyInfisical; diff --git a/frontend/pages/api/auth/publicKeyInfisical.ts b/frontend/pages/api/auth/publicKeyInfisical.ts new file mode 100644 index 0000000000..d3e0f646c8 --- /dev/null +++ b/frontend/pages/api/auth/publicKeyInfisical.ts @@ -0,0 +1,10 @@ +const publicKeyInfisical = () => { + return fetch('/api/v1/key/publicKey/infisical', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); +}; + +export default publicKeyInfisical; diff --git a/frontend/pages/api/files/GetSecrets.js b/frontend/pages/api/files/GetSecrets.js deleted file mode 100644 index fa91d09621..0000000000 --- a/frontend/pages/api/files/GetSecrets.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient.js"; - -/** - * This function fetches the encrypted secrets from the .env file - * @param {*} workspaceId - * @param {*} env - * @returns - */ -const getSecrets = async (workspaceId, env) => { - return SecurityClient.fetchCall( - "/api/v1/secret/" + - workspaceId + - "?" + - new URLSearchParams({ - environment: env, - channel: "web", - }), - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to get project secrets"); - } - }); -}; - -export default getSecrets; diff --git a/frontend/pages/api/files/GetSecrets.ts b/frontend/pages/api/files/GetSecrets.ts new file mode 100644 index 0000000000..8ff22b57c1 --- /dev/null +++ b/frontend/pages/api/files/GetSecrets.ts @@ -0,0 +1,33 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function fetches the encrypted secrets from the .env file + * @param {string} workspaceId - project is for which a user is trying to get secrets + * @param {string} env - environment of a project for which a user is trying ot get secrets + * @returns + */ +const getSecrets = async (workspaceId: string, env: string) => { + return SecurityClient.fetchCall( + '/api/v1/secret/' + + workspaceId + + '?' + + new URLSearchParams({ + environment: env, + channel: 'web' + }), + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to get project secrets'); + } + }); +}; + +export default getSecrets; diff --git a/frontend/pages/api/files/UploadSecrets.js b/frontend/pages/api/files/UploadSecrets.js deleted file mode 100644 index c2f94e64d0..0000000000 --- a/frontend/pages/api/files/UploadSecrets.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function uploads the encrypted .env file - * @param {*} req - * @param {*} res - * @returns - */ -const uploadSecrets = async ({ workspaceId, secrets, keys, environment }) => { - return SecurityClient.fetchCall("/api/v1/secret/" + workspaceId, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - secrets, - keys, - environment, - channel: "web", - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to push secrets"); - } - }); -}; - -export default uploadSecrets; diff --git a/frontend/pages/api/files/UploadSecrets.ts b/frontend/pages/api/files/UploadSecrets.ts new file mode 100644 index 0000000000..04fcb78b51 --- /dev/null +++ b/frontend/pages/api/files/UploadSecrets.ts @@ -0,0 +1,45 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; + secrets: any; + keys: string; + environment: string; +} + +/** + * This function uploads the encrypted .env file + * @param {object} obj + * @param {string} obj.workspaceId + * @param {} obj.secrets + * @param {} obj.keys + * @param {string} obj.environment + * @returns + */ +const uploadSecrets = async ({ + workspaceId, + secrets, + keys, + environment +}: Props) => { + return SecurityClient.fetchCall('/api/v1/secret/' + workspaceId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + secrets, + keys, + environment, + channel: 'web' + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to push secrets'); + } + }); +}; + +export default uploadSecrets; diff --git a/frontend/pages/api/integrations/ChangeHerokuConfigVars.js b/frontend/pages/api/integrations/ChangeHerokuConfigVars.js deleted file mode 100644 index 118848ae6a..0000000000 --- a/frontend/pages/api/integrations/ChangeHerokuConfigVars.js +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const changeHerokuConfigVars = ({ integrationId, key, secrets }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId + "/sync", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - key, - secrets, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to sync secrets to Heroku"); - } - }); -}; - -export default changeHerokuConfigVars; diff --git a/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts b/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts new file mode 100644 index 0000000000..e011bda7bd --- /dev/null +++ b/frontend/pages/api/integrations/ChangeHerokuConfigVars.ts @@ -0,0 +1,41 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; + key: { encryptedKey: any; nonce: any }; + secrets: { + ciphertextKey: any; + ivKey: any; + tagKey: any; + hashKey: any; + ciphertextValue: any; + ivValue: any; + tagValue: any; + hashValue: any; + type: string; + }[]; +} + +const changeHerokuConfigVars = ({ integrationId, key, secrets }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration/' + integrationId + '/sync', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key, + secrets + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to sync secrets to Heroku'); + } + }); +}; + +export default changeHerokuConfigVars; diff --git a/frontend/pages/api/integrations/DeleteIntegration.js b/frontend/pages/api/integrations/DeleteIntegration.js deleted file mode 100644 index 7698d7eae8..0000000000 --- a/frontend/pages/api/integrations/DeleteIntegration.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an integration from a certain project - * @param {*} integrationId - * @returns - */ -const deleteIntegration = ({ integrationId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspace; - } else { - console.log("Failed to delete an integration"); - } - }); -}; - -export default deleteIntegration; diff --git a/frontend/pages/api/integrations/DeleteIntegration.ts b/frontend/pages/api/integrations/DeleteIntegration.ts new file mode 100644 index 0000000000..89aa0ab0c5 --- /dev/null +++ b/frontend/pages/api/integrations/DeleteIntegration.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; +} + +/** + * This route deletes an integration from a certain project + * @param {*} integrationId + * @returns + */ +const deleteIntegration = ({ integrationId }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration/' + integrationId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspace; + } else { + console.log('Failed to delete an integration'); + } + }); +}; + +export default deleteIntegration; diff --git a/frontend/pages/api/integrations/DeleteIntegrationAuth.js b/frontend/pages/api/integrations/DeleteIntegrationAuth.js deleted file mode 100644 index eb6106e8ef..0000000000 --- a/frontend/pages/api/integrations/DeleteIntegrationAuth.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an integration authorization from a certain project - * @param {*} integrationAuthId - * @returns - */ -const deleteIntegrationAuth = ({ integrationAuthId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/" + integrationAuthId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete an integration authorization"); - } - }); -}; - -export default deleteIntegrationAuth; diff --git a/frontend/pages/api/integrations/DeleteIntegrationAuth.ts b/frontend/pages/api/integrations/DeleteIntegrationAuth.ts new file mode 100644 index 0000000000..3a2da2cbf3 --- /dev/null +++ b/frontend/pages/api/integrations/DeleteIntegrationAuth.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationAuthId: string; +} + +/** + * This route deletes an integration authorization from a certain project + * @param {*} integrationAuthId + * @returns + */ +const deleteIntegrationAuth = ({ integrationAuthId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration-auth/' + integrationAuthId, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete an integration authorization'); + } + }); +}; + +export default deleteIntegrationAuth; diff --git a/frontend/pages/api/integrations/GetIntegrationApps.js b/frontend/pages/api/integrations/GetIntegrationApps.js deleted file mode 100644 index dc0bfb4b69..0000000000 --- a/frontend/pages/api/integrations/GetIntegrationApps.js +++ /dev/null @@ -1,21 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const getIntegrationApps = ({ integrationAuthId }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/" + integrationAuthId + "/apps", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).apps; - } else { - console.log("Failed to get available apps for an integration"); - } - }); -}; - -export default getIntegrationApps; diff --git a/frontend/pages/api/integrations/GetIntegrationApps.ts b/frontend/pages/api/integrations/GetIntegrationApps.ts new file mode 100644 index 0000000000..5597c24b15 --- /dev/null +++ b/frontend/pages/api/integrations/GetIntegrationApps.ts @@ -0,0 +1,25 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationAuthId: string; +} + +const getIntegrationApps = ({ integrationAuthId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/integration-auth/' + integrationAuthId + '/apps', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).apps; + } else { + console.log('Failed to get available apps for an integration'); + } + }); +}; + +export default getIntegrationApps; diff --git a/frontend/pages/api/integrations/GetIntegrations.js b/frontend/pages/api/integrations/GetIntegrations.js deleted file mode 100644 index 401c80d3b3..0000000000 --- a/frontend/pages/api/integrations/GetIntegrations.js +++ /dev/null @@ -1,18 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -const getIntegrations = () => { - return SecurityClient.fetchCall("/api/v1/integration/integrations", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return (await res.json()).integrations; - } else { - console.log("Failed to get project integrations"); - } - }); -}; - -export default getIntegrations; diff --git a/frontend/pages/api/integrations/GetIntegrations.ts b/frontend/pages/api/integrations/GetIntegrations.ts new file mode 100644 index 0000000000..c189010a4d --- /dev/null +++ b/frontend/pages/api/integrations/GetIntegrations.ts @@ -0,0 +1,18 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +const getIntegrations = () => { + return SecurityClient.fetchCall('/api/v1/integration/integrations', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).integrations; + } else { + console.log('Failed to get project integrations'); + } + }); +}; + +export default getIntegrations; diff --git a/frontend/pages/api/integrations/StartIntegration.js b/frontend/pages/api/integrations/StartIntegration.js deleted file mode 100644 index a4e8b0b02d..0000000000 --- a/frontend/pages/api/integrations/StartIntegration.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route starts the integration after teh default one if gonna set up. - * @param {*} integrationId - * @returns - */ -const startIntegration = ({ integrationId, appName, environment }) => { - return SecurityClient.fetchCall( - "/api/v1/integration/" + integrationId, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - update: { - app: appName, - environment, - isActive: true, - }, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to start an integration"); - } - }); -}; - -export default startIntegration; diff --git a/frontend/pages/api/integrations/StartIntegration.ts b/frontend/pages/api/integrations/StartIntegration.ts new file mode 100644 index 0000000000..bef82e7e17 --- /dev/null +++ b/frontend/pages/api/integrations/StartIntegration.ts @@ -0,0 +1,36 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + integrationId: string; + appName: string; + environment: string; +} + +/** + * This route starts the integration after teh default one if gonna set up. + * @param {*} integrationId + * @returns + */ +const startIntegration = ({ integrationId, appName, environment }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration/' + integrationId, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + update: { + app: appName, + environment, + isActive: true + } + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to start an integration'); + } + }); +}; + +export default startIntegration; diff --git a/frontend/pages/api/integrations/authorizeIntegration.js b/frontend/pages/api/integrations/authorizeIntegration.js deleted file mode 100644 index b9a1d3995d..0000000000 --- a/frontend/pages/api/integrations/authorizeIntegration.js +++ /dev/null @@ -1,31 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This is the first step of the change password process (pake) - * @param {*} clientPublicKey - * @returns - */ -const AuthorizeIntegration = ({ workspaceId, code, integration }) => { - return SecurityClient.fetchCall( - "/api/v1/integration-auth/oauth-token", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - workspaceId, - code, - integration, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to authorize the integration"); - } - }); -}; - -export default AuthorizeIntegration; diff --git a/frontend/pages/api/integrations/authorizeIntegration.ts b/frontend/pages/api/integrations/authorizeIntegration.ts new file mode 100644 index 0000000000..1454bde4d2 --- /dev/null +++ b/frontend/pages/api/integrations/authorizeIntegration.ts @@ -0,0 +1,36 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; + code: string; + integration: string; +} +/** + * This is the first step of the change password process (pake) + * @param {object} obj + * @param {object} obj.workspaceId - project id for which we want to authorize the integration + * @param {object} obj.code + * @param {object} obj.integration - integration which a user is trying to turn on + * @returns + */ +const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => { + return SecurityClient.fetchCall('/api/v1/integration-auth/oauth-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + workspaceId, + code, + integration + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to authorize the integration'); + } + }); +}; + +export default AuthorizeIntegration; diff --git a/frontend/pages/api/integrations/getWorkspaceAuthorizations.js b/frontend/pages/api/integrations/getWorkspaceAuthorizations.js deleted file mode 100644 index ad8c4732b9..0000000000 --- a/frontend/pages/api/integrations/getWorkspaceAuthorizations.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets authorizations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceAuthorizations = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/authorizations", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).authorizations; - } else { - console.log("Failed to get project authorizations"); - } - }); -}; - -export default getWorkspaceAuthorizations; diff --git a/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts b/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts new file mode 100644 index 0000000000..f1b4065550 --- /dev/null +++ b/frontend/pages/api/integrations/getWorkspaceAuthorizations.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; +} + +/** + * This route gets authorizations of a certain project (Heroku, etc.) + * @param {*} workspaceId + * @returns + */ +const getWorkspaceAuthorizations = ({ workspaceId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/authorizations', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).authorizations; + } else { + console.log('Failed to get project authorizations'); + } + }); +}; + +export default getWorkspaceAuthorizations; diff --git a/frontend/pages/api/integrations/getWorkspaceIntegrations.js b/frontend/pages/api/integrations/getWorkspaceIntegrations.js deleted file mode 100644 index 22470e4bea..0000000000 --- a/frontend/pages/api/integrations/getWorkspaceIntegrations.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets integrations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceIntegrations = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/integrations", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).integrations; - } else { - console.log("Failed to get the project integrations"); - } - }); -}; - -export default getWorkspaceIntegrations; diff --git a/frontend/pages/api/integrations/getWorkspaceIntegrations.ts b/frontend/pages/api/integrations/getWorkspaceIntegrations.ts new file mode 100644 index 0000000000..a33256749d --- /dev/null +++ b/frontend/pages/api/integrations/getWorkspaceIntegrations.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + workspaceId: string; +} + +/** + * This route gets integrations of a certain project (Heroku, etc.) + * @param {*} workspaceId + * @returns + */ +const getWorkspaceIntegrations = ({ workspaceId }: Props) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/integrations', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).integrations; + } else { + console.log('Failed to get the project integrations'); + } + }); +}; + +export default getWorkspaceIntegrations; diff --git a/frontend/pages/api/organization/GetOrg.ts b/frontend/pages/api/organization/GetOrg.ts index ecb07bd2d3..5e56e5c727 100644 --- a/frontend/pages/api/organization/GetOrg.ts +++ b/frontend/pages/api/organization/GetOrg.ts @@ -1,21 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get info about a certain org * @param {string} orgId - the organization ID * @returns */ -const getOrganization = ({ orgId }: { orgId: string; }) => { - return SecurityClient.fetchCall("/api/v1/organization/" + orgId, { - method: "GET", +const getOrganization = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId, { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).organization; } else { - console.log("Failed to get org info"); + console.log('Failed to get org info'); } }); }; diff --git a/frontend/pages/api/organization/GetOrgProjects.js b/frontend/pages/api/organization/GetOrgProjects.js deleted file mode 100644 index 656fea18f9..0000000000 --- a/frontend/pages/api/organization/GetOrgProjects.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get all the users in an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationProjects = (req, res) => { - return SecurityClient.fetchCall( - "/api/organization/" + req.orgId + "/workspaces", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspaces; - } else { - console.log("Failed to get projects for an org"); - } - }); -}; - -export default getOrganizationProjects; diff --git a/frontend/pages/api/organization/GetOrgProjects.ts b/frontend/pages/api/organization/GetOrgProjects.ts new file mode 100644 index 0000000000..9546944863 --- /dev/null +++ b/frontend/pages/api/organization/GetOrgProjects.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get all the users in an org. + * @param {*} req + * @param {*} res + * @returns + */ + +// TODO: this file is not used anywhere +const getOrganizationProjects = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/organization/' + req.orgId + '/workspaces', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspaces; + } else { + console.log('Failed to get projects for an org'); + } + }); +}; + +export default getOrganizationProjects; diff --git a/frontend/pages/api/organization/GetOrgSubscription.js b/frontend/pages/api/organization/GetOrgSubscription.js deleted file mode 100644 index 97c6f4e5ce..0000000000 --- a/frontend/pages/api/organization/GetOrgSubscription.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get the current subscription of an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationSubscriptions = (req, res) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + req.orgId + "/subscriptions", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).subscriptions; - } else { - console.log("Failed to get org subscriptions"); - } - }); -}; - -export default getOrganizationSubscriptions; diff --git a/frontend/pages/api/organization/GetOrgSubscription.ts b/frontend/pages/api/organization/GetOrgSubscription.ts new file mode 100644 index 0000000000..c93679cdc8 --- /dev/null +++ b/frontend/pages/api/organization/GetOrgSubscription.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get the current subscription of an org. + * @param {*} req + * @param {*} res + * @returns + */ +const getOrganizationSubscriptions = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + req.orgId + '/subscriptions', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).subscriptions; + } else { + console.log('Failed to get org subscriptions'); + } + }); +}; + +export default getOrganizationSubscriptions; diff --git a/frontend/pages/api/organization/GetOrgUserProjects.js b/frontend/pages/api/organization/GetOrgUserProjects.js deleted file mode 100644 index c2d751f64d..0000000000 --- a/frontend/pages/api/organization/GetOrgUserProjects.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us get all the projects of a certain user in an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationUserProjects = (req) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + req.orgId + "/my-workspaces", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).workspaces; - } else { - console.log("Failed to get projects of a user in an org"); - } - }); -}; - -export default getOrganizationUserProjects; diff --git a/frontend/pages/api/organization/GetOrgUserProjects.ts b/frontend/pages/api/organization/GetOrgUserProjects.ts new file mode 100644 index 0000000000..872b8d7818 --- /dev/null +++ b/frontend/pages/api/organization/GetOrgUserProjects.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us get all the projects of a certain user in an org. + * @param {*} req + * @param {*} res + * @returns + */ +const getOrganizationUserProjects = (req: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + req.orgId + '/my-workspaces', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).workspaces; + } else { + console.log('Failed to get projects of a user in an org'); + } + }); +}; + +export default getOrganizationUserProjects; diff --git a/frontend/pages/api/organization/GetOrgUsers.ts b/frontend/pages/api/organization/GetOrgUsers.ts index 53b9518cf8..f1aaa7209f 100644 --- a/frontend/pages/api/organization/GetOrgUsers.ts +++ b/frontend/pages/api/organization/GetOrgUsers.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get all the users in an org. @@ -6,20 +6,17 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {string} obj.orgId - organization Id * @returns */ -const getOrganizationUsers = ({ orgId }: { orgId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/users", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getOrganizationUsers = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId + '/users', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return (await res.json()).users; } else { - console.log("Failed to get org users"); + console.log('Failed to get org users'); } }); }; diff --git a/frontend/pages/api/organization/StripeRedirect.js b/frontend/pages/api/organization/StripeRedirect.js deleted file mode 100644 index e8911cf53c..0000000000 --- a/frontend/pages/api/organization/StripeRedirect.js +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route redirects the user to the right stripe billing page. - * @param {*} req - * @param {*} res - * @returns - */ -const StripeRedirect = ({ orgId }) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/customer-portal-session", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (window.location.href = (await res.json()).url); - } else { - console.log("Failed to redirect to Stripe"); - } - }); -}; - -export default StripeRedirect; diff --git a/frontend/pages/api/organization/StripeRedirect.ts b/frontend/pages/api/organization/StripeRedirect.ts new file mode 100644 index 0000000000..a9fe1f066d --- /dev/null +++ b/frontend/pages/api/organization/StripeRedirect.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route redirects the user to the right stripe billing page. + * @param {*} req + * @param {*} res + * @returns + */ +const StripeRedirect = ({ orgId }: { orgId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + orgId + '/customer-portal-session', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (window.location.href = (await res.json()).url); + } else { + console.log('Failed to redirect to Stripe'); + } + }); +}; + +export default StripeRedirect; diff --git a/frontend/pages/api/organization/addIncidentContact.js b/frontend/pages/api/organization/addIncidentContact.js deleted file mode 100644 index 0e6068a1a1..0000000000 --- a/frontend/pages/api/organization/addIncidentContact.js +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route add an incident contact email to a certain organization - * @param {*} param0 - * @returns - */ -const addIncidentContact = (organizationId, email) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizationId + "/incidentContactOrg", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to add an incident contact"); - } - }); -}; - -export default addIncidentContact; diff --git a/frontend/pages/api/organization/addIncidentContact.ts b/frontend/pages/api/organization/addIncidentContact.ts new file mode 100644 index 0000000000..d3676e6419 --- /dev/null +++ b/frontend/pages/api/organization/addIncidentContact.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route add an incident contact email to a certain organization + * @param {*} param0 + * @returns + */ +const addIncidentContact = (organizationId: string, email: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to add an incident contact'); + } + }); +}; + +export default addIncidentContact; diff --git a/frontend/pages/api/organization/addUserToOrg.js b/frontend/pages/api/organization/addUserToOrg.js deleted file mode 100644 index 15f22cff93..0000000000 --- a/frontend/pages/api/organization/addUserToOrg.js +++ /dev/null @@ -1,28 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function sends an email invite to a user to join an org - * @param {*} email - * @param {*} orgId - * @returns - */ -const addUserToOrg = (email, orgId) => { - return SecurityClient.fetchCall("/api/v1/invite-org/signup", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - inviteeEmail: email, - organizationId: orgId, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to add a user to an org"); - } - }); -}; - -export default addUserToOrg; diff --git a/frontend/pages/api/organization/addUserToOrg.ts b/frontend/pages/api/organization/addUserToOrg.ts new file mode 100644 index 0000000000..70dbad56cc --- /dev/null +++ b/frontend/pages/api/organization/addUserToOrg.ts @@ -0,0 +1,28 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function sends an email invite to a user to join an org + * @param {*} email + * @param {*} orgId + * @returns + */ +const addUserToOrg = (email: string, orgId: string) => { + return SecurityClient.fetchCall('/api/v1/invite-org/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inviteeEmail: email, + organizationId: orgId + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to add a user to an org'); + } + }); +}; + +export default addUserToOrg; diff --git a/frontend/pages/api/organization/deleteIncidentContact.js b/frontend/pages/api/organization/deleteIncidentContact.js deleted file mode 100644 index f6e57c5905..0000000000 --- a/frontend/pages/api/organization/deleteIncidentContact.js +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes an incident Contact from a certain organization - * @param {*} param0 - * @returns - */ -const deleteIncidentContact = (organizaionId, email) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizaionId + "/incidentContactOrg", - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete an incident contact"); - } - }); -}; - -export default deleteIncidentContact; diff --git a/frontend/pages/api/organization/deleteIncidentContact.ts b/frontend/pages/api/organization/deleteIncidentContact.ts new file mode 100644 index 0000000000..fd6374e9b6 --- /dev/null +++ b/frontend/pages/api/organization/deleteIncidentContact.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route deletes an incident Contact from a certain organization + * @param {*} param0 + * @returns + */ +const deleteIncidentContact = (organizationId: string, email: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete an incident contact'); + } + }); +}; + +export default deleteIncidentContact; diff --git a/frontend/pages/api/organization/deleteUserFromOrganization.js b/frontend/pages/api/organization/deleteUserFromOrganization.js deleted file mode 100644 index 66ea847939..0000000000 --- a/frontend/pages/api/organization/deleteUserFromOrganization.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain organization - * @param {*} membershipId - * @returns - */ -const deleteUserFromOrganization = (membershipId) => { - return SecurityClient.fetchCall( - "/api/v1/membership-org/" + membershipId, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a user from an org"); - } - }); -}; - -export default deleteUserFromOrganization; diff --git a/frontend/pages/api/organization/deleteUserFromOrganization.ts b/frontend/pages/api/organization/deleteUserFromOrganization.ts new file mode 100644 index 0000000000..988a09485a --- /dev/null +++ b/frontend/pages/api/organization/deleteUserFromOrganization.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function removes a certain member from a certain organization + * @param {*} membershipId + * @returns + */ +const deleteUserFromOrganization = (membershipId: string) => { + return SecurityClient.fetchCall('/api/v1/membership-org/' + membershipId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a user from an org'); + } + }); +}; + +export default deleteUserFromOrganization; diff --git a/frontend/pages/api/organization/getIncidentContacts.js b/frontend/pages/api/organization/getIncidentContacts.js deleted file mode 100644 index 4f3f61a487..0000000000 --- a/frontend/pages/api/organization/getIncidentContacts.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This routes gets all the incident contacts of a certain organization - * @param {*} workspaceId - * @returns - */ -const getIncidentContacts = (organizationId) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + organizationId + "/incidentContactOrg", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).incidentContactsOrg; - } else { - console.log("Failed to get incident contacts"); - } - }); -}; - -export default getIncidentContacts; diff --git a/frontend/pages/api/organization/getIncidentContacts.ts b/frontend/pages/api/organization/getIncidentContacts.ts new file mode 100644 index 0000000000..bb9c3613a7 --- /dev/null +++ b/frontend/pages/api/organization/getIncidentContacts.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This routes gets all the incident contacts of a certain organization + * @param {*} workspaceId + * @returns + */ +const getIncidentContacts = (organizationId: string) => { + return SecurityClient.fetchCall( + '/api/v1/organization/' + organizationId + '/incidentContactOrg', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).incidentContactsOrg; + } else { + console.log('Failed to get incident contacts'); + } + }); +}; + +export default getIncidentContacts; diff --git a/frontend/pages/api/organization/getOrgs.ts b/frontend/pages/api/organization/getOrgs.ts index cc655642ff..5f01bc4d16 100644 --- a/frontend/pages/api/organization/getOrgs.ts +++ b/frontend/pages/api/organization/getOrgs.ts @@ -1,20 +1,20 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get the all the orgs of a certain user. * @returns */ const getOrganizations = () => { - return SecurityClient.fetchCall("/api/v1/organization", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/organization', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).organizations; } else { - console.log("Failed to get orgs of a user"); + console.log('Failed to get orgs of a user'); } }); }; diff --git a/frontend/pages/api/organization/renameOrg.js b/frontend/pages/api/organization/renameOrg.js deleted file mode 100644 index a9d80a3a53..0000000000 --- a/frontend/pages/api/organization/renameOrg.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us rename a certain org. - * @param {*} req - * @param {*} res - * @returns - */ -const renameOrg = (orgId, newOrgName) => { - return SecurityClient.fetchCall( - "/api/v1/organization/" + orgId + "/name", - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: newOrgName, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to rename an organization"); - } - }); -}; - -export default renameOrg; diff --git a/frontend/pages/api/organization/renameOrg.ts b/frontend/pages/api/organization/renameOrg.ts new file mode 100644 index 0000000000..c5c0b4d65b --- /dev/null +++ b/frontend/pages/api/organization/renameOrg.ts @@ -0,0 +1,27 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us rename a certain org. + * @param {*} req + * @param {*} res + * @returns + */ +const renameOrg = (orgId: string, newOrgName: string) => { + return SecurityClient.fetchCall('/api/v1/organization/' + orgId + '/name', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: newOrgName + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to rename an organization'); + } + }); +}; + +export default renameOrg; diff --git a/frontend/pages/api/serviceToken/addServiceToken.js b/frontend/pages/api/serviceToken/addServiceToken.ts similarity index 51% rename from frontend/pages/api/serviceToken/addServiceToken.js rename to frontend/pages/api/serviceToken/addServiceToken.ts index 4c6031db84..5dad69f69c 100644 --- a/frontend/pages/api/serviceToken/addServiceToken.js +++ b/frontend/pages/api/serviceToken/addServiceToken.ts @@ -1,4 +1,14 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; + +interface Props { + name: string; + workspaceId: string; + environment: string; + expiresIn: number; + publicKey: string; + encryptedKey: string; + nonce: string; +} /** * This route gets service tokens for a specific user in a project @@ -12,12 +22,12 @@ const addServiceToken = ({ expiresIn, publicKey, encryptedKey, - nonce, -}) => { - return SecurityClient.fetchCall("/api/v1/service-token/", { - method: "POST", + nonce +}: Props) => { + return SecurityClient.fetchCall('/api/v1/service-token/', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ name, @@ -26,13 +36,13 @@ const addServiceToken = ({ expiresIn, publicKey, encryptedKey, - nonce, - }), + nonce + }) }).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return (await res.json()).token; } else { - console.log("Failed to add service tokens"); + console.log('Failed to add service tokens'); } }); }; diff --git a/frontend/pages/api/serviceToken/getServiceTokens.js b/frontend/pages/api/serviceToken/getServiceTokens.js deleted file mode 100644 index 79c6a7fc06..0000000000 --- a/frontend/pages/api/serviceToken/getServiceTokens.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route gets service tokens for a specific user in a project - * @param {*} param0 - * @returns - */ -const getServiceTokens = ({ workspaceId }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/service-tokens", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ).then(async (res) => { - if (res.status == 200) { - return (await res.json()).serviceTokens; - } else { - console.log("Failed to get service tokens"); - } - }); -}; - -export default getServiceTokens; diff --git a/frontend/pages/api/serviceToken/getServiceTokens.ts b/frontend/pages/api/serviceToken/getServiceTokens.ts new file mode 100644 index 0000000000..d2577cc831 --- /dev/null +++ b/frontend/pages/api/serviceToken/getServiceTokens.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route gets service tokens for a specific user in a project + * @param {*} param0 + * @returns + */ +const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/service-tokens', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).serviceTokens; + } else { + console.log('Failed to get service tokens'); + } + }); +}; + +export default getServiceTokens; diff --git a/frontend/pages/api/user/getUser.ts b/frontend/pages/api/user/getUser.ts index 4bfebc5fe4..7562b0f3be 100644 --- a/frontend/pages/api/user/getUser.ts +++ b/frontend/pages/api/user/getUser.ts @@ -1,19 +1,19 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route gets the information about a specific user. */ const getUser = () => { - return SecurityClient.fetchCall("/api/v1/user", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/user', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { return (await res.json()).user; } else { - console.log("Failed to get user info"); + console.log('Failed to get user info'); } }); }; diff --git a/frontend/pages/api/userActions/checkUserAction.js b/frontend/pages/api/userActions/checkUserAction.ts similarity index 51% rename from frontend/pages/api/userActions/checkUserAction.js rename to frontend/pages/api/userActions/checkUserAction.ts index dfe2178563..9941ae85a9 100644 --- a/frontend/pages/api/userActions/checkUserAction.js +++ b/frontend/pages/api/userActions/checkUserAction.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route registers a certain action for a user @@ -6,24 +6,24 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {*} workspaceId * @returns */ -const checkUserAction = ({ action }) => { +const checkUserAction = ({ action }: { action: string }) => { return SecurityClient.fetchCall( - "/api/v1/user-action" + - "?" + + '/api/v1/user-action' + + '?' + new URLSearchParams({ - action, + action }), { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } } ).then(async (res) => { - if (res.status == 200) { + if (res && res.status == 200) { return (await res.json()).userAction; } else { - console.log("Failed to check a user action"); + console.log('Failed to check a user action'); } }); }; diff --git a/frontend/pages/api/userActions/registerUserAction.js b/frontend/pages/api/userActions/registerUserAction.js deleted file mode 100644 index 619886d897..0000000000 --- a/frontend/pages/api/userActions/registerUserAction.js +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route registers a certain action for a user - * @param {*} action - * @returns - */ -const registerUserAction = ({ action }) => { - return SecurityClient.fetchCall("/api/v1/user-action", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - action, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to register a user action"); - } - }); -}; - -export default registerUserAction; diff --git a/frontend/pages/api/userActions/registerUserAction.ts b/frontend/pages/api/userActions/registerUserAction.ts new file mode 100644 index 0000000000..d8f4d41e6e --- /dev/null +++ b/frontend/pages/api/userActions/registerUserAction.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route registers a certain action for a user + * @param {*} action + * @returns + */ +const registerUserAction = ({ action }: { action: string }) => { + return SecurityClient.fetchCall('/api/v1/user-action', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to register a user action'); + } + }); +}; + +export default registerUserAction; diff --git a/frontend/pages/api/workspace/addUserToWorkspace.js b/frontend/pages/api/workspace/addUserToWorkspace.js deleted file mode 100644 index 86e991389f..0000000000 --- a/frontend/pages/api/workspace/addUserToWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function adds a user to a project - * @param {*} email - * @param {*} workspaceId - * @returns - */ -const addUserToWorkspace = (email, workspaceId) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/invite-signup", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return await res.json(); - } else { - console.log("Failed to add a user to project"); - } - }); -}; - -export default addUserToWorkspace; diff --git a/frontend/pages/api/workspace/addUserToWorkspace.ts b/frontend/pages/api/workspace/addUserToWorkspace.ts new file mode 100644 index 0000000000..e1efa51a1a --- /dev/null +++ b/frontend/pages/api/workspace/addUserToWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function adds a user to a project + * @param {*} email + * @param {*} workspaceId + * @returns + */ +const addUserToWorkspace = (email: string, workspaceId: string) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/invite-signup', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return await res.json(); + } else { + console.log('Failed to add a user to project'); + } + }); +}; + +export default addUserToWorkspace; diff --git a/frontend/pages/api/workspace/changeUserRoleInWorkspace.js b/frontend/pages/api/workspace/changeUserRoleInWorkspace.js deleted file mode 100644 index a93e22181a..0000000000 --- a/frontend/pages/api/workspace/changeUserRoleInWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function change the access of a user in a certain workspace - * @param {*} membershipId - * @param {*} role - * @returns - */ -const changeUserRoleInWorkspace = (membershipId, role) => { - return SecurityClient.fetchCall( - "/api/v1/membership/" + membershipId + "/change-role", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - role: role, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to change the user role in a project"); - } - }); -}; - -export default changeUserRoleInWorkspace; diff --git a/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts b/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts new file mode 100644 index 0000000000..9ac49440a7 --- /dev/null +++ b/frontend/pages/api/workspace/changeUserRoleInWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function change the access of a user in a certain workspace + * @param {*} membershipId + * @param {*} role + * @returns + */ +const changeUserRoleInWorkspace = (membershipId: string, role: string) => { + return SecurityClient.fetchCall( + '/api/v1/membership/' + membershipId + '/change-role', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + role: role + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to change the user role in a project'); + } + }); +}; + +export default changeUserRoleInWorkspace; diff --git a/frontend/pages/api/workspace/createWorkspace.ts b/frontend/pages/api/workspace/createWorkspace.ts index 0cedb0a57d..877ef7567f 100644 --- a/frontend/pages/api/workspace/createWorkspace.ts +++ b/frontend/pages/api/workspace/createWorkspace.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route creates a new workspace for a user within a certain organization. @@ -6,21 +6,27 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {string} organizationId - org ID * @returns */ -const createWorkspace = ( { workspaceName, organizationId }: { workspaceName: string; organizationId: string; }) => { - return SecurityClient.fetchCall("/api/v1/workspace", { - method: "POST", +const createWorkspace = ({ + workspaceName, + organizationId +}: { + workspaceName: string; + organizationId: string; +}) => { + return SecurityClient.fetchCall('/api/v1/workspace', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json' }, body: JSON.stringify({ workspaceName: workspaceName, - organizationId: organizationId, - }), + organizationId: organizationId + }) }).then(async (res) => { if (res?.status == 200) { return (await res.json()).workspace; } else { - console.log("Failed to create a project"); + console.log('Failed to create a project'); } }); }; diff --git a/frontend/pages/api/workspace/deleteUserFromWorkspace.js b/frontend/pages/api/workspace/deleteUserFromWorkspace.js deleted file mode 100644 index bad8beced1..0000000000 --- a/frontend/pages/api/workspace/deleteUserFromWorkspace.js +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain workspace - * @param {*} membershipId - * @returns - */ -const deleteUserFromWorkspace = (membershipId) => { - return SecurityClient.fetchCall("/api/v1/membership/" + membershipId, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a user from a project"); - } - }); -}; - -export default deleteUserFromWorkspace; diff --git a/frontend/pages/api/workspace/deleteUserFromWorkspace.ts b/frontend/pages/api/workspace/deleteUserFromWorkspace.ts new file mode 100644 index 0000000000..ea02f96d44 --- /dev/null +++ b/frontend/pages/api/workspace/deleteUserFromWorkspace.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This function removes a certain member from a certain workspace + * @param {*} membershipId + * @returns + */ +const deleteUserFromWorkspace = (membershipId: string) => { + return SecurityClient.fetchCall('/api/v1/membership/' + membershipId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a user from a project'); + } + }); +}; + +export default deleteUserFromWorkspace; diff --git a/frontend/pages/api/workspace/deleteWorkspace.js b/frontend/pages/api/workspace/deleteWorkspace.js deleted file mode 100644 index 33d34844d6..0000000000 --- a/frontend/pages/api/workspace/deleteWorkspace.js +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route deletes a specified workspace. - * @param {*} workspaceId - * @returns - */ -const deleteWorkspace = (workspaceId) => { - return SecurityClient.fetchCall("/api/v1/workspace/" + workspaceId, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to delete a project"); - } - }); -}; - -export default deleteWorkspace; diff --git a/frontend/pages/api/workspace/deleteWorkspace.ts b/frontend/pages/api/workspace/deleteWorkspace.ts new file mode 100644 index 0000000000..2bdb64cd31 --- /dev/null +++ b/frontend/pages/api/workspace/deleteWorkspace.ts @@ -0,0 +1,23 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route deletes a specified workspace. + * @param {*} workspaceId + * @returns + */ +const deleteWorkspace = (workspaceId: string) => { + return SecurityClient.fetchCall('/api/v1/workspace/' + workspaceId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete a project'); + } + }); +}; + +export default deleteWorkspace; diff --git a/frontend/pages/api/workspace/getLatestFileKey.ts b/frontend/pages/api/workspace/getLatestFileKey.ts index 86ecb74565..1ecd2035dc 100644 --- a/frontend/pages/api/workspace/getLatestFileKey.ts +++ b/frontend/pages/api/workspace/getLatestFileKey.ts @@ -1,24 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * Get the latest key pairs from a certain workspace * @param {string} workspaceId * @returns */ -const getLatestFileKey = ({ workspaceId } : { workspaceId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/key/" + workspaceId + "/latest", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getLatestFileKey = ({ workspaceId }: { workspaceId: string }) => { + return SecurityClient.fetchCall('/api/v1/key/' + workspaceId + '/latest', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return await res.json(); } else { - console.log("Failed to get the latest key pairs for a certain project"); + console.log('Failed to get the latest key pairs for a certain project'); } }); }; diff --git a/frontend/pages/api/workspace/getProjectInfo.ts b/frontend/pages/api/workspace/getProjectInfo.ts index c6eef9dcea..a1ab0438c5 100644 --- a/frontend/pages/api/workspace/getProjectInfo.ts +++ b/frontend/pages/api/workspace/getProjectInfo.ts @@ -1,24 +1,21 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get the information of a certain project. * @param {*} projectId - project ID (we renamed workspaces to projects in the app) * @returns */ -const getProjectInfo = ({ projectId }: { projectId: string; }) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + projectId, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, +const getProjectInfo = ({ projectId }: { projectId: string }) => { + return SecurityClient.fetchCall('/api/v1/workspace/' + projectId, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } - ).then(async (res) => { + }).then(async (res) => { if (res?.status == 200) { return (await res.json()).workspace; } else { - console.log("Failed to get project info"); + console.log('Failed to get project info'); } }); }; diff --git a/frontend/pages/api/workspace/getWorkspaceUsers.ts b/frontend/pages/api/workspace/getWorkspaceUsers.ts index 9805c26955..c4f00eb0da 100644 --- a/frontend/pages/api/workspace/getWorkspaceUsers.ts +++ b/frontend/pages/api/workspace/getWorkspaceUsers.ts @@ -1,24 +1,24 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; /** * This route lets us get all the users in the workspace. * @param {string} workspaceId - workspace ID * @returns */ -const getWorkspaceUsers = ({ workspaceId }: { workspaceId: string; }) => { +const getWorkspaceUsers = ({ workspaceId }: { workspaceId: string }) => { return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/users", + '/api/v1/workspace/' + workspaceId + '/users', { - method: "GET", + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } } ).then(async (res) => { if (res?.status == 200) { return (await res.json()).users; } else { - console.log("Failed to get Project Users"); + console.log('Failed to get Project Users'); } }); }; diff --git a/frontend/pages/api/workspace/getWorkspaces.ts b/frontend/pages/api/workspace/getWorkspaces.ts index a77a04751b..1bbb42c7a7 100644 --- a/frontend/pages/api/workspace/getWorkspaces.ts +++ b/frontend/pages/api/workspace/getWorkspaces.ts @@ -1,4 +1,4 @@ -import SecurityClient from "~/utilities/SecurityClient"; +import SecurityClient from '~/utilities/SecurityClient'; interface Workspace { __v: number; @@ -12,18 +12,18 @@ interface Workspace { * @returns */ const getWorkspaces = () => { - return SecurityClient.fetchCall("/api/v1/workspace", { - method: "GET", + return SecurityClient.fetchCall('/api/v1/workspace', { + method: 'GET', headers: { - "Content-Type": "application/json", - }, + 'Content-Type': 'application/json' + } }).then(async (res) => { if (res?.status == 200) { const data = (await res.json()) as unknown as { workspaces: Workspace[] }; return data.workspaces; } - throw new Error("Failed to get projects"); + throw new Error('Failed to get projects'); }); }; diff --git a/frontend/pages/api/workspace/renameWorkspace.js b/frontend/pages/api/workspace/renameWorkspace.js deleted file mode 100644 index 6dd297f820..0000000000 --- a/frontend/pages/api/workspace/renameWorkspace.js +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route lets us rename a certain workspace. - * @param {*} req - * @param {*} res - * @returns - */ -const renameWorkspace = (workspaceId, newWorkspaceName) => { - return SecurityClient.fetchCall( - "/api/v1/workspace/" + workspaceId + "/name", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: newWorkspaceName, - }), - } - ).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to rename a project"); - } - }); -}; - -export default renameWorkspace; diff --git a/frontend/pages/api/workspace/renameWorkspace.ts b/frontend/pages/api/workspace/renameWorkspace.ts new file mode 100644 index 0000000000..a25d2068c1 --- /dev/null +++ b/frontend/pages/api/workspace/renameWorkspace.ts @@ -0,0 +1,30 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route lets us rename a certain workspace. + * @param {*} req + * @param {*} res + * @returns + */ +const renameWorkspace = (workspaceId: string, newWorkspaceName: string) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/name', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: newWorkspaceName + }) + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to rename a project'); + } + }); +}; + +export default renameWorkspace; diff --git a/frontend/pages/api/workspace/uploadKeys.js b/frontend/pages/api/workspace/uploadKeys.js deleted file mode 100644 index 37b384f304..0000000000 --- a/frontend/pages/api/workspace/uploadKeys.js +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "~/utilities/SecurityClient"; - -/** - * This route uplods the keys in an encrypted format. - * @param {*} workspaceId - * @param {*} userId - * @param {*} encryptedKey - * @param {*} nonce - * @returns - */ -const uploadKeys = (workspaceId, userId, encryptedKey, nonce) => { - return SecurityClient.fetchCall("/api/v1/key/" + workspaceId, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - key: { - userId: userId, - encryptedKey: encryptedKey, - nonce: nonce, - }, - }), - }).then(async (res) => { - if (res.status == 200) { - return res; - } else { - console.log("Failed to upload keys for a new user"); - } - }); -}; - -export default uploadKeys; diff --git a/frontend/pages/api/workspace/uploadKeys.ts b/frontend/pages/api/workspace/uploadKeys.ts new file mode 100644 index 0000000000..2f791735d9 --- /dev/null +++ b/frontend/pages/api/workspace/uploadKeys.ts @@ -0,0 +1,38 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +/** + * This route uplods the keys in an encrypted format. + * @param {*} workspaceId + * @param {*} userId + * @param {*} encryptedKey + * @param {*} nonce + * @returns + */ +const uploadKeys = ( + workspaceId: string, + userId: string, + encryptedKey: string, + nonce: string +) => { + return SecurityClient.fetchCall('/api/v1/key/' + workspaceId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key: { + userId: userId, + encryptedKey: encryptedKey, + nonce: nonce + } + }) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to upload keys for a new user'); + } + }); +}; + +export default uploadKeys; diff --git a/frontend/pages/dashboard/[id].js b/frontend/pages/dashboard/[id].js index 187f0c3409..4c40258c01 100644 --- a/frontend/pages/dashboard/[id].js +++ b/frontend/pages/dashboard/[id].js @@ -1,7 +1,7 @@ -import React, { Fragment, useCallback, useEffect, useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; +import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; import { faArrowDownAZ, faArrowDownZA, @@ -18,29 +18,29 @@ import { faPerson, faPlus, faShuffle, - faX, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Menu, Transition } from "@headlessui/react"; + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Menu, Transition } from '@headlessui/react'; -import Button from "~/components/basic/buttons/Button"; -import ListBox from "~/components/basic/Listbox"; -import BottonRightPopup from "~/components/basic/popups/BottomRightPopup"; -import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; -import DashboardInputField from "~/components/dashboard/DashboardInputField"; -import DropZone from "~/components/dashboard/DropZone"; -import NavHeader from "~/components/navigation/NavHeader"; -import getSecretsForProject from "~/components/utilities/secrets/getSecretsForProject"; -import pushKeys from "~/components/utilities/secrets/pushKeys"; -import pushKeysIntegration from "~/components/utilities/secrets/pushKeysIntegration"; -import guidGenerator from "~/utilities/randomId"; +import Button from '~/components/basic/buttons/Button'; +import ListBox from '~/components/basic/Listbox'; +import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; +import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; +import DashboardInputField from '~/components/dashboard/DashboardInputField'; +import DropZone from '~/components/dashboard/DropZone'; +import NavHeader from '~/components/navigation/NavHeader'; +import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; +import pushKeys from '~/components/utilities/secrets/pushKeys'; +import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration'; +import guidGenerator from '~/utilities/randomId'; -import { envMapping } from "../../public/data/frequentConstants"; -import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; -import getUser from "../api/user/getUser"; -import checkUserAction from "../api/userActions/checkUserAction"; -import registerUserAction from "../api/userActions/registerUserAction"; -import getWorkspaces from "../api/workspace/getWorkspaces"; +import { envMapping } from '../../public/data/frequentConstants'; +import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations'; +import getUser from '../api/user/getUser'; +import checkUserAction from '../api/userActions/checkUserAction'; +import registerUserAction from '../api/userActions/registerUserAction'; +import getWorkspaces from '../api/workspace/getWorkspaces'; /** * This component represent a single row for an environemnt variable on the dashboard @@ -61,7 +61,7 @@ const KeyPair = ({ modifyValue, modifyVisibility, isBlurred, - duplicates, + duplicates }) => { const [randomStringLength, setRandomStringLength] = useState(32); @@ -210,21 +210,21 @@ export default function Dashboard() { const [fileState, setFileState] = useState([]); const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [workspaceId, setWorkspaceId] = useState(""); + const [workspaceId, setWorkspaceId] = useState(''); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [env, setEnv] = useState( - router.asPath.split("?").length == 1 - ? "Development" - : Object.keys(envMapping).includes(router.asPath.split("?")[1]) - ? router.asPath.split("?")[1] - : "Development" + router.asPath.split('?').length == 1 + ? 'Development' + : Object.keys(envMapping).includes(router.asPath.split('?')[1]) + ? router.asPath.split('?')[1] + : 'Development' ); const [isNew, setIsNew] = useState(false); - const [searchKeys, setSearchKeys] = useState(""); + const [searchKeys, setSearchKeys] = useState(''); const [errorDragAndDrop, setErrorDragAndDrop] = useState(false); const [projectIdCopied, setProjectIdCopied] = useState(false); - const [sortMethod, setSortMethod] = useState("alphabetical"); + const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); @@ -248,16 +248,16 @@ export default function Dashboard() { // prompt the user if they try and leave with unsaved changes useEffect(() => { const warningText = - "Do you want to save your results before leaving this page?"; + 'Do you want to save your results before leaving this page?'; const handleWindowClose = (e) => { if (!buttonReady) return; e.preventDefault(); return (e.returnValue = warningText); }; - window.addEventListener("beforeunload", handleWindowClose); + window.addEventListener('beforeunload', handleWindowClose); // router.events.on('routeChangeStart', beforeRouteHandler); return () => { - window.removeEventListener("beforeunload", handleWindowClose); + window.removeEventListener('beforeunload', handleWindowClose); // router.events.off('routeChangeStart', beforeRouteHandler); }; }, [buttonReady]); @@ -281,13 +281,13 @@ export default function Dashboard() { let userWorkspaces = await getWorkspaces(); const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); if ( - !listWorkspaces.includes(router.asPath.split("/")[2].split("?")[0]) + !listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0]) ) { - router.push("/dashboard/" + listWorkspaces[0]); + router.push('/dashboard/' + listWorkspaces[0]); } - if (env != router.asPath.split("?")[1]) { - router.push(router.asPath.split("?")[0] + "?" + env); + if (env != router.asPath.split('?')[1]) { + router.push(router.asPath.split('?')[0] + '?' + env); } setBlurred(true); setWorkspaceId(router.query.id); @@ -297,7 +297,7 @@ export default function Dashboard() { setFileState, setIsKeyAvailable, setData, - workspaceId: router.query.id, + workspaceId: router.query.id }); const user = await getUser(); @@ -308,11 +308,11 @@ export default function Dashboard() { ); let userAction = await checkUserAction({ - action: "first_time_secrets_pushed", + action: 'first_time_secrets_pushed' }); setHasUserEverPushed(userAction ? true : false); } catch (error) { - console.log("Error", error); + console.log('Error', error); setData([]); } })(); @@ -395,15 +395,15 @@ export default function Dashboard() { if (nameErrors) { return createNotification({ - text: "Solve all name errors first!", - type: "error", + text: 'Solve all name errors before saving secrets.', + type: 'error' }); } if (duplicatesExist) { return createNotification({ - text: "Your secrets weren't saved; please fix the conflicts first.", - type: "error", + text: 'Remove duplicated secret names before saving.', + type: 'error' }); } @@ -416,7 +416,7 @@ export default function Dashboard() { * If there are any, update environment variables for those integrations */ let integrations = await getWorkspaceIntegrations({ - workspaceId: router.query.id, + workspaceId: router.query.id }); integrations.map(async (integration) => { if ( @@ -429,7 +429,7 @@ export default function Dashboard() { ); await pushKeysIntegration({ obj: objIntegration, - integrationId: integration._id, + integrationId: integration._id }); } }); @@ -437,7 +437,7 @@ export default function Dashboard() { // If this user has never saved environment variables before, show them a prompt to read docs if (!hasUserEverPushed) { setCheckDocsPopUpVisible(true); - await registerUserAction({ action: "first_time_secrets_pushed" }); + await registerUserAction({ action: 'first_time_secrets_pushed' }); } }; @@ -452,7 +452,7 @@ export default function Dashboard() { const sortValuesHandler = () => { const sortedData = data.sort((a, b) => - sortMethod == "alphabetical" + sortMethod == 'alphabetical' ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) ).map((item, index) => { @@ -466,12 +466,12 @@ export default function Dashboard() { // This function downloads the secrets as a .env file const download = () => { - const file = data.map((item) => [item.key, item.value].join("=")).join("\n"); + const file = data.map((item) => [item.key, item.value].join('=')).join('\n'); const blob = new Blob([file]); const fileDownloadUrl = URL.createObjectURL(blob); - let alink = document.createElement("a"); + let alink = document.createElement('a'); alink.href = fileDownloadUrl; - alink.download = envMapping[env] + ".env"; + alink.download = envMapping[env] + '.env'; alink.click(); }; @@ -483,7 +483,7 @@ export default function Dashboard() { * This function copies the project id to the clipboard */ function copyToClipboard() { - var copyText = document.getElementById("myInput"); + var copyText = document.getElementById('myInput'); copyText.select(); copyText.setSelectionRange(0, 99999); // For mobile devices @@ -526,7 +526,7 @@ export default function Dashboard() { {data?.length == 0 && ( setSearchKeys(e.target.value)} - placeholder={"Search keys..."} + placeholder={'Search keys...'} />
@@ -604,7 +604,7 @@ export default function Dashboard() { color="mineshaft" size="icon-md" icon={ - sortMethod == "alphabetical" + sortMethod == 'alphabetical' ? faArrowDownAZ : faArrowDownZA } @@ -675,7 +675,7 @@ export default function Dashboard() { keyPair.key .toLowerCase() .includes(searchKeys.toLowerCase()) && - keyPair.type == "personal" + keyPair.type == 'personal' )?.map((keyPair) => (
8 ? "h-3/4" : "h-min" + data?.length > 8 ? 'h-3/4' : 'h-min' }`} >
@@ -723,7 +723,7 @@ export default function Dashboard() { keyPair.key .toLowerCase() .includes(searchKeys.toLowerCase()) && - keyPair.type == "shared" + keyPair.type == 'shared' )?.map((keyPair) => ( )} {fileState.message == - "Failed membership validation for workspace" && ( + 'Failed membership validation for workspace' && (

You are not authorized to view this project.

)} - {fileState.message == "Access needed to pull the latest file" || + {fileState.message == 'Access needed to pull the latest file' || (!isKeyAvailable && ( <> { +const learningItem = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { if (link) { return ( - -
{ - if (userAction) { - await registerUserAction({ - action: userAction - }) + +
{ + if (userAction && userAction != 'first_time_secrets_pushed') { + await registerUserAction({ + action: userAction + }); } }} - className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"> + className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer" + >
- {complete && -
- -
} + {complete && ( +
+ +
+ )}
{text}
{subText}
-
- {complete ? "Complete!" : "About " + time} +
+ {complete ? 'Complete!' : 'About ' + time}
- {complete &&
} + {complete && ( +
+ )}
); } else { return ( -
{ - if (userAction) { - await registerUserAction({ - action: userAction - }) - } - }} - className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"> +
{ + if (userAction) { + await registerUserAction({ + action: userAction + }); + } + }} + className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer" + >
- {complete && -
- -
} + {complete && ( +
+ +
+ )}
{text}
{subText}
-
- {complete ? "Complete!" : "About " + time} +
+ {complete ? 'Complete!' : 'About ' + time}
- {complete &&
} + {complete && ( +
+ )}
); } -} +}; /** - * This tab is called Home because in the future it will include some company news, - * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction + * This tab is called Home because in the future it will include some company news, + * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction * for the new users */ export default function Home() { @@ -94,46 +136,90 @@ export default function Home() { const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); const [hasUserStarred, setHasUserStarred] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); const [usersInOrg, setUsersInOrg] = useState(false); useEffect(() => { - const checkUserActionsFunction = async () => { - const userActionSlack = await checkUserAction({ - action: "slack_cta_clicked", - }); - setHasUserClickedSlack(userActionSlack ? true : false); - - const userActionIntro = await checkUserAction({ - action: "intro_cta_clicked", - }); - setHasUserClickedIntro(userActionIntro ? true : false); - - const userActionStar = await checkUserAction({ - action: "star_cta_clicked", - }); - setHasUserStarred(userActionStar ? true : false); - - const orgId = localStorage.getItem("orgData.id"); - const orgUsers = await getOrganizationUsers({ - orgId: orgId ? orgId : "", - }); - setUsersInOrg(orgUsers.length > 1) - }; - checkUserActionsFunction(); + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setHasUserStarred, + setUsersInOrg + }); }, []); return (
-
Your quick start guide
-
Click on the items below and follow the instructions.
- {learningItem({ text: "Get to know Infisical", subText: "", complete: hasUserClickedIntro, icon: faHandPeace, time: "3 min", userAction: "intro_cta_clicked", link: "https://www.youtube.com/watch?v=JS3OKYU2078" })} - {learningItem({ text: "Add your secrets", subText: "Click to see example secrets, and add your own.", complete: false, icon: faPlus, time: "2 min", userAction: "first_time_secrets_pushed", link: "/dashboard/" + router.query.id })} - {learningItem({ text: "Inject secrets locally", subText: "Replace .env files with a more secure an efficient alternative.", complete: false, icon: faNetworkWired, time: "8 min", link: "https://infisical.com/docs/getting-started/quickstart" })} - {learningItem({ text: "Integrate Infisical with your infrastructure", subText: "Only a few integrations are currently available. Many more coming soon!", complete: false, icon: faPlug, time: "15 min", link: "https://infisical.com/docs/integrations/overview" })} - {learningItem({ text: "Invite your teammates", subText: "", complete: usersInOrg, icon: faUserPlus, time: "2 min", link: "/settings/org/" + router.query.id + "?invite" })} - {learningItem({ text: "Join Infisical Slack", subText: "Have any questions? Ask us!", complete: hasUserClickedSlack, icon: faSlack, time: "1 min", userAction: "slack_cta_clicked", link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g" })} - {learningItem({ text: "Star Infisical on GitHub", subText: "Like what we're doing? You know what to do! :)", complete: hasUserStarred, icon: faStar, time: "1 min", userAction: "star_cta_clicked", link: "https://github.com/Infisical/infisical" })} +
+ Your quick start guide +
+
+ Click on the items below and follow the instructions. +
+ {learningItem({ + text: 'Get to know Infisical', + subText: '', + complete: hasUserClickedIntro, + icon: faHandPeace, + time: '3 min', + userAction: 'intro_cta_clicked', + link: 'https://www.youtube.com/watch?v=JS3OKYU2078' + })} + {learningItem({ + text: 'Add your secrets', + subText: 'Click to see example secrets, and add your own.', + complete: hasUserPushedSecrets, + icon: faPlus, + time: '2 min', + userAction: 'first_time_secrets_pushed', + link: '/dashboard/' + router.query.id + })} + {learningItem({ + text: 'Inject secrets locally', + subText: + 'Replace .env files with a more secure an efficient alternative.', + complete: false, + icon: faNetworkWired, + time: '8 min', + link: 'https://infisical.com/docs/getting-started/quickstart' + })} + {learningItem({ + text: 'Integrate Infisical with your infrastructure', + subText: + 'Only a few integrations are currently available. Many more coming soon!', + complete: false, + icon: faPlug, + time: '15 min', + link: 'https://infisical.com/docs/integrations/overview' + })} + {learningItem({ + text: 'Invite your teammates', + subText: '', + complete: usersInOrg, + icon: faUserPlus, + time: '2 min', + link: '/settings/org/' + router.query.id + '?invite' + })} + {learningItem({ + text: 'Join Infisical Slack', + subText: 'Have any questions? Ask us!', + complete: hasUserClickedSlack, + icon: faSlack, + time: '1 min', + userAction: 'slack_cta_clicked', + link: 'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g' + })} + {learningItem({ + text: 'Star Infisical on GitHub', + subText: "Like what we're doing? You know what to do! :)", + complete: hasUserStarred, + icon: faStar, + time: '1 min', + userAction: 'star_cta_clicked', + link: 'https://github.com/Infisical/infisical' + })}
); diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index 898882e311..0a0a2a9756 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -1,36 +1,36 @@ -import React, { useEffect, useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; +import React, { useEffect, useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; import { faArrowRight, faCheck, faRotate, - faX, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import ListBox from "~/components/basic/Listbox"; -import NavHeader from "~/components/navigation/NavHeader"; -import getSecretsForProject from "~/components/utilities/secrets/getSecretsForProject"; -import pushKeysIntegration from "~/components/utilities/secrets/pushKeysIntegration"; -import guidGenerator from "~/utilities/randomId"; +import Button from '~/components/basic/buttons/Button'; +import ListBox from '~/components/basic/Listbox'; +import NavHeader from '~/components/navigation/NavHeader'; +import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; +import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration'; +import guidGenerator from '~/utilities/randomId'; -import { - envMapping, - frameworks, +import { + envMapping, + frameworks, reverseEnvMapping -} from "../../public/data/frequentConstants"; -import deleteIntegration from "../api/integrations/DeleteIntegration"; -import deleteIntegrationAuth from "../api/integrations/DeleteIntegrationAuth"; -import getIntegrationApps from "../api/integrations/GetIntegrationApps"; -import getIntegrations from "../api/integrations/GetIntegrations"; -import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations"; -import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; -import startIntegration from "../api/integrations/StartIntegration"; +} from '../../public/data/frequentConstants'; +import deleteIntegration from '../api/integrations/DeleteIntegration'; +import deleteIntegrationAuth from '../api/integrations/DeleteIntegrationAuth'; +import getIntegrationApps from '../api/integrations/GetIntegrationApps'; +import getIntegrations from '../api/integrations/GetIntegrations'; +import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations'; +import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations'; +import startIntegration from '../api/integrations/StartIntegration'; -const crypto = require("crypto"); +const crypto = require('crypto'); const Integration = ({ projectIntegration }) => { const [integrationEnvironment, setIntegrationEnvironment] = useState( @@ -47,7 +47,7 @@ const Integration = ({ projectIntegration }) => { useEffect(async () => { const tempHerokuApps = await getIntegrationApps({ - integrationAuthId: projectIntegration.integrationAuth, + integrationAuthId: projectIntegration.integrationAuth }); const tempHerokuAppNames = tempHerokuApps.map((app) => app.name); setApps(tempHerokuAppNames); @@ -67,9 +67,9 @@ const Integration = ({ projectIntegration }) => { { const result = await startIntegration({ integrationId: projectIntegration._id, environment: envMapping[integrationEnvironment], - appName: integrationApp, + appName: integrationApp }); if (result?.status == 200) { let currentSecrets = await getSecretsForProject({ @@ -124,18 +124,18 @@ const Integration = ({ projectIntegration }) => { setFileState, setIsKeyAvailable, setData, - workspaceId: router.query.id, + workspaceId: router.query.id }); let obj = Object.assign( {}, ...currentSecrets.map((row) => ({ - [row[2]]: row[3], + [row[2]]: row[3] })) ); await pushKeysIntegration({ obj, - integrationId: projectIntegration._id, + integrationId: projectIntegration._id }); router.reload(); } @@ -148,7 +148,7 @@ const Integration = ({ projectIntegration }) => {
)} - {!["Heroku"].includes(integrations[integration].name) && ( + {!['Heroku'].includes(integrations[integration].name) && (
Coming Soon @@ -351,7 +353,8 @@ export default function Integrations() {

Click on a framework to get the setup instructions.

-
+
+
{frameworks.map((framework) => (
-
1 ? "text-sm px-1" : "text-xl px-2"} text-center w-full max-w-xs`}> - {framework?.image && integration logo} - {framework?.name && framework?.image &&
} +
1 + ? 'text-sm px-1' + : 'text-xl px-2' + } text-center w-full max-w-xs`} + > + {framework?.image && ( + integration logo + )} + {framework?.name && framework?.image && ( +
+ )} {framework?.name && framework.name}
diff --git a/frontend/pages/settings/org/[id].js b/frontend/pages/settings/org/[id].js index ca765abe71..80f6efc5d0 100644 --- a/frontend/pages/settings/org/[id].js +++ b/frontend/pages/settings/org/[id].js @@ -1,62 +1,62 @@ -import React, { useEffect, useState } from "react"; -import Head from "next/head"; -import { useRouter } from "next/router"; +import React, { useEffect, useState } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; import { faMagnifyingGlass, faPlus, - faX, -} from "@fortawesome/free-solid-svg-icons"; -import { faCheck } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + faX +} from '@fortawesome/free-solid-svg-icons'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import AddIncidentContactDialog from "~/components/basic/dialog/AddIncidentContactDialog"; -import AddUserDialog from "~/components/basic/dialog/AddUserDialog"; -import InputField from "~/components/basic/InputField"; -import UserTable from "~/components/basic/table/UserTable"; -import NavHeader from "~/components/navigation/NavHeader"; -import guidGenerator from "~/utilities/randomId"; +import Button from '~/components/basic/buttons/Button'; +import AddIncidentContactDialog from '~/components/basic/dialog/AddIncidentContactDialog'; +import AddUserDialog from '~/components/basic/dialog/AddUserDialog'; +import InputField from '~/components/basic/InputField'; +import UserTable from '~/components/basic/table/UserTable'; +import NavHeader from '~/components/navigation/NavHeader'; +import guidGenerator from '~/utilities/randomId'; -import addUserToOrg from "../../api/organization/addUserToOrg"; -import deleteIncidentContact from "../../api/organization/deleteIncidentContact"; -import getIncidentContacts from "../../api/organization/getIncidentContacts"; -import getOrganization from "../../api/organization/GetOrg"; -import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription"; -import getOrganizationUsers from "../../api/organization/GetOrgUsers"; -import renameOrg from "../../api/organization/renameOrg"; -import getUser from "../../api/user/getUser"; -import deleteWorkspace from "../../api/workspace/deleteWorkspace"; -import getWorkspaces from "../../api/workspace/getWorkspaces"; +import addUserToOrg from '../../api/organization/addUserToOrg'; +import deleteIncidentContact from '../../api/organization/deleteIncidentContact'; +import getIncidentContacts from '../../api/organization/getIncidentContacts'; +import getOrganization from '../../api/organization/GetOrg'; +import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription'; +import getOrganizationUsers from '../../api/organization/GetOrgUsers'; +import renameOrg from '../../api/organization/renameOrg'; +import getUser from '../../api/user/getUser'; +import deleteWorkspace from '../../api/workspace/deleteWorkspace'; +import getWorkspaces from '../../api/workspace/getWorkspaces'; export default function SettingsOrg() { const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [orgName, setOrgName] = useState(""); - const [emailUser, setEmailUser] = useState(""); - const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); - const [searchUsers, setSearchUsers] = useState(""); - const [workspaceId, setWorkspaceId] = useState(""); + const [orgName, setOrgName] = useState(''); + const [emailUser, setEmailUser] = useState(''); + const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(''); + const [searchUsers, setSearchUsers] = useState(''); + const [workspaceId, setWorkspaceId] = useState(''); const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false); const [isAddUserOpen, setIsAddUserOpen] = useState( - router.asPath.split("?")[1] == "invite" + router.asPath.split('?')[1] == 'invite' ); const [incidentContacts, setIncidentContacts] = useState([]); - const [searchIncidentContact, setSearchIncidentContact] = useState(""); + const [searchIncidentContact, setSearchIncidentContact] = useState(''); const [userList, setUserList] = useState(); - const [personalEmail, setPersonalEmail] = useState(""); + const [personalEmail, setPersonalEmail] = useState(''); let workspaceIdTemp; - const [email, setEmail] = useState(""); - const [currentPlan, setCurrentPlan] = useState(""); + const [email, setEmail] = useState(''); + const [currentPlan, setCurrentPlan] = useState(''); useEffect(async () => { let org = await getOrganization({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); let orgData = org; setOrgName(orgData.name); let incidentContactsData = await getIncidentContacts( - localStorage.getItem("orgData.id") + localStorage.getItem('orgData.id') ); setIncidentContacts(incidentContactsData?.map((contact) => contact.email)); @@ -66,7 +66,7 @@ export default function SettingsOrg() { workspaceIdTemp = router.query.id; let orgUsers = await getOrganizationUsers({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); setUserList( orgUsers.map((user) => ({ @@ -78,11 +78,11 @@ export default function SettingsOrg() { status: user?.status, userId: user.user?._id, membershipId: user._id, - publicKey: user.user?.publicKey, + publicKey: user.user?.publicKey })) ); const subscriptions = await getOrganizationSubscriptions({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); setCurrentPlan(subscriptions.data[0].plan.product); }, []); @@ -93,7 +93,7 @@ export default function SettingsOrg() { }; const submitChanges = (newOrgName) => { - renameOrg(localStorage.getItem("orgData.id"), newOrgName); + renameOrg(localStorage.getItem('orgData.id'), newOrgName); setButtonReady(false); }; @@ -118,8 +118,8 @@ export default function SettingsOrg() { } async function submitAddUserModal(email) { - await addUserToOrg(email, localStorage.getItem("orgData.id")); - setEmail(""); + await addUserToOrg(email, localStorage.getItem('orgData.id')); + setEmail(''); setIsAddUserOpen(false); router.reload(); } @@ -128,7 +128,7 @@ export default function SettingsOrg() { setIncidentContacts( incidentContacts.filter((contact) => contact != incidentContact) ); - deleteIncidentContact(localStorage.getItem("orgData.id"), incidentContact); + deleteIncidentContact(localStorage.getItem('orgData.id'), incidentContact); }; /** @@ -148,7 +148,7 @@ export default function SettingsOrg() { ) { await deleteWorkspace(router.query.id); let userWorkspaces = await getWorkspaces(); - router.push("/dashboard/" + userWorkspaces[0]._id); + router.push('/dashboard/' + userWorkspaces[0]._id); } } }; @@ -241,7 +241,7 @@ export default function SettingsOrg() { className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none" value={searchUsers} onChange={(e) => setSearchUsers(e.target.value)} - placeholder={"Search members..."} + placeholder={'Search members...'} />
@@ -302,7 +302,7 @@ export default function SettingsOrg() { className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none" value={searchIncidentContact} onChange={(e) => setSearchIncidentContact(e.target.value)} - placeholder={"Search..."} + placeholder={'Search...'} />
{incidentContacts?.filter((email) => diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 9486191f75..8991dc8678 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,72 +1,71 @@ -import React, { useEffect, useRef, useState } from "react"; -import ReactCodeInput from "react-code-input"; -import dynamic from "next/dynamic"; -import Head from "next/head"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect, useState } from 'react'; +import ReactCodeInput from 'react-code-input'; +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import Error from "~/components/basic/Error"; -import InputField from "~/components/basic/InputField"; -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; -import attemptLogin from "~/utilities/attemptLogin"; -import passwordCheck from "~/utilities/checks/PasswordCheck"; +import Button from '~/components/basic/buttons/Button'; +import Error from '~/components/basic/Error'; +import InputField from '~/components/basic/InputField'; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; +import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey'; +import attemptLogin from '~/utilities/attemptLogin'; +import passwordCheck from '~/utilities/checks/PasswordCheck'; -import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode"; -import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup"; -import sendVerificationEmail from "./api/auth/SendVerificationEmail"; -import getWorkspaces from "./api/workspace/getWorkspaces"; +import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; +import completeAccountInformationSignup from './api/auth/CompleteAccountInformationSignup'; +import sendVerificationEmail from './api/auth/SendVerificationEmail'; +import getWorkspaces from './api/workspace/getWorkspaces'; // const ReactCodeInput = dynamic(import("react-code-input")); -const nacl = require("tweetnacl"); -const jsrp = require("jsrp"); -nacl.util = require("tweetnacl-util"); +const nacl = require('tweetnacl'); +const jsrp = require('jsrp'); +nacl.util = require('tweetnacl-util'); const client = new jsrp.client(); // The stye for the verification code input const props = { inputStyle: { - fontFamily: "monospace", - margin: "4px", - MozAppearance: "textfield", - width: "55px", - borderRadius: "5px", - fontSize: "24px", - height: "55px", - paddingLeft: "7", - backgroundColor: "#0d1117", - color: "white", - border: "1px solid gray", - textAlign: "center", - }, + fontFamily: 'monospace', + margin: '4px', + MozAppearance: 'textfield', + width: '55px', + borderRadius: '5px', + fontSize: '24px', + height: '55px', + paddingLeft: '7', + backgroundColor: '#0d1117', + color: 'white', + border: '1px solid gray', + textAlign: 'center' + } } as const; const propsPhone = { inputStyle: { - fontFamily: "monospace", - margin: "4px", - MozAppearance: "textfield", - width: "40px", - borderRadius: "5px", - fontSize: "24px", - height: "40px", - paddingLeft: "7", - backgroundColor: "#0d1117", - color: "white", - border: "1px solid gray", - textAlign: "center", - }, + fontFamily: 'monospace', + margin: '4px', + MozAppearance: 'textfield', + width: '40px', + borderRadius: '5px', + fontSize: '24px', + height: '40px', + paddingLeft: '7', + backgroundColor: '#0d1117', + color: 'white', + border: '1px solid gray', + textAlign: 'center' + } } as const; export default function SignUp() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [code, setCode] = useState(""); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [code, setCode] = useState(''); const [codeError, setCodeError] = useState(false); const [firstNameError, setFirstNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false); @@ -77,22 +76,22 @@ export default function SignUp() { const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false); const [emailError, setEmailError] = useState(false); - const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const [emailErrorMessage, setEmailErrorMessage] = useState(''); const [step, setStep] = useState(1); const router = useRouter(); const [errorLogin, setErrorLogin] = useState(false); const [isLoading, setIsLoading] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false); - const [verificationToken, setVerificationToken] = useState(); + const [verificationToken, setVerificationToken] = useState(''); const [backupKeyIssued, setBackupKeyIssued] = useState(false); useEffect(() => { const tryAuth = async () => { try { const userWorkspaces = await getWorkspaces(); - router.push("/dashboard/" + userWorkspaces[0]._id); + router.push('/dashboard/' + userWorkspaces[0]._id); } catch (error) { - console.log("Error - Not logged in yet"); + console.log('Error - Not logged in yet'); } }; tryAuth(); @@ -109,8 +108,8 @@ export default function SignUp() { setStep(2); } else if (step == 2) { // Checking if the code matches the email. - const response = await checkEmailVerificationCode(email, code); - if (response.status === 200 || code == "111222") { + const response = await checkEmailVerificationCode({ email, code }); + if (response.status === 200 || code == '111222') { setVerificationToken((await response.json()).token); setStep(3); } else { @@ -128,15 +127,15 @@ export default function SignUp() { let emailCheckBool = false; if (!email) { setEmailError(true); - setEmailErrorMessage("Please enter your email."); + setEmailErrorMessage('Please enter your email.'); emailCheckBool = true; } else if ( - !email.includes("@") || - !email.includes(".") || + !email.includes('@') || + !email.includes('.') || !/[a-z]/.test(email) ) { setEmailError(true); - setEmailErrorMessage("Please enter a valid email."); + setEmailErrorMessage('Please enter a valid email.'); emailCheckBool = true; } else { setEmailError(false); @@ -170,7 +169,7 @@ export default function SignUp() { setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, - currentErrorCheck: errorCheck, + currentErrorCheck: errorCheck }); if (!errorCheck) { @@ -181,22 +180,22 @@ export default function SignUp() { const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt( - PRIVATE_KEY, - password + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: PRIVATE_KEY, + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" + '0' ) - ) as { ciphertext: string; iv: string; tag: string }; + }) as { ciphertext: string; iv: string; tag: string }; - localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); + localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); client.init( { username: email, - password: password, + password: password }, async () => { client.createVerifier( @@ -212,17 +211,17 @@ export default function SignUp() { tag, salt: result.salt, verifier: result.verifier, - token: verificationToken, + token: verificationToken }); // if everything works, go the main dashboard page. if (response.status === 200) { // response = await response.json(); - localStorage.setItem("publicKey", PUBLIC_KEY); - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); + localStorage.setItem('publicKey', PUBLIC_KEY); + localStorage.setItem('encryptedPrivateKey', ciphertext); + localStorage.setItem('iv', iv); + localStorage.setItem('tag', tag); try { await attemptLogin( @@ -295,10 +294,10 @@ export default function SignUp() { const step2 = (

- {"We've"} sent a verification email to{" "} + {"We've"} sent a verification email to{' '}

- {email}{" "} + {email}{' '}

14 characters @@ -436,7 +435,7 @@ export default function SignUp() { )}
1 lowercase character @@ -456,7 +455,7 @@ export default function SignUp() { )}
1 number @@ -505,13 +504,13 @@ export default function SignUp() { await issueBackupKey({ email, password, - personalName: firstName + " " + lastName, + personalName: firstName + ' ' + lastName, setBackupKeyError, - setBackupKeyIssued, + setBackupKeyIssued }); const userWorkspaces = await getWorkspaces(); const userWorkspace = userWorkspaces[0]._id; - router.push("/home/" + userWorkspace); + router.push('/home/' + userWorkspace); }} size="lg" /> diff --git a/frontend/pages/signupinvite.js b/frontend/pages/signupinvite.js index c8a8397d79..26b6e1544f 100644 --- a/frontend/pages/signupinvite.js +++ b/frontend/pages/signupinvite.js @@ -1,38 +1,38 @@ -import React, { useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import InputField from "~/components/basic/InputField"; -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; -import attemptLogin from "~/utilities/attemptLogin"; -import passwordCheck from "~/utilities/checks/PasswordCheck"; +import Button from '~/components/basic/buttons/Button'; +import InputField from '~/components/basic/InputField'; +import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; +import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey'; +import attemptLogin from '~/utilities/attemptLogin'; +import passwordCheck from '~/utilities/checks/PasswordCheck'; -import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite"; -import verifySignupInvite from "./api/auth/VerifySignupInvite"; +import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite'; +import verifySignupInvite from './api/auth/VerifySignupInvite'; -const nacl = require("tweetnacl"); -const jsrp = require("jsrp"); -nacl.util = require("tweetnacl-util"); +const nacl = require('tweetnacl'); +const jsrp = require('jsrp'); +nacl.util = require('tweetnacl-util'); const client = new jsrp.client(); -const queryString = require("query-string"); +const queryString = require('query-string'); export default function SignupInvite() { - const [password, setPassword] = useState(""); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); const [firstNameError, setFirstNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false); const [passwordErrorLength, setPasswordErrorLength] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const router = useRouter(); - const parsedUrl = queryString.parse(router.asPath.split("?")[1]); + const parsedUrl = queryString.parse(router.asPath.split('?')[1]); const [email, setEmail] = useState(parsedUrl.to); const token = parsedUrl.token; const [errorLogin, setErrorLogin] = useState(false); @@ -74,21 +74,22 @@ export default function SignupInvite() { const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt( - PRIVATE_KEY, - password + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: PRIVATE_KEY, + secret: password .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" + '0' ) - ); - localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); + }); + + localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); client.init( { username: email, - password: password, + password: password }, async () => { client.createVerifier(async (err, result) => { @@ -102,17 +103,17 @@ export default function SignupInvite() { tag, salt: result.salt, verifier: result.verifier, - token: verificationToken, + token: verificationToken }); // if everything works, go the main dashboard page. - if (!errorCheck && response.status == "200") { + if (!errorCheck && response.status == '200') { response = await response.json(); - localStorage.setItem("publicKey", PUBLIC_KEY); - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); + localStorage.setItem('publicKey', PUBLIC_KEY); + localStorage.setItem('encryptedPrivateKey', ciphertext); + localStorage.setItem('iv', iv); + localStorage.setItem('tag', tag); try { await attemptLogin( @@ -126,7 +127,7 @@ export default function SignupInvite() { setStep(3); } catch (error) { setIsLoading(false); - console.log("Error", error); + console.log('Error', error); } } }); @@ -155,14 +156,14 @@ export default function SignupInvite() { onButtonPressed={async () => { const response = await verifySignupInvite({ email, - code: token, + code: token }); if (response.status == 200) { setVerificationToken((await response.json()).token); setStep(2); } else { - console.log("ERROR", response); - router.push("/requestnewinvite"); + console.log('ERROR', response); + router.push('/requestnewinvite'); } }} size="lg" @@ -211,7 +212,7 @@ export default function SignupInvite() { setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, - currentErrorCheck: false, + currentErrorCheck: false }); }} type="password" @@ -244,7 +245,7 @@ export default function SignupInvite() { )}
14 characters @@ -264,7 +265,7 @@ export default function SignupInvite() { )}
1 lowercase character @@ -284,7 +285,7 @@ export default function SignupInvite() { )}
1 number @@ -335,11 +336,11 @@ export default function SignupInvite() { await issueBackupKey({ email, password, - personalName: firstName + " " + lastName, + personalName: firstName + ' ' + lastName, setBackupKeyError, - setBackupKeyIssued, + setBackupKeyIssued }); - router.push("/dashboard/"); + router.push('/dashboard/'); }} size="lg" /> diff --git a/frontend/pages/users/[id].js b/frontend/pages/users/[id].js index de0307786e..fe96ded3a7 100644 --- a/frontend/pages/users/[id].js +++ b/frontend/pages/users/[id].js @@ -1,39 +1,39 @@ -import React, { useEffect, useState } from "react"; -import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect, useState } from 'react'; +import Head from 'next/head'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { faMagnifyingGlass, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from "~/components/basic/buttons/Button"; -import AddProjectMemberDialog from "~/components/basic/dialog/AddProjectMemberDialog"; -import UserTable from "~/components/basic/table/UserTable"; -import NavHeader from "~/components/navigation/NavHeader"; -import guidGenerator from "~/utilities/randomId"; +import Button from '~/components/basic/buttons/Button'; +import AddProjectMemberDialog from '~/components/basic/dialog/AddProjectMemberDialog'; +import UserTable from '~/components/basic/table/UserTable'; +import NavHeader from '~/components/navigation/NavHeader'; +import guidGenerator from '~/utilities/randomId'; -import getOrganizationUsers from "../api/organization/GetOrgUsers"; -import getUser from "../api/user/getUser"; +import getOrganizationUsers from '../api/organization/GetOrgUsers'; +import getUser from '../api/user/getUser'; // import DeleteUserDialog from '~/components/basic/dialog/DeleteUserDialog'; -import addUserToWorkspace from "../api/workspace/addUserToWorkspace"; -import getWorkspaceUsers from "../api/workspace/getWorkspaceUsers"; -import uploadKeys from "../api/workspace/uploadKeys"; +import addUserToWorkspace from '../api/workspace/addUserToWorkspace'; +import getWorkspaceUsers from '../api/workspace/getWorkspaceUsers'; +import uploadKeys from '../api/workspace/uploadKeys'; // #TODO: Update all the workspaceIds -const crypto = require("crypto"); +const crypto = require('crypto'); const { decryptAssymmetric, - encryptAssymmetric, -} = require("../../components/utilities/cryptography/crypto"); -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); + encryptAssymmetric +} = require('../../components/utilities/cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); export default function Users() { let [isAddOpen, setIsAddOpen] = useState(false); // let [isDeleteOpen, setIsDeleteOpen] = useState(false); // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); - let [email, setEmail] = useState(""); - const [personalEmail, setPersonalEmail] = useState(""); - const [searchUsers, setSearchUsers] = useState(""); + let [email, setEmail] = useState(''); + const [personalEmail, setPersonalEmail] = useState(''); + const [searchUsers, setSearchUsers] = useState(''); const router = useRouter(); let workspaceId; @@ -57,25 +57,25 @@ export default function Users() { async function submitAddModal() { let result = await addUserToWorkspace(email, router.query.id); if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ ciphertext: result.latestKey.encryptedKey, nonce: result.latestKey.nonce, publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY, + privateKey: PRIVATE_KEY }); const { ciphertext, nonce } = encryptAssymmetric({ plaintext: key, publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY, + privateKey: PRIVATE_KEY }); uploadKeys(router.query.id, result.invitee._id, ciphertext, nonce); } - setEmail(""); + setEmail(''); setIsAddOpen(false); router.reload(); } @@ -93,7 +93,7 @@ export default function Users() { workspaceId = router.query.id; let workspaceUsers = await getWorkspaceUsers({ - workspaceId, + workspaceId }); const tempUserList = workspaceUsers.map((user) => ({ key: guidGenerator(), @@ -104,16 +104,16 @@ export default function Users() { status: user?.status, userId: user.user?._id, membershipId: user._id, - publicKey: user.user?.publicKey, + publicKey: user.user?.publicKey })); setUserList(tempUserList); const orgUsers = await getOrganizationUsers({ - orgId: localStorage.getItem("orgData.id"), + orgId: localStorage.getItem('orgData.id') }); setOrgUserList(orgUsers); setEmail( orgUsers - ?.filter((user) => user.status == "accepted") + ?.filter((user) => user.status == 'accepted') .map((user) => user.user.email) .filter( (email) => !tempUserList?.map((user1) => user1.email).includes(email) @@ -140,7 +140,7 @@ export default function Users() { submitModal={submitAddModal} email={email} data={orgUserList - ?.filter((user) => user.status == "accepted") + ?.filter((user) => user.status == 'accepted') .map((user) => user.user.email) .filter( (email) => !userList?.map((user1) => user1.email).includes(email) @@ -159,7 +159,7 @@ export default function Users() { className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none" value={searchUsers} onChange={(e) => setSearchUsers(e.target.value)} - placeholder={"Search members..."} + placeholder={'Search members...'} />
diff --git a/frontend/public/images/progress-71.svg b/frontend/public/images/progress-71.svg new file mode 100644 index 0000000000..1c1643e709 --- /dev/null +++ b/frontend/public/images/progress-71.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ce99bdc6df..e936a69a2c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,25 +2,13 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "~/components/*": [ - "components/*" - ], - "~/utilities/*": [ - "components/utilities/*" - ], - "~/*": [ - "const" - ], - "~/pages/*": [ - "pages/*" - ] + "~/components/*": ["components/*"], + "~/utilities/*": ["components/utilities/*"], + "~/*": ["const"], + "~/pages/*": ["pages/*"] }, - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -35,12 +23,6 @@ "jsx": "preserve", "incremental": true }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] } diff --git a/helm-charts/infisical/Chart.yaml b/helm-charts/infisical/Chart.yaml index ecef711f13..48eb5d4dc0 100644 --- a/helm-charts/infisical/Chart.yaml +++ b/helm-charts/infisical/Chart.yaml @@ -7,7 +7,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.1.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm-charts/infisical/templates/backend-deployment.yaml b/helm-charts/infisical/templates/backend-deployment.yaml index 9bc72eaf53..c2620ff8ac 100644 --- a/helm-charts/infisical/templates/backend-deployment.yaml +++ b/helm-charts/infisical/templates/backend-deployment.yaml @@ -21,7 +21,7 @@ spec: ports: - containerPort: 4000 env: - {{- range $key, $value := .Values.secrets }} + {{- range $key, $value := .Values.backendEnvironmentVariables }} {{- if eq $value "MUST_REPLACE" }} {{ fail "Environment variables are not set. Please set all environment variables to continue." }} {{ end }} diff --git a/helm-charts/infisical/templates/frontend-deployment.yaml b/helm-charts/infisical/templates/frontend-deployment.yaml index e0dbdae0db..79bf8837b7 100644 --- a/helm-charts/infisical/templates/frontend-deployment.yaml +++ b/helm-charts/infisical/templates/frontend-deployment.yaml @@ -18,6 +18,11 @@ spec: - name: frontend image: infisical/frontend imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + env: + {{- range $key, $value := .Values.frontendEnvironmentVariables }} + - name: {{ $key }} + value: {{ quote $value }} + {{- end }} ports: - containerPort: 4000 --- diff --git a/helm-charts/infisical/values.yaml b/helm-charts/infisical/values.yaml index 94bce75ef9..a93a4769d0 100644 --- a/helm-charts/infisical/values.yaml +++ b/helm-charts/infisical/values.yaml @@ -34,10 +34,10 @@ ingress: ## Complete Ingress example # ingress: # enabled: true -# annotations: +# annotations: # kubernetes.io/ingress.class: "nginx" # cert-manager.io/issuer: letsencrypt-nginx -# hostName: example.com +# hostName: k8.infisical.com # frontend: # path: / # pathType: Prefix @@ -45,14 +45,14 @@ ingress: # path: /api # pathType: Prefix # tls: -# hosts: -# - k8.infisical.com -# secretName: letsencrypt-nginx +# - secretName: letsencrypt-nginx +# hosts: +# - k8.infisical.com ### ### YOU MUST FILL IN ALL SECRETS BELOW ### -secrets: +backendEnvironmentVariables: # Required keys for platform encryption/decryption ops. Replace with nacl sk keys PRIVATE_KEY: MUST_REPLACE PUBLIC_KEY: MUST_REPLACE @@ -73,4 +73,7 @@ secrets: # You may replace with Mongo Cloud URI MONGO_URL: mongodb://root:root@mongodb-service:27017/ + +# frontendEnvironmentVariables: +# INFISICAL_TELEMETRY_ENABLED: true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..3a41579024 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1963 @@ +{ + "name": "infisical", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "infisical", + "license": "ISC", + "devDependencies": { + "eslint": "^8.29.0", + "husky": "^8.0.2", + "prettier": "^2.8.1" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "husky": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "dev": true + }, + "ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "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==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "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 + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", + "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..b0d02f3a04 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "infisical", + "repository": { + "type": "git", + "url": "git+https://github.com/infisical/infisical.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/infisical/infisical/issues" + }, + "homepage": "https://github.com/infisical/infisical#readme", + "scripts": { + "prepare": "husky install" + }, + "lint-staged": { + "**/*": "prettier --write --ignore-unknown", + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ] + }, + "devDependencies": { + "eslint": "^8.29.0", + "husky": "^8.0.2", + "prettier": "^2.8.1" + } +}