Compare commits
90 Commits
pr/JoelJac
...
feat/pat-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a6e8e1c6 | ||
|
|
4bd23a8f4c | ||
|
|
f4f3fdf2d5 | ||
|
|
f8ac6dfeb1 | ||
|
|
7d2d335b37 | ||
|
|
76875db865 | ||
|
|
96e2d87b57 | ||
|
|
be353d9f72 | ||
|
|
38bc2c12c3 | ||
|
|
97644fa508 | ||
|
|
eb3446ae23 | ||
|
|
6c29961d09 | ||
|
|
ef1117d8cc | ||
|
|
5c4b651aee | ||
|
|
391e5a20f5 | ||
|
|
4b8f3bd8da | ||
|
|
94248076e6 | ||
|
|
eecc3db4e9 | ||
|
|
426e7594f4 | ||
|
|
934dc473f0 | ||
|
|
be57255bf7 | ||
|
|
f89561da54 | ||
|
|
c2c4e620c2 | ||
|
|
844eee0fa4 | ||
|
|
d21bb65511 | ||
|
|
4f614f7257 | ||
|
|
0e2887b4e9 | ||
|
|
18652ce400 | ||
|
|
08c655235d | ||
|
|
51412549e8 | ||
|
|
22c6eabd13 | ||
|
|
a079e0f04b | ||
|
|
375d53263a | ||
|
|
57ef3e085f | ||
|
|
9fb6e59e36 | ||
|
|
1b0802b0e6 | ||
|
|
fb45fe4627 | ||
|
|
0f592d1789 | ||
|
|
787aab650f | ||
|
|
1f7a8edb14 | ||
|
|
81f1e05a6c | ||
|
|
0a71783eaa | ||
|
|
c326f54f7e | ||
|
|
1113c79e20 | ||
|
|
6fd30f9aca | ||
|
|
2c5b0dcd1b | ||
|
|
6f4455ba03 | ||
|
|
ba8c4480d9 | ||
|
|
380397cc55 | ||
|
|
d19807b212 | ||
|
|
c89c2a5f5c | ||
|
|
256553b9bb | ||
|
|
89d9951f3b | ||
|
|
dd65ad3103 | ||
|
|
018ed3db26 | ||
|
|
a9cd6c0c01 | ||
|
|
e53382666a | ||
|
|
7621ff2961 | ||
|
|
fc20b76080 | ||
|
|
146c73d7b6 | ||
|
|
6b58915caa | ||
|
|
457857a711 | ||
|
|
a3f3e3e62d | ||
|
|
66f20d10e1 | ||
|
|
32e9366609 | ||
|
|
e41e956273 | ||
|
|
a14870f3f0 | ||
|
|
0e96665254 | ||
|
|
efdc1c2f5d | ||
|
|
c5334d4c06 | ||
|
|
4f549974ed | ||
|
|
41d617b507 | ||
|
|
be7387ed19 | ||
|
|
acfb0189df | ||
|
|
8fdba760a2 | ||
|
|
bf98009abb | ||
|
|
dce396c164 | ||
|
|
07e8af7947 | ||
|
|
e69d5a6253 | ||
|
|
6d66d12a9e | ||
|
|
439cd82c88 | ||
|
|
6dbaf524ce | ||
|
|
68e439d1a4 | ||
|
|
8deba7a28e | ||
|
|
7ec8659381 | ||
|
|
3611cac241 | ||
|
|
919579b1da | ||
|
|
4798d7bbbd | ||
|
|
a0c6b22641 | ||
|
|
de8929ab18 |
15
.github/workflows/tests.yml
vendored
15
.github/workflows/tests.yml
vendored
@@ -17,22 +17,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: mv .env.example .env
|
run: mv .env.example .env
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: true
|
run_install: true
|
||||||
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node }}
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|||||||
48
docker-compose.deploy.yml
Normal file
48
docker-compose.deploy.yml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# THIS IS NOT TO BE USED FOR PERSONAL DEPLOYMENTS!
|
||||||
|
# Internal Docker Compose Image used for internal testing deployments
|
||||||
|
|
||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
hoppscotch-db:
|
||||||
|
image: postgres:15
|
||||||
|
user: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: testpass
|
||||||
|
POSTGRES_DB: hoppscotch
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
|
||||||
|
]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
hoppscotch-aio:
|
||||||
|
container_name: hoppscotch-aio
|
||||||
|
build:
|
||||||
|
dockerfile: prod.Dockerfile
|
||||||
|
context: .
|
||||||
|
target: aio
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
|
||||||
|
- ENABLE_SUBPATH_BASED_ACCESS=true
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
|
command: ["sh", "-c", "pnpm exec prisma migrate deploy && node /usr/src/app/aio_run.mjs"]
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://localhost:80'
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ services:
|
|||||||
- PORT=3000
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
- ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ curlCheck() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
curlCheck "http://localhost:3000"
|
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
|
||||||
curlCheck "http://localhost:3100"
|
curlCheck "http://localhost:80/backend/ping"
|
||||||
curlCheck "http://localhost:3170/ping"
|
else
|
||||||
|
curlCheck "http://localhost:3000"
|
||||||
|
curlCheck "http://localhost:3100"
|
||||||
|
curlCheck "http://localhost:3170/ping"
|
||||||
|
fi
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -23,13 +23,13 @@
|
|||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^16.2.3",
|
"@commitlint/cli": "16.3.0",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "16.2.4",
|
||||||
"@hoppscotch/ui": "^0.1.0",
|
"@hoppscotch/ui": "0.1.0",
|
||||||
"@types/node": "17.0.27",
|
"@types/node": "17.0.27",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "^7.0.4",
|
"husky": "7.0.4",
|
||||||
"lint-staged": "12.4.0"
|
"lint-staged": "12.4.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"vue": "3.3.9"
|
"vue": "3.3.9"
|
||||||
},
|
},
|
||||||
"packageExtensions": {
|
"packageExtensions": {
|
||||||
"httpsnippet@^3.0.1": {
|
"httpsnippet@3.0.1": {
|
||||||
"peerDependencies": {
|
"dependencies": {
|
||||||
"ajv": "6.12.3"
|
"ajv": "6.12.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,16 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "6.9.3",
|
"@codemirror/language": "6.10.1",
|
||||||
"@lezer/highlight": "1.2.0",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@lezer/lr": "^1.3.14"
|
"@lezer/lr": "1.3.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.5.1",
|
"@lezer/generator": "1.5.1",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "9.2.2",
|
||||||
"rollup": "^3.29.3",
|
"rollup": "3.29.4",
|
||||||
"rollup-plugin-dts": "^6.0.2",
|
"rollup-plugin-dts": "6.0.2",
|
||||||
"rollup-plugin-ts": "^3.4.5",
|
"rollup-plugin-ts": "3.4.5",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18.8.0 AS builder
|
FROM node:20.12.2 AS builder
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"assets": [
|
"assets": [{ "include": "mailer/templates/**/*", "outDir": "dist" }],
|
||||||
"**/*.hbs"
|
|
||||||
],
|
|
||||||
"watchAssets": true
|
"watchAssets": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.12.6",
|
"version": "2024.3.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -24,83 +24,84 @@
|
|||||||
"do-test": "pnpm run test"
|
"do-test": "pnpm run test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.9.4",
|
"@apollo/server": "4.9.5",
|
||||||
"@nestjs-modules/mailer": "^1.9.1",
|
"@nestjs-modules/mailer": "1.9.1",
|
||||||
"@nestjs/apollo": "^12.0.9",
|
"@nestjs/apollo": "12.0.9",
|
||||||
"@nestjs/common": "^10.2.6",
|
"@nestjs/common": "10.2.7",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "3.1.1",
|
||||||
"@nestjs/core": "^10.2.6",
|
"@nestjs/core": "10.2.7",
|
||||||
"@nestjs/graphql": "^12.0.9",
|
"@nestjs/graphql": "12.0.9",
|
||||||
"@nestjs/jwt": "^10.1.1",
|
"@nestjs/jwt": "10.1.1",
|
||||||
"@nestjs/passport": "^10.0.2",
|
"@nestjs/passport": "10.0.2",
|
||||||
"@nestjs/platform-express": "^10.2.6",
|
"@nestjs/platform-express": "10.2.7",
|
||||||
"@nestjs/schedule": "^4.0.1",
|
"@nestjs/schedule": "4.0.1",
|
||||||
"@nestjs/throttler": "^5.0.0",
|
"@nestjs/terminus": "10.2.3",
|
||||||
"@prisma/client": "^5.8.0",
|
"@nestjs/throttler": "5.0.1",
|
||||||
"argon2": "^0.30.3",
|
"@prisma/client": "5.8.1",
|
||||||
"bcrypt": "^5.1.0",
|
"argon2": "0.30.3",
|
||||||
"cookie": "^0.5.0",
|
"bcrypt": "5.1.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie": "0.5.0",
|
||||||
"cron": "^3.1.6",
|
"cookie-parser": "1.4.6",
|
||||||
"express": "^4.17.1",
|
"cron": "3.1.6",
|
||||||
"express-session": "^1.17.3",
|
"express": "4.18.2",
|
||||||
"fp-ts": "^2.13.1",
|
"express-session": "1.17.3",
|
||||||
"graphql": "^16.8.1",
|
"fp-ts": "2.13.1",
|
||||||
"graphql-query-complexity": "^0.12.0",
|
"graphql": "16.8.1",
|
||||||
"graphql-redis-subscriptions": "^2.6.0",
|
"graphql-query-complexity": "0.12.0",
|
||||||
"graphql-subscriptions": "^2.0.0",
|
"graphql-redis-subscriptions": "2.6.0",
|
||||||
"handlebars": "^4.7.7",
|
"graphql-subscriptions": "2.0.0",
|
||||||
"io-ts": "^2.2.16",
|
"handlebars": "4.7.7",
|
||||||
"luxon": "^3.2.1",
|
"io-ts": "2.2.16",
|
||||||
"nodemailer": "^6.9.1",
|
"luxon": "3.2.1",
|
||||||
"passport": "^0.6.0",
|
"nodemailer": "6.9.1",
|
||||||
"passport-github2": "^0.1.12",
|
"passport": "0.6.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-github2": "0.1.12",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-jwt": "4.0.1",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-local": "1.0.0",
|
||||||
"posthog-node": "^3.6.3",
|
"passport-microsoft": "1.0.0",
|
||||||
"prisma": "^5.8.0",
|
"posthog-node": "3.6.3",
|
||||||
"reflect-metadata": "^0.1.13",
|
"prisma": "5.8.1",
|
||||||
"rimraf": "^3.0.2",
|
"reflect-metadata": "0.1.13",
|
||||||
"rxjs": "^7.6.0"
|
"rimraf": "3.0.2",
|
||||||
|
"rxjs": "7.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.1.18",
|
"@nestjs/cli": "10.2.1",
|
||||||
"@nestjs/schematics": "^10.0.2",
|
"@nestjs/schematics": "10.0.3",
|
||||||
"@nestjs/testing": "^10.2.6",
|
"@nestjs/testing": "10.2.7",
|
||||||
"@relmify/jest-fp-ts": "^2.0.2",
|
"@relmify/jest-fp-ts": "2.0.2",
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/argon2": "0.15.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "5.0.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "0.5.1",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "1.4.3",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "4.17.14",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "29.4.0",
|
||||||
"@types/luxon": "^3.2.0",
|
"@types/luxon": "3.2.0",
|
||||||
"@types/node": "^18.11.10",
|
"@types/node": "18.11.10",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/passport-github2": "^1.2.5",
|
"@types/passport-github2": "1.2.5",
|
||||||
"@types/passport-google-oauth20": "^2.0.11",
|
"@types/passport-google-oauth20": "2.0.11",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "3.0.8",
|
||||||
"@types/passport-microsoft": "^0.0.0",
|
"@types/passport-microsoft": "0.0.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "^8.29.0",
|
"eslint": "8.29.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"jest": "^29.4.1",
|
"jest": "29.4.1",
|
||||||
"jest-mock-extended": "^3.0.1",
|
"jest-mock-extended": "3.0.1",
|
||||||
"jwt": "link:@types/nestjs/jwt",
|
"jwt": "link:@types/nestjs/jwt",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "2.8.4",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"supertest": "^6.3.2",
|
"supertest": "6.3.2",
|
||||||
"ts-jest": "29.0.5",
|
"ts-jest": "29.0.5",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "9.4.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsconfig-paths": "4.1.1",
|
"tsconfig-paths": "4.1.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "4.9.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- This is a custom migration file which is not generated by Prisma.
|
||||||
|
-- The aim of this migration is to add text search indices to the TeamCollection and TeamRequest tables.
|
||||||
|
|
||||||
|
-- Create Extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
-- Create GIN Trigram Index for Team Collection title
|
||||||
|
CREATE INDEX
|
||||||
|
"TeamCollection_title_trgm_idx"
|
||||||
|
ON
|
||||||
|
"TeamCollection"
|
||||||
|
USING
|
||||||
|
GIN (title gin_trgm_ops);
|
||||||
|
|
||||||
|
-- Create GIN Trigram Index for Team Collection title
|
||||||
|
CREATE INDEX
|
||||||
|
"TeamRequest_title_trgm_idx"
|
||||||
|
ON
|
||||||
|
"TeamRequest"
|
||||||
|
USING
|
||||||
|
GIN (title gin_trgm_ops);
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "lastLoggedOn" TIMESTAMP(3);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PersonalAccessToken" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userUid" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expiresOn" TIMESTAMP(3),
|
||||||
|
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedOn" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "PersonalAccessToken_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PersonalAccessToken_token_key" ON "PersonalAccessToken"("token");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -89,24 +89,26 @@ model TeamEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
uid String @id @default(cuid())
|
uid String @id @default(cuid())
|
||||||
displayName String?
|
displayName String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
photoURL String?
|
photoURL String?
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
refreshToken String?
|
refreshToken String?
|
||||||
providerAccounts Account[]
|
providerAccounts Account[]
|
||||||
VerificationToken VerificationToken[]
|
VerificationToken VerificationToken[]
|
||||||
settings UserSettings?
|
settings UserSettings?
|
||||||
UserHistory UserHistory[]
|
UserHistory UserHistory[]
|
||||||
UserEnvironments UserEnvironment[]
|
UserEnvironments UserEnvironment[]
|
||||||
userCollections UserCollection[]
|
userCollections UserCollection[]
|
||||||
userRequests UserRequest[]
|
userRequests UserRequest[]
|
||||||
currentRESTSession Json?
|
currentRESTSession Json?
|
||||||
currentGQLSession Json?
|
currentGQLSession Json?
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
lastLoggedOn DateTime?
|
||||||
invitedUsers InvitedUsers[]
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
shortcodes Shortcode[]
|
invitedUsers InvitedUsers[]
|
||||||
|
shortcodes Shortcode[]
|
||||||
|
personalAccessTokens PersonalAccessToken[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
@@ -218,3 +220,14 @@ model InfraConfig {
|
|||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PersonalAccessToken {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userUid String
|
||||||
|
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
||||||
|
label String
|
||||||
|
token String @unique @default(uuid())
|
||||||
|
expiresOn DateTime? @db.Timestamp(3)
|
||||||
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { CreateAccessTokenDto } from './dto/create-access-token.dto';
|
||||||
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
|
import { PATAuthGuard } from 'src/guards/rest-pat-auth.guard';
|
||||||
|
import { AccessTokenInterceptor } from 'src/interceptors/access-token.interceptor';
|
||||||
|
import { TeamEnvironmentsService } from 'src/team-environments/team-environments.service';
|
||||||
|
import { TeamCollectionService } from 'src/team-collection/team-collection.service';
|
||||||
|
import { ACCESS_TOKENS_INVALID_DATA_ID } from 'src/errors';
|
||||||
|
import { createCLIErrorResponse } from './helper';
|
||||||
|
|
||||||
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
|
@Controller({ path: 'access-tokens', version: '1' })
|
||||||
|
export class AccessTokenController {
|
||||||
|
constructor(
|
||||||
|
private readonly accessTokenService: AccessTokenService,
|
||||||
|
private readonly teamCollectionService: TeamCollectionService,
|
||||||
|
private readonly teamEnvironmentsService: TeamEnvironmentsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('create')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async createPAT(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
|
@Body() createAccessTokenDto: CreateAccessTokenDto,
|
||||||
|
) {
|
||||||
|
const result = await this.accessTokenService.createPAT(
|
||||||
|
createAccessTokenDto,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
if (E.isLeft(result)) throwHTTPErr(result.left);
|
||||||
|
return result.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('revoke')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async deletePAT(@Query('id') id: string) {
|
||||||
|
const result = await this.accessTokenService.deletePAT(id);
|
||||||
|
|
||||||
|
if (E.isLeft(result)) throwHTTPErr(result.left);
|
||||||
|
return result.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('list')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async listAllUserPAT(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
|
@Query('offset', ParseIntPipe) offset: number,
|
||||||
|
@Query('limit', ParseIntPipe) limit: number,
|
||||||
|
) {
|
||||||
|
return await this.accessTokenService.listAllUserPAT(
|
||||||
|
user.uid,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('collection/:id')
|
||||||
|
@UseGuards(PATAuthGuard)
|
||||||
|
@UseInterceptors(AccessTokenInterceptor)
|
||||||
|
async fetchCollection(@GqlUser() user: AuthUser, @Param('id') id: string) {
|
||||||
|
const res = await this.teamCollectionService.getCollectionForCLI(
|
||||||
|
id,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(res))
|
||||||
|
throw new BadRequestException(
|
||||||
|
createCLIErrorResponse(ACCESS_TOKENS_INVALID_DATA_ID),
|
||||||
|
);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('environment/:id')
|
||||||
|
@UseGuards(PATAuthGuard)
|
||||||
|
@UseInterceptors(AccessTokenInterceptor)
|
||||||
|
async fetchEnvironment(@GqlUser() user: AuthUser, @Param('id') id: string) {
|
||||||
|
const res = await this.teamEnvironmentsService.getTeamEnvironmentForCLI(
|
||||||
|
id,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(res))
|
||||||
|
throw new BadRequestException(
|
||||||
|
createCLIErrorResponse(ACCESS_TOKENS_INVALID_DATA_ID),
|
||||||
|
);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AccessTokenController } from './access-token.controller';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { TeamCollectionModule } from 'src/team-collection/team-collection.module';
|
||||||
|
import { TeamEnvironmentsModule } from 'src/team-environments/team-environments.module';
|
||||||
|
import { TeamModule } from 'src/team/team.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
TeamCollectionModule,
|
||||||
|
TeamEnvironmentsModule,
|
||||||
|
TeamModule,
|
||||||
|
],
|
||||||
|
controllers: [AccessTokenController],
|
||||||
|
providers: [AccessTokenService],
|
||||||
|
exports: [AccessTokenService],
|
||||||
|
})
|
||||||
|
export class AccessTokenModule {}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { AccessTokenService } from './access-token.service';
|
||||||
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import {
|
||||||
|
ACCESS_TOKEN_EXPIRY_INVALID,
|
||||||
|
ACCESS_TOKEN_LABEL_SHORT,
|
||||||
|
ACCESS_TOKEN_NOT_FOUND,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { PersonalAccessToken } from '@prisma/client';
|
||||||
|
import { AccessToken } from 'src/types/AccessToken';
|
||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const accessTokenService = new AccessTokenService(mockPrisma);
|
||||||
|
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
const user: AuthUser = {
|
||||||
|
uid: '123344',
|
||||||
|
email: 'dwight@dundermifflin.com',
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
|
isAdmin: false,
|
||||||
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
createdOn: currentTime,
|
||||||
|
currentGQLSession: {},
|
||||||
|
currentRESTSession: {},
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATCreatedOn = new Date();
|
||||||
|
const expiryInDays = 7;
|
||||||
|
const PATExpiresOn = new Date(
|
||||||
|
PATCreatedOn.getTime() + expiryInDays * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userAccessToken: PersonalAccessToken = {
|
||||||
|
id: 'skfvhj8uvdfivb',
|
||||||
|
userUid: user.uid,
|
||||||
|
label: 'test',
|
||||||
|
token: '0140e328-b187-4823-ae4b-ed4bec832ac2',
|
||||||
|
expiresOn: PATExpiresOn,
|
||||||
|
createdOn: PATCreatedOn,
|
||||||
|
updatedOn: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const userAccessTokenCasted: AccessToken = {
|
||||||
|
id: userAccessToken.id,
|
||||||
|
label: userAccessToken.label,
|
||||||
|
createdOn: userAccessToken.createdOn,
|
||||||
|
lastUsedOn: userAccessToken.updatedOn,
|
||||||
|
expiresOn: userAccessToken.expiresOn,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReset(mockPrisma);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AccessTokenService', () => {
|
||||||
|
describe('createPAT', () => {
|
||||||
|
test('should throw ACCESS_TOKEN_LABEL_SHORT if label is too short', async () => {
|
||||||
|
const result = await accessTokenService.createPAT(
|
||||||
|
{
|
||||||
|
label: 'a',
|
||||||
|
expiryInDays: 7,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: ACCESS_TOKEN_LABEL_SHORT,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw ACCESS_TOKEN_EXPIRY_INVALID if expiry date is invalid', async () => {
|
||||||
|
const result = await accessTokenService.createPAT(
|
||||||
|
{
|
||||||
|
label: 'test',
|
||||||
|
expiryInDays: 9,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: ACCESS_TOKEN_EXPIRY_INVALID,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully create a new Access Token', async () => {
|
||||||
|
mockPrisma.personalAccessToken.create.mockResolvedValueOnce(
|
||||||
|
userAccessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.createPAT(
|
||||||
|
{
|
||||||
|
label: userAccessToken.label,
|
||||||
|
expiryInDays,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight({
|
||||||
|
token: `pat-${userAccessToken.token}`,
|
||||||
|
info: userAccessTokenCasted,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deletePAT', () => {
|
||||||
|
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
|
||||||
|
mockPrisma.personalAccessToken.delete.mockRejectedValueOnce(
|
||||||
|
'RecordNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.deletePAT(userAccessToken.id);
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: ACCESS_TOKEN_NOT_FOUND,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully delete a new Access Token', async () => {
|
||||||
|
mockPrisma.personalAccessToken.delete.mockResolvedValueOnce(
|
||||||
|
userAccessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.deletePAT(userAccessToken.id);
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listAllUserPAT', () => {
|
||||||
|
test('should successfully return a list of user Access Tokens', async () => {
|
||||||
|
mockPrisma.personalAccessToken.findMany.mockResolvedValueOnce([
|
||||||
|
userAccessToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await accessTokenService.listAllUserPAT(user.uid, 0, 10);
|
||||||
|
expect(result).toEqual([userAccessTokenCasted]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserPAT', () => {
|
||||||
|
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
|
||||||
|
mockPrisma.personalAccessToken.findUniqueOrThrow.mockRejectedValueOnce(
|
||||||
|
'NotFoundError',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.getUserPAT(userAccessToken.token);
|
||||||
|
expect(result).toEqualLeft(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully return a user Access Tokens', async () => {
|
||||||
|
mockPrisma.personalAccessToken.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
|
...userAccessToken,
|
||||||
|
user,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await accessTokenService.getUserPAT(
|
||||||
|
`pat-${userAccessToken.token}`,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight({
|
||||||
|
user,
|
||||||
|
...userAccessToken,
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateLastUsedforPAT', () => {
|
||||||
|
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
|
||||||
|
mockPrisma.personalAccessToken.update.mockRejectedValueOnce(
|
||||||
|
'RecordNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.updateLastUsedForPAT(
|
||||||
|
userAccessToken.token,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully update lastUsedOn for a user Access Tokens', async () => {
|
||||||
|
mockPrisma.personalAccessToken.update.mockResolvedValueOnce(
|
||||||
|
userAccessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accessTokenService.updateLastUsedForPAT(
|
||||||
|
`pat-${userAccessToken.token}`,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(userAccessTokenCasted);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { CreateAccessTokenDto } from './dto/create-access-token.dto';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { isValidLength } from 'src/utils';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
ACCESS_TOKEN_EXPIRY_INVALID,
|
||||||
|
ACCESS_TOKEN_LABEL_SHORT,
|
||||||
|
ACCESS_TOKEN_NOT_FOUND,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { CreateAccessTokenResponse } from './helper';
|
||||||
|
import { PersonalAccessToken } from '@prisma/client';
|
||||||
|
import { AccessToken } from 'src/types/AccessToken';
|
||||||
|
@Injectable()
|
||||||
|
export class AccessTokenService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
TITLE_LENGTH = 3;
|
||||||
|
VALID_TOKEN_DURATIONS = [7, 30, 60, 90];
|
||||||
|
TOKEN_PREFIX = 'pat-';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the expiration date of the token
|
||||||
|
*
|
||||||
|
* @param expiresOn Number of days the token is valid for
|
||||||
|
* @returns Date object of the expiration date
|
||||||
|
*/
|
||||||
|
private calculateExpirationDate(expiresOn: null | number) {
|
||||||
|
if (expiresOn === null) return null;
|
||||||
|
return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the expiration date of the token
|
||||||
|
*
|
||||||
|
* @param expiresOn Number of days the token is valid for
|
||||||
|
* @returns Boolean indicating if the expiration date is valid
|
||||||
|
*/
|
||||||
|
private validateExpirationDate(expiresOn: null | number) {
|
||||||
|
if (expiresOn === null || this.VALID_TOKEN_DURATIONS.includes(expiresOn))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typecast a database PersonalAccessToken to a AccessToken model
|
||||||
|
* @param token database PersonalAccessToken
|
||||||
|
* @returns AccessToken model
|
||||||
|
*/
|
||||||
|
private cast(token: PersonalAccessToken): AccessToken {
|
||||||
|
return <AccessToken>{
|
||||||
|
id: token.id,
|
||||||
|
label: token.label,
|
||||||
|
createdOn: token.createdOn,
|
||||||
|
expiresOn: token.expiresOn,
|
||||||
|
lastUsedOn: token.updatedOn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract UUID from the token
|
||||||
|
*
|
||||||
|
* @param token Personal Access Token
|
||||||
|
* @returns UUID of the token
|
||||||
|
*/
|
||||||
|
private extractUUID(token): string | null {
|
||||||
|
if (!token.startsWith(this.TOKEN_PREFIX)) return null;
|
||||||
|
return token.slice(this.TOKEN_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Personal Access Token
|
||||||
|
*
|
||||||
|
* @param createAccessTokenDto DTO for creating a Personal Access Token
|
||||||
|
* @param user AuthUser object
|
||||||
|
* @returns Either of the created token or error message
|
||||||
|
*/
|
||||||
|
async createPAT(createAccessTokenDto: CreateAccessTokenDto, user: AuthUser) {
|
||||||
|
const isTitleValid = isValidLength(
|
||||||
|
createAccessTokenDto.label,
|
||||||
|
this.TITLE_LENGTH,
|
||||||
|
);
|
||||||
|
if (!isTitleValid)
|
||||||
|
return E.left({
|
||||||
|
message: ACCESS_TOKEN_LABEL_SHORT,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.validateExpirationDate(createAccessTokenDto.expiryInDays))
|
||||||
|
return E.left({
|
||||||
|
message: ACCESS_TOKEN_EXPIRY_INVALID,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdPAT = await this.prisma.personalAccessToken.create({
|
||||||
|
data: {
|
||||||
|
userUid: user.uid,
|
||||||
|
label: createAccessTokenDto.label,
|
||||||
|
expiresOn: this.calculateExpirationDate(
|
||||||
|
createAccessTokenDto.expiryInDays,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res: CreateAccessTokenResponse = {
|
||||||
|
token: `${this.TOKEN_PREFIX}${createdPAT.token}`,
|
||||||
|
info: this.cast(createdPAT),
|
||||||
|
};
|
||||||
|
|
||||||
|
return E.right(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Personal Access Token
|
||||||
|
*
|
||||||
|
* @param accessTokenID ID of the Personal Access Token
|
||||||
|
* @returns Either of true or error message
|
||||||
|
*/
|
||||||
|
async deletePAT(accessTokenID: string) {
|
||||||
|
try {
|
||||||
|
await this.prisma.personalAccessToken.delete({
|
||||||
|
where: { id: accessTokenID },
|
||||||
|
});
|
||||||
|
return E.right(true);
|
||||||
|
} catch {
|
||||||
|
return E.left({
|
||||||
|
message: ACCESS_TOKEN_NOT_FOUND,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all Personal Access Tokens of a user
|
||||||
|
*
|
||||||
|
* @param userUid UID of the user
|
||||||
|
* @param offset Offset for pagination
|
||||||
|
* @param limit Limit for pagination
|
||||||
|
* @returns Either of the list of Personal Access Tokens or error message
|
||||||
|
*/
|
||||||
|
async listAllUserPAT(userUid: string, offset: number, limit: number) {
|
||||||
|
const userPATs = await this.prisma.personalAccessToken.findMany({
|
||||||
|
where: {
|
||||||
|
userUid: userUid,
|
||||||
|
},
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
createdOn: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAccessTokenList = userPATs.map((pat) => this.cast(pat));
|
||||||
|
|
||||||
|
return userAccessTokenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Personal Access Token
|
||||||
|
*
|
||||||
|
* @param accessToken Personal Access Token
|
||||||
|
* @returns Either of the Personal Access Token or error message
|
||||||
|
*/
|
||||||
|
async getUserPAT(accessToken: string) {
|
||||||
|
const extractedToken = this.extractUUID(accessToken);
|
||||||
|
if (!extractedToken) return E.left(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userPAT = await this.prisma.personalAccessToken.findUniqueOrThrow({
|
||||||
|
where: { token: extractedToken },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
return E.right(userPAT);
|
||||||
|
} catch {
|
||||||
|
return E.left(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last used date of a Personal Access Token
|
||||||
|
*
|
||||||
|
* @param token Personal Access Token
|
||||||
|
* @returns Either of the updated Personal Access Token or error message
|
||||||
|
*/
|
||||||
|
async updateLastUsedForPAT(token: string) {
|
||||||
|
const extractedToken = this.extractUUID(token);
|
||||||
|
if (!extractedToken) return E.left(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedAccessToken = await this.prisma.personalAccessToken.update({
|
||||||
|
where: { token: extractedToken },
|
||||||
|
data: {
|
||||||
|
updatedOn: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return E.right(this.cast(updatedAccessToken));
|
||||||
|
} catch {
|
||||||
|
return E.left(ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Inputs to create a new PAT
|
||||||
|
export class CreateAccessTokenDto {
|
||||||
|
label: string;
|
||||||
|
expiryInDays: number | null;
|
||||||
|
}
|
||||||
17
packages/hoppscotch-backend/src/access-token/helper.ts
Normal file
17
packages/hoppscotch-backend/src/access-token/helper.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { AccessToken } from 'src/types/AccessToken';
|
||||||
|
|
||||||
|
// Response type of PAT creation method
|
||||||
|
export type CreateAccessTokenResponse = {
|
||||||
|
token: string;
|
||||||
|
info: AccessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Response type of any error in PAT module
|
||||||
|
export type CLIErrorResponse = {
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a CLIErrorResponse object
|
||||||
|
export function createCLIErrorResponse(reason: string): CLIErrorResponse {
|
||||||
|
return { reason };
|
||||||
|
}
|
||||||
@@ -74,6 +74,7 @@ const dbAdminUsers: DbUser[] = [
|
|||||||
refreshToken: 'refreshToken',
|
refreshToken: 'refreshToken',
|
||||||
currentRESTSession: '',
|
currentRESTSession: '',
|
||||||
currentGQLSession: '',
|
currentGQLSession: '',
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,20 +86,10 @@ const dbAdminUsers: DbUser[] = [
|
|||||||
refreshToken: 'refreshToken',
|
refreshToken: 'refreshToken',
|
||||||
currentRESTSession: '',
|
currentRESTSession: '',
|
||||||
currentGQLSession: '',
|
currentGQLSession: '',
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const dbNonAminUser: DbUser = {
|
|
||||||
uid: 'uid 3',
|
|
||||||
displayName: 'displayName',
|
|
||||||
email: 'email@email.com',
|
|
||||||
photoURL: 'photoURL',
|
|
||||||
isAdmin: false,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: '',
|
|
||||||
currentGQLSession: '',
|
|
||||||
createdOn: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AdminService', () => {
|
describe('AdminService', () => {
|
||||||
describe('fetchInvitedUsers', () => {
|
describe('fetchInvitedUsers', () => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
EnableAndDisableSSOArgs,
|
EnableAndDisableSSOArgs,
|
||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from 'src/infra-config/helper';
|
import { ServiceStatus } from 'src/infra-config/helper';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@@ -274,10 +274,10 @@ export class InfraResolver {
|
|||||||
async infraConfigs(
|
async infraConfigs(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'configNames',
|
name: 'configNames',
|
||||||
type: () => [InfraConfigEnumForClient],
|
type: () => [InfraConfigEnum],
|
||||||
description: 'Configs to fetch',
|
description: 'Configs to fetch',
|
||||||
})
|
})
|
||||||
names: InfraConfigEnumForClient[],
|
names: InfraConfigEnum[],
|
||||||
) {
|
) {
|
||||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { loadInfraConfiguration } from './infra-config/helper';
|
|||||||
import { MailerModule } from './mailer/mailer.module';
|
import { MailerModule } from './mailer/mailer.module';
|
||||||
import { PosthogModule } from './posthog/posthog.module';
|
import { PosthogModule } from './posthog/posthog.module';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { AccessTokenModule } from './access-token/access-token.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -100,6 +102,8 @@ import { ScheduleModule } from '@nestjs/schedule';
|
|||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
PosthogModule,
|
PosthogModule,
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
|
HealthModule,
|
||||||
|
AccessTokenModule,
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { SignInMagicDto } from './dto/signin-magic.dto';
|
import { SignInMagicDto } from './dto/signin-magic.dto';
|
||||||
@@ -18,12 +19,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import {
|
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
|
||||||
AuthProvider,
|
|
||||||
authCookieHandler,
|
|
||||||
authProviderCheck,
|
|
||||||
throwHTTPErr,
|
|
||||||
} from './helper';
|
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
@@ -31,6 +27,8 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
|
|||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
import { UserLastLoginInterceptor } from 'src/interceptors/user-last-login.interceptor';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -114,6 +112,7 @@ export class AuthController {
|
|||||||
@Get('google/callback')
|
@Get('google/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(GoogleSSOGuard)
|
@UseGuards(GoogleSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async googleAuthRedirect(@Request() req, @Res() res) {
|
async googleAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
@@ -139,6 +138,7 @@ export class AuthController {
|
|||||||
@Get('github/callback')
|
@Get('github/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(GithubSSOGuard)
|
@UseGuards(GithubSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async githubAuthRedirect(@Request() req, @Res() res) {
|
async githubAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
@@ -164,6 +164,7 @@ export class AuthController {
|
|||||||
@Get('microsoft/callback')
|
@Get('microsoft/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(MicrosoftSSOGuard)
|
@UseGuards(MicrosoftSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async microsoftAuthRedirect(@Request() req, @Res() res) {
|
async microsoftAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
|
|||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
import {
|
||||||
|
isInfraConfigTablePopulated,
|
||||||
|
loadInfraConfiguration,
|
||||||
|
} from 'src/infra-config/helper';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
|
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
|
||||||
|
if (!isInfraConfigPopulated) {
|
||||||
|
return { module: AuthModule };
|
||||||
|
}
|
||||||
|
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
@@ -172,9 +173,11 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
||||||
|
// usersService.updateUserLastLoggedOn
|
||||||
|
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
|
||||||
|
|
||||||
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -197,9 +200,11 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
||||||
|
// usersService.updateUserLastLoggedOn
|
||||||
|
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
|
||||||
|
|
||||||
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -239,7 +244,7 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -264,7 +269,7 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockRejectedValueOnce('RecordNotFound');
|
mockPrisma.verificationToken.delete.mockRejectedValueOnce('RecordNotFound');
|
||||||
|
|
||||||
@@ -280,7 +285,7 @@ describe('generateAuthTokens', () => {
|
|||||||
test('Should successfully generate tokens with valid inputs', async () => {
|
test('Should successfully generate tokens with valid inputs', async () => {
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
|
|
||||||
const result = await authService.generateAuthTokens(user.uid);
|
const result = await authService.generateAuthTokens(user.uid);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -292,7 +297,7 @@ describe('generateAuthTokens', () => {
|
|||||||
test('Should throw USER_NOT_FOUND when updating refresh tokens fails', async () => {
|
test('Should throw USER_NOT_FOUND when updating refresh tokens fails', async () => {
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -319,7 +324,7 @@ describe('refreshAuthTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -348,7 +353,7 @@ describe('refreshAuthTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb');
|
mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb');
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.right({
|
E.right({
|
||||||
...user,
|
...user,
|
||||||
refreshToken: 'sdhjcbjsdhcbshjdcb',
|
refreshToken: 'sdhjcbjsdhcbshjdcb',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from 'src/types/AuthTokens';
|
} from 'src/types/AuthTokens';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
@@ -112,12 +112,12 @@ export class AuthService {
|
|||||||
|
|
||||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
|
|
||||||
const updatedUser = await this.usersService.UpdateUserRefreshToken(
|
const updatedUser = await this.usersService.updateUserRefreshToken(
|
||||||
refreshTokenHash,
|
refreshTokenHash,
|
||||||
userUid,
|
userUid,
|
||||||
);
|
);
|
||||||
if (E.isLeft(updatedUser))
|
if (E.isLeft(updatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: updatedUser.left,
|
message: updatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -255,7 +255,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async verifyMagicLinkTokens(
|
async verifyMagicLinkTokens(
|
||||||
magicLinkIDTokens: VerifyMagicDto,
|
magicLinkIDTokens: VerifyMagicDto,
|
||||||
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
|
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
|
||||||
const passwordlessTokens = await this.validatePasswordlessTokens(
|
const passwordlessTokens = await this.validatePasswordlessTokens(
|
||||||
magicLinkIDTokens,
|
magicLinkIDTokens,
|
||||||
);
|
);
|
||||||
@@ -320,6 +320,8 @@ export class AuthService {
|
|||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.usersService.updateUserLastLoggedOn(passwordlessTokens.value.userUid);
|
||||||
|
|
||||||
return E.right(tokens.right);
|
return E.right(tokens.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +375,7 @@ export class AuthService {
|
|||||||
if (usersCount === 1) {
|
if (usersCount === 1) {
|
||||||
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
||||||
if (E.isLeft(elevatedUser))
|
if (E.isLeft(elevatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: elevatedUser.left,
|
message: elevatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
@@ -25,15 +24,6 @@ export enum AuthProvider {
|
|||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function allows throw to be used as an expression
|
|
||||||
* @param errMessage Message present in the error message
|
|
||||||
*/
|
|
||||||
export function throwHTTPErr(errorData: AuthError): never {
|
|
||||||
const { message, statusCode } = errorData;
|
|
||||||
throw new HttpException(message, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets and returns the cookies in the response object on successful authentication
|
* Sets and returns the cookies in the response object on successful authentication
|
||||||
* @param res Express Response Object
|
* @param res Express Response Object
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
|
||||||
scope: [configService.get('GITHUB_SCOPE')],
|
scope: [configService.get('INFRA.GITHUB_SCOPE')],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
|
||||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
|
||||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
|
||||||
tenant: configService.get('MICROSOFT_TENANT'),
|
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ export const USER_ALREADY_INVITED = 'admin/user_already_invited' as const;
|
|||||||
*/
|
*/
|
||||||
export const USER_UPDATE_FAILED = 'user/update_failed' as const;
|
export const USER_UPDATE_FAILED = 'user/update_failed' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User display name validation failure
|
||||||
|
* (UserService)
|
||||||
|
*/
|
||||||
|
export const USER_SHORT_DISPLAY_NAME = 'user/short_display_name' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User deletion failure
|
* User deletion failure
|
||||||
* (UserService)
|
* (UserService)
|
||||||
@@ -228,6 +234,12 @@ export const TEAM_COL_NOT_SAME_PARENT =
|
|||||||
export const TEAM_COL_SAME_NEXT_COLL =
|
export const TEAM_COL_SAME_NEXT_COLL =
|
||||||
'team_coll/collection_and_next_collection_are_same';
|
'team_coll/collection_and_next_collection_are_same';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection search failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COL_SEARCH_FAILED = 'team_coll/team_collection_search_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Team Collection Re-Ordering Failed
|
* Team Collection Re-Ordering Failed
|
||||||
* (TeamCollectionService)
|
* (TeamCollectionService)
|
||||||
@@ -283,6 +295,13 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
|||||||
export const TEAM_COLL_DATA_INVALID =
|
export const TEAM_COLL_DATA_INVALID =
|
||||||
'team_coll/team_coll_data_invalid' as const;
|
'team_coll/team_coll_data_invalid' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection parent tree generation failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COLL_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_coll/team_coll_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -308,6 +327,19 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
|
|||||||
*/
|
*/
|
||||||
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request search failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_SEARCH_FAILED = 'team_req/team_request_search_failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request parent tree generation failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_req/team_req_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No Postmark Sender Email defined
|
* No Postmark Sender Email defined
|
||||||
* (AuthService)
|
* (AuthService)
|
||||||
@@ -705,6 +737,13 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
|||||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||||
'infra_config/service_not_configured' as const;
|
'infra_config/service_not_configured' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infra Config update/fetch operation not allowed
|
||||||
|
* (InfraConfigService)
|
||||||
|
*/
|
||||||
|
export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
|
||||||
|
'infra_config/operation_not_allowed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message for when the database table does not exist
|
* Error message for when the database table does not exist
|
||||||
* (InfraConfigService)
|
* (InfraConfigService)
|
||||||
@@ -717,3 +756,44 @@ export const DATABASE_TABLE_NOT_EXIST =
|
|||||||
* (InfraConfigService)
|
* (InfraConfigService)
|
||||||
*/
|
*/
|
||||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inputs supplied are invalid
|
||||||
|
*/
|
||||||
|
export const INVALID_PARAMS = 'invalid_parameters' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provided label for the access-token is short (less than 3 characters)
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKEN_LABEL_SHORT = 'access_token/label_too_short';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provided expiryInDays value is not valid
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKEN_EXPIRY_INVALID = 'access_token/expiry_days_invalid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provided PAT ID is invalid
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKEN_NOT_FOUND = 'access_token/access_token_not_found';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccessTokens is expired
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKENS_EXPIRED = 'TOKEN_EXPIRED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccessTokens is invalid
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKENS_INVALID = 'TOKEN_INVALID';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccessTokens is invalid
|
||||||
|
* (AccessTokenService)
|
||||||
|
*/
|
||||||
|
export const ACCESS_TOKENS_INVALID_DATA_ID = 'INVALID_ID';
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { AccessTokenService } from 'src/access-token/access-token.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { ACCESS_TOKENS_EXPIRED, ACCESS_TOKENS_INVALID } from 'src/errors';
|
||||||
|
import { createCLIErrorResponse } from 'src/access-token/helper';
|
||||||
|
@Injectable()
|
||||||
|
export class PATAuthGuard implements CanActivate {
|
||||||
|
constructor(private accessTokenService: AccessTokenService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
if (!token) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
createCLIErrorResponse(ACCESS_TOKENS_INVALID),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAccessToken = await this.accessTokenService.getUserPAT(token);
|
||||||
|
if (E.isLeft(userAccessToken))
|
||||||
|
throw new BadRequestException(
|
||||||
|
createCLIErrorResponse(ACCESS_TOKENS_INVALID),
|
||||||
|
);
|
||||||
|
request.user = userAccessToken.right.user;
|
||||||
|
|
||||||
|
const accessToken = userAccessToken.right;
|
||||||
|
if (accessToken.expiresOn === null) return true;
|
||||||
|
|
||||||
|
const today = DateTime.now().toISO();
|
||||||
|
if (accessToken.expiresOn.toISOString() > today) return true;
|
||||||
|
|
||||||
|
throw new BadRequestException(
|
||||||
|
createCLIErrorResponse(ACCESS_TOKENS_EXPIRED),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/hoppscotch-backend/src/health/health.controller.ts
Normal file
24
packages/hoppscotch-backend/src/health/health.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
HealthCheck,
|
||||||
|
HealthCheckService,
|
||||||
|
PrismaHealthIndicator,
|
||||||
|
} from '@nestjs/terminus';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
||||||
|
@Controller('health')
|
||||||
|
export class HealthController {
|
||||||
|
constructor(
|
||||||
|
private health: HealthCheckService,
|
||||||
|
private prismaHealth: PrismaHealthIndicator,
|
||||||
|
private prisma: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@HealthCheck()
|
||||||
|
check() {
|
||||||
|
return this.health.check([
|
||||||
|
async () => this.prismaHealth.pingCheck('database', this.prisma),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/hoppscotch-backend/src/health/health.module.ts
Normal file
10
packages/hoppscotch-backend/src/health/health.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { TerminusModule } from '@nestjs/terminus';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, TerminusModule],
|
||||||
|
controllers: [HealthController],
|
||||||
|
})
|
||||||
|
export class HealthModule {}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
import {
|
||||||
|
AUTH_PROVIDER_NOT_CONFIGURED,
|
||||||
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
@@ -14,14 +17,21 @@ const AuthProviderConfigurations = {
|
|||||||
[AuthProvider.GOOGLE]: [
|
[AuthProvider.GOOGLE]: [
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.GITHUB]: [
|
[AuthProvider.GITHUB]: [
|
||||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GITHUB_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.MICROSOFT]: [
|
[AuthProvider.MICROSOFT]: [
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
|
InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
],
|
],
|
||||||
[AuthProvider.EMAIL]: [
|
[AuthProvider.EMAIL]: [
|
||||||
InfraConfigEnum.MAILER_SMTP_URL,
|
InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
@@ -54,6 +64,139 @@ export async function loadInfraConfiguration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the default values from .env file and return them as an array
|
||||||
|
* @returns Array of default infra configs
|
||||||
|
*/
|
||||||
|
export async function getDefaultInfraConfigs(): Promise<
|
||||||
|
{ name: InfraConfigEnum; value: string }[]
|
||||||
|
> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
|
||||||
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
|
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
|
value: process.env.MAILER_SMTP_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
value: process.env.MAILER_ADDRESS_FROM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
value: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
|
value: process.env.GOOGLE_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
|
value: process.env.GOOGLE_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
|
value: process.env.GITHUB_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
value: process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
|
value: process.env.GITHUB_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_SCOPE,
|
||||||
|
value: process.env.GITHUB_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
value: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
|
value: process.env.MICROSOFT_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
|
value: process.env.MICROSOFT_TENANT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: getConfiguredSSOProviders(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
value: false.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
value: generateAnalyticsUserId(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return infraConfigDefaultObjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the missing entries in the 'infra_config' table
|
||||||
|
* @returns Array of InfraConfig
|
||||||
|
*/
|
||||||
|
export async function getMissingInfraConfigEntries() {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
const [dbInfraConfigs, infraConfigDefaultObjs] = await Promise.all([
|
||||||
|
prisma.infraConfig.findMany(),
|
||||||
|
getDefaultInfraConfigs(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const missingEntries = infraConfigDefaultObjs.filter(
|
||||||
|
(config) =>
|
||||||
|
!dbInfraConfigs.some((dbConfig) => dbConfig.name === config.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
return missingEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if 'infra_config' table is loaded with all entries
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export async function isInfraConfigTablePopulated(): Promise<boolean> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
try {
|
||||||
|
const propsRemainingToInsert = await getMissingInfraConfigEntries();
|
||||||
|
|
||||||
|
if (propsRemainingToInsert.length > 0) {
|
||||||
|
console.log(
|
||||||
|
'Infra Config table is not populated with all entries. Populating now...',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the app after 5 seconds
|
* Stop the app after 5 seconds
|
||||||
* (Docker will re-start the app)
|
* (Docker will re-start the app)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { InfraConfigService } from './infra-config.service';
|
|||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
||||||
import { throwHTTPErr } from 'src/auth/helper';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'site', version: '1' })
|
@Controller({ path: 'site', version: '1' })
|
||||||
@@ -17,11 +17,11 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async fetchSetupInfo() {
|
async fetchSetupInfo() {
|
||||||
const status = await this.infraConfigService.get(
|
const status = await this.infraConfigService.get(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(status))
|
if (E.isLeft(status))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: status.left,
|
message: status.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -32,13 +32,13 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async setSetupAsComplete() {
|
async setSetupAsComplete() {
|
||||||
const res = await this.infraConfigService.update(
|
const res = await this.infraConfigService.update(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
false.toString(),
|
false.toString(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(res))
|
if (E.isLeft(res))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: res.left,
|
message: res.left,
|
||||||
statusCode: HttpStatus.FORBIDDEN,
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -8,7 +8,7 @@ export class InfraConfig {
|
|||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
@@ -16,7 +16,7 @@ export class InfraConfig {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(InfraConfigEnumForClient, {
|
registerEnumType(InfraConfigEnum, {
|
||||||
name: 'InfraConfigEnum',
|
name: 'InfraConfigEnum',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import {
|
import {
|
||||||
InfraConfigEnum,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
InfraConfigEnumForClient,
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/types/InfraConfig';
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as helper from './helper';
|
import * as helper from './helper';
|
||||||
|
import { InfraConfig as dbInfraConfig } from '@prisma/client';
|
||||||
|
import { InfraConfig } from './infra-config.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
|
|||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const INITIALIZED_DATE_CONST = new Date();
|
||||||
|
const dbInfraConfigs: dbInfraConfig[] = [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: 'abcdefghijkl',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: 'google',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const infraConfigs: InfraConfig[] = [
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[0].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[0].value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[1].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[1].value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('InfraConfigService', () => {
|
describe('InfraConfigService', () => {
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
|
it('should update the infra config without backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
const result = await infraConfigService.update(name, value);
|
||||||
|
|
||||||
|
expect(helper.stopApp).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the infra config with backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
|
||||||
|
const result = await infraConfigService.update(name, value, true);
|
||||||
|
|
||||||
|
expect(helper.stopApp).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the infra config', async () => {
|
it('should update the infra config', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should get the infra config', async () => {
|
it('should get the infra config', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass correct params to prisma findUnique', async () => {
|
it('should pass correct params to prisma findUnique', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
await infraConfigService.get(name);
|
await infraConfigService.get(name);
|
||||||
|
|
||||||
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the infra config does not exist', async () => {
|
it('should throw an error if the infra config does not exist', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
||||||
|
|
||||||
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
|
|||||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMany', () => {
|
||||||
|
it('should throw error if any disallowed names are provided', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
});
|
||||||
|
it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
disallowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames, false);
|
||||||
|
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => disallowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return right with infraConfigs if Prisma query succeeds', async () => {
|
||||||
|
const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
allowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(allowedNames);
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => allowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ import { InfraConfig } from './infra-config.model';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import {
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
InfraConfigEnum,
|
|
||||||
InfraConfigEnumForClient,
|
|
||||||
} from 'src/types/InfraConfig';
|
|
||||||
import {
|
import {
|
||||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
INFRA_CONFIG_INVALID_INPUT,
|
INFRA_CONFIG_INVALID_INPUT,
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
INFRA_CONFIG_NOT_LISTED,
|
|
||||||
INFRA_CONFIG_RESET_FAILED,
|
INFRA_CONFIG_RESET_FAILED,
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||||
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
import {
|
||||||
|
throwErr,
|
||||||
|
validateSMTPEmail,
|
||||||
|
validateSMTPUrl,
|
||||||
|
validateUrl,
|
||||||
|
} from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
ServiceStatus,
|
ServiceStatus,
|
||||||
generateAnalyticsUserId,
|
getDefaultInfraConfigs,
|
||||||
getConfiguredSSOProviders,
|
getMissingInfraConfigEntries,
|
||||||
stopApp,
|
stopApp,
|
||||||
} from './helper';
|
} from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
@@ -35,90 +37,31 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
||||||
|
EXCLUDE_FROM_UPDATE_CONFIGS = [
|
||||||
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
];
|
||||||
|
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
||||||
|
EXCLUDE_FROM_FETCH_CONFIGS = [
|
||||||
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
];
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.initializeInfraConfigTable();
|
await this.initializeInfraConfigTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultInfraConfigs(): Promise<
|
|
||||||
{ name: InfraConfigEnum; value: string }[]
|
|
||||||
> {
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
|
||||||
value: process.env.MAILER_SMTP_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
|
||||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
|
||||||
value: process.env.GITHUB_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
|
||||||
value: process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: getConfiguredSSOProviders(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
|
||||||
value: false.toString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
|
||||||
value: generateAnalyticsUserId(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the 'infra_config' table with values from .env
|
* Initialize the 'infra_config' table with values from .env
|
||||||
* @description This function create rows 'infra_config' in very first time (only once)
|
* @description This function create rows 'infra_config' in very first time (only once)
|
||||||
*/
|
*/
|
||||||
async initializeInfraConfigTable() {
|
async initializeInfraConfigTable() {
|
||||||
try {
|
try {
|
||||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
const propsToInsert = await getMissingInfraConfigEntries();
|
||||||
const enumValues = Object.values(InfraConfigEnum);
|
|
||||||
|
|
||||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
|
||||||
|
|
||||||
// Check if all the 'names' are listed in the default values
|
|
||||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
|
||||||
throw new Error(INFRA_CONFIG_NOT_LISTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
|
||||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
|
||||||
const propsToInsert = infraConfigDefaultObjs.filter(
|
|
||||||
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (propsToInsert.length > 0) {
|
if (propsToInsert.length > 0) {
|
||||||
await this.prisma.infraConfig.createMany({ data: propsToInsert });
|
await this.prisma.infraConfig.createMany({ data: propsToInsert });
|
||||||
@@ -169,11 +112,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
|
||||||
value: string,
|
|
||||||
restartEnabled = false,
|
|
||||||
) {
|
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -197,6 +136,11 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
||||||
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
const isValidate = this.validateEnvValues(infraConfigs);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -230,12 +174,26 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case AuthProvider.GOOGLE:
|
case AuthProvider.GOOGLE:
|
||||||
return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GOOGLE_CLIENT_ID &&
|
||||||
|
configMap.GOOGLE_CLIENT_SECRET &&
|
||||||
|
configMap.GOOGLE_CALLBACK_URL &&
|
||||||
|
configMap.GOOGLE_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.GITHUB:
|
case AuthProvider.GITHUB:
|
||||||
return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GITHUB_CLIENT_ID &&
|
||||||
|
configMap.GITHUB_CLIENT_SECRET &&
|
||||||
|
configMap.GITHUB_CALLBACK_URL &&
|
||||||
|
configMap.GITHUB_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.MICROSOFT:
|
case AuthProvider.MICROSOFT:
|
||||||
return (
|
return (
|
||||||
configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET
|
configMap.MICROSOFT_CLIENT_ID &&
|
||||||
|
configMap.MICROSOFT_CLIENT_SECRET &&
|
||||||
|
configMap.MICROSOFT_CALLBACK_URL &&
|
||||||
|
configMap.MICROSOFT_SCOPE &&
|
||||||
|
configMap.MICROSOFT_TENANT
|
||||||
);
|
);
|
||||||
case AuthProvider.EMAIL:
|
case AuthProvider.EMAIL:
|
||||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||||
@@ -310,7 +268,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param name Name of the InfraConfig
|
* @param name Name of the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async get(name: InfraConfigEnumForClient) {
|
async get(name: InfraConfigEnum) {
|
||||||
try {
|
try {
|
||||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||||
where: { name },
|
where: { name },
|
||||||
@@ -325,9 +283,18 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
/**
|
/**
|
||||||
* Get InfraConfigs by names
|
* Get InfraConfigs by names
|
||||||
* @param names Names of the InfraConfigs
|
* @param names Names of the InfraConfigs
|
||||||
|
* @param checkDisallowedKeys If true, check if the names are allowed to fetch by client
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async getMany(names: InfraConfigEnumForClient[]) {
|
async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) {
|
||||||
|
if (checkDisallowedKeys) {
|
||||||
|
// Check if the names are allowed to fetch by client
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i]))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||||
where: { name: { in: names } },
|
where: { name: { in: names } },
|
||||||
@@ -353,25 +320,28 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* Reset all the InfraConfigs to their default values (from .env)
|
* Reset all the InfraConfigs to their default values (from .env)
|
||||||
*/
|
*/
|
||||||
async reset() {
|
async reset() {
|
||||||
|
// These are all the infra-configs that should not be reset
|
||||||
|
const RESET_EXCLUSION_LIST = [
|
||||||
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
|
||||||
|
(p) => RESET_EXCLUSION_LIST.includes(p.name) === false,
|
||||||
|
);
|
||||||
|
|
||||||
await this.prisma.infraConfig.deleteMany({
|
await this.prisma.infraConfig.deleteMany({
|
||||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
where: {
|
||||||
|
name: {
|
||||||
|
in: updatedInfraConfigDefaultObjs.map((p) => p.name),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hardcode t
|
|
||||||
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
|
|
||||||
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
);
|
|
||||||
await this.prisma.infraConfig.createMany({
|
await this.prisma.infraConfig.createMany({
|
||||||
data: [
|
data: updatedInfraConfigDefaultObjs,
|
||||||
...updatedInfraConfigDefaultObjs,
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: 'true',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stopApp();
|
stopApp();
|
||||||
@@ -387,36 +357,60 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
validateEnvValues(
|
validateEnvValues(
|
||||||
infraConfigs: {
|
infraConfigs: {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
name: InfraConfigEnum;
|
||||||
value: string;
|
value: string;
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
switch (infraConfigs[i].name) {
|
switch (infraConfigs[i].name) {
|
||||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GOOGLE_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GITHUB_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_TENANT:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class InfraConfigArgs {
|
export class InfraConfigArgs {
|
||||||
@Field(() => InfraConfigEnumForClient, {
|
@Field(() => InfraConfigEnum, {
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
import { AccessTokenService } from 'src/access-token/access-token.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccessTokenInterceptor implements NestInterceptor {
|
||||||
|
constructor(private readonly accessTokenService: AccessTokenService) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.handle().pipe(
|
||||||
|
map(async (data) => {
|
||||||
|
const userAccessToken =
|
||||||
|
await this.accessTokenService.updateLastUsedForPAT(token);
|
||||||
|
if (E.isLeft(userAccessToken)) throw new UnauthorizedException();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserLastLoginInterceptor implements NestInterceptor {
|
||||||
|
constructor(private userService: UserService) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const user: AuthUser = context.switchToHttp().getRequest().user;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.userService.updateUserLastLoggedOn(user.uid);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: createdOn,
|
||||||
createdOn: createdOn,
|
createdOn: createdOn,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
|||||||
25
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
25
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { TeamRequest } from '@prisma/client';
|
||||||
|
|
||||||
|
// Type of data returned from the query to obtain all search results
|
||||||
|
export type SearchQueryReturnType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'collection' | 'request';
|
||||||
|
method?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type of data returned from the query to obtain all parents
|
||||||
|
export type ParentTreeQueryReturnType = {
|
||||||
|
id: string;
|
||||||
|
parentID: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
// Type of data returned from the query to fetch collection details from CLI
|
||||||
|
export type GetCollectionResponse = {
|
||||||
|
id: string;
|
||||||
|
data: string | null;
|
||||||
|
title: string;
|
||||||
|
parentID: string | null;
|
||||||
|
folders: GetCollectionResponse[];
|
||||||
|
requests: TeamRequest[];
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
|
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
|
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
import { INVALID_PARAMS } from 'src/errors';
|
||||||
|
|
||||||
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
|
@Controller({ path: 'team-collection', version: '1' })
|
||||||
|
export class TeamCollectionController {
|
||||||
|
constructor(private readonly teamCollectionService: TeamCollectionService) {}
|
||||||
|
|
||||||
|
@Get('search/:teamID')
|
||||||
|
@RequiresTeamRole(
|
||||||
|
TeamMemberRole.VIEWER,
|
||||||
|
TeamMemberRole.EDITOR,
|
||||||
|
TeamMemberRole.OWNER,
|
||||||
|
)
|
||||||
|
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
|
||||||
|
async searchByTitle(
|
||||||
|
@Query('searchQuery') searchQuery: string,
|
||||||
|
@Param('teamID') teamID: string,
|
||||||
|
@Query('take') take: string,
|
||||||
|
@Query('skip') skip: string,
|
||||||
|
) {
|
||||||
|
if (!teamID || !searchQuery) {
|
||||||
|
return <RESTError>{
|
||||||
|
message: INVALID_PARAMS,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.teamCollectionService.searchByTitle(
|
||||||
|
searchQuery.trim(),
|
||||||
|
teamID,
|
||||||
|
parseInt(take),
|
||||||
|
parseInt(skip),
|
||||||
|
);
|
||||||
|
if (E.isLeft(res)) throwHTTPErr(res.left);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-membe
|
|||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
|
import { TeamCollectionController } from './team-collection.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
||||||
@@ -15,5 +16,6 @@ import { PubSubModule } from '../pubsub/pubsub.module';
|
|||||||
GqlCollectionTeamMemberGuard,
|
GqlCollectionTeamMemberGuard,
|
||||||
],
|
],
|
||||||
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
||||||
|
controllers: [TeamCollectionController],
|
||||||
})
|
})
|
||||||
export class TeamCollectionModule {}
|
export class TeamCollectionModule {}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_INVALID_COLL_ID,
|
TEAM_INVALID_COLL_ID,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
TEAM_NOT_OWNER,
|
TEAM_NOT_OWNER,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
@@ -19,15 +20,18 @@ import { PubSubService } from 'src/pubsub/pubsub.service';
|
|||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
import { TeamCollection } from './team-collection.model';
|
||||||
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
|
const mockTeamService = mockDeep<TeamService>();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const teamCollectionService = new TeamCollectionService(
|
const teamCollectionService = new TeamCollectionService(
|
||||||
mockPrisma,
|
mockPrisma,
|
||||||
mockPubSub as any,
|
mockPubSub as any,
|
||||||
|
mockTeamService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
@@ -39,6 +43,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
@@ -1738,3 +1743,63 @@ describe('updateTeamCollection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//ToDo: write test cases for exportCollectionsToJSON
|
//ToDo: write test cases for exportCollectionsToJSON
|
||||||
|
|
||||||
|
describe('getCollectionForCLI', () => {
|
||||||
|
test('should throw TEAM_COLL_NOT_FOUND if collectionID is invalid', async () => {
|
||||||
|
mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||||
|
'NotFoundError',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await teamCollectionService.getCollectionForCLI(
|
||||||
|
'invalidID',
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_MEMBER_NOT_FOUND if user not in same team', async () => {
|
||||||
|
mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||||
|
rootTeamCollection,
|
||||||
|
);
|
||||||
|
mockTeamService.getTeamMember.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await teamCollectionService.getCollectionForCLI(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
// test('should return the TeamCollection data for CLI', async () => {
|
||||||
|
// mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||||
|
// rootTeamCollection,
|
||||||
|
// );
|
||||||
|
// mockTeamService.getTeamMember.mockResolvedValue({
|
||||||
|
// membershipID: 'sdc3sfdv',
|
||||||
|
// userUid: user.uid,
|
||||||
|
// role: TeamMemberRole.OWNER,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const result = await teamCollectionService.getCollectionForCLI(
|
||||||
|
// rootTeamCollection.id,
|
||||||
|
// user.uid,
|
||||||
|
// );
|
||||||
|
// expect(result).toEqualRight({
|
||||||
|
// id: rootTeamCollection.id,
|
||||||
|
// data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
// title: rootTeamCollection.title,
|
||||||
|
// parentID: rootTeamCollection.parentID,
|
||||||
|
// folders: [
|
||||||
|
// {
|
||||||
|
// id: childTeamCollection.id,
|
||||||
|
// data: JSON.stringify(childTeamCollection.data),
|
||||||
|
// title: childTeamCollection.title,
|
||||||
|
// parentID: childTeamCollection.parentID,
|
||||||
|
// folders: [],
|
||||||
|
// requests: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// requests: [],
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
import { TeamCollection } from './team-collection.model';
|
||||||
import {
|
import {
|
||||||
@@ -14,20 +14,38 @@ import {
|
|||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COLL_DATA_INVALID,
|
TEAM_COLL_DATA_INVALID,
|
||||||
|
TEAM_REQ_SEARCH_FAILED,
|
||||||
|
TEAM_COL_SEARCH_FAILED,
|
||||||
|
TEAM_REQ_PARENT_TREE_GEN_FAILED,
|
||||||
|
TEAM_COLL_PARENT_TREE_GEN_FAILED,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { escapeSqlLikeString, isValidLength } from 'src/utils';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import {
|
||||||
|
Prisma,
|
||||||
|
TeamCollection as DBTeamCollection,
|
||||||
|
TeamRequest,
|
||||||
|
} from '@prisma/client';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
import { stringToJson } from 'src/utils';
|
import { stringToJson } from 'src/utils';
|
||||||
|
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
|
||||||
|
import {
|
||||||
|
GetCollectionResponse,
|
||||||
|
ParentTreeQueryReturnType,
|
||||||
|
SearchQueryReturnType,
|
||||||
|
} from './helper';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamCollectionService {
|
export class TeamCollectionService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
|
private readonly teamService: TeamService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
TITLE_LENGTH = 3;
|
TITLE_LENGTH = 3;
|
||||||
@@ -1056,4 +1074,376 @@ export class TeamCollectionService {
|
|||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections and TeamRequests by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
async searchByTitle(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take = 10,
|
||||||
|
skip = 0,
|
||||||
|
) {
|
||||||
|
// Fetch all collections and requests that match the search query
|
||||||
|
const searchResults: SearchQueryReturnType[] = [];
|
||||||
|
|
||||||
|
const matchedCollections = await this.searchCollections(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedCollections))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedCollections.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedCollections.right);
|
||||||
|
|
||||||
|
const matchedRequests = await this.searchRequests(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedRequests))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedRequests.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedRequests.right);
|
||||||
|
|
||||||
|
// Generate the parent tree for searchResults
|
||||||
|
const searchResultsWithTree: CollectionSearchNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < searchResults.length; i++) {
|
||||||
|
const fetchedParentTree = await this.fetchParentTree(searchResults[i]);
|
||||||
|
if (E.isLeft(fetchedParentTree))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: fetchedParentTree.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResultsWithTree.push({
|
||||||
|
type: searchResults[i].type,
|
||||||
|
title: searchResults[i].title,
|
||||||
|
method: searchResults[i].method,
|
||||||
|
id: searchResults[i].id,
|
||||||
|
path: !fetchedParentTree
|
||||||
|
? []
|
||||||
|
: (fetchedParentTree.right as CollectionSearchNode[]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right({ data: searchResultsWithTree });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
private async searchCollections(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take: number,
|
||||||
|
skip: number,
|
||||||
|
) {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
SELECT
|
||||||
|
id,title,'collection' AS type
|
||||||
|
FROM
|
||||||
|
"TeamCollection"
|
||||||
|
WHERE
|
||||||
|
"TeamCollection"."teamID"=${teamID}
|
||||||
|
AND
|
||||||
|
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
|
||||||
|
ORDER BY
|
||||||
|
similarity(title, ${searchQuery})
|
||||||
|
LIMIT ${take}
|
||||||
|
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
||||||
|
return E.right(res);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_COL_SEARCH_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamRequests by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
private async searchRequests(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take: number,
|
||||||
|
skip: number,
|
||||||
|
) {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
SELECT
|
||||||
|
id,title,request->>'method' as method,'request' AS type
|
||||||
|
FROM
|
||||||
|
"TeamRequest"
|
||||||
|
WHERE
|
||||||
|
"TeamRequest"."teamID"=${teamID}
|
||||||
|
AND
|
||||||
|
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
|
||||||
|
ORDER BY
|
||||||
|
similarity(title, ${searchQuery})
|
||||||
|
LIMIT ${take}
|
||||||
|
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
||||||
|
return E.right(res);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_REQ_SEARCH_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a search result
|
||||||
|
*
|
||||||
|
* @param searchResult The search result for which we want to generate the parent tree
|
||||||
|
* @returns The parent tree of the search result
|
||||||
|
*/
|
||||||
|
private async fetchParentTree(searchResult: SearchQueryReturnType) {
|
||||||
|
return searchResult.type === 'collection'
|
||||||
|
? await this.fetchCollectionParentTree(searchResult.id)
|
||||||
|
: await this.fetchRequestParentTree(searchResult.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a collection
|
||||||
|
*
|
||||||
|
* @param id The ID of the collection
|
||||||
|
* @returns The parent tree of the collection
|
||||||
|
*/
|
||||||
|
private async fetchCollectionParentTree(id: string) {
|
||||||
|
try {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
WITH RECURSIVE collection_tree AS (
|
||||||
|
SELECT tc.id, tc."parentID", tc.title
|
||||||
|
FROM "TeamCollection" AS tc
|
||||||
|
JOIN "TeamCollection" AS tr ON tc.id = tr."parentID"
|
||||||
|
WHERE tr.id = ${id}
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT parent.id, parent."parentID", parent.title
|
||||||
|
FROM "TeamCollection" AS parent
|
||||||
|
JOIN collection_tree AS ct ON parent.id = ct."parentID"
|
||||||
|
)
|
||||||
|
SELECT * FROM collection_tree;
|
||||||
|
`;
|
||||||
|
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionParentTree = this.generateParentTree(res);
|
||||||
|
return E.right(collectionParentTree);
|
||||||
|
} catch (error) {
|
||||||
|
E.left(TEAM_COLL_PARENT_TREE_GEN_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree from the collections
|
||||||
|
*
|
||||||
|
* @param parentCollections The parent collections
|
||||||
|
* @returns The parent tree of the parent collections
|
||||||
|
*/
|
||||||
|
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
|
||||||
|
function findChildren(id: string): CollectionSearchNode[] {
|
||||||
|
const collection = parentCollections.filter((item) => item.id === id)[0];
|
||||||
|
if (collection.parentID == null) {
|
||||||
|
return <CollectionSearchNode[]>[
|
||||||
|
{
|
||||||
|
id: collection.id,
|
||||||
|
title: collection.title,
|
||||||
|
type: 'collection' as const,
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = <CollectionSearchNode[]>[
|
||||||
|
{
|
||||||
|
id: collection.id,
|
||||||
|
title: collection.title,
|
||||||
|
type: 'collection' as const,
|
||||||
|
path: findChildren(collection.parentID),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCollections.length > 0) {
|
||||||
|
if (parentCollections[0].parentID == null) {
|
||||||
|
return <CollectionSearchNode[]>[
|
||||||
|
{
|
||||||
|
id: parentCollections[0].id,
|
||||||
|
title: parentCollections[0].title,
|
||||||
|
type: 'collection',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CollectionSearchNode[]>[
|
||||||
|
{
|
||||||
|
id: parentCollections[0].id,
|
||||||
|
title: parentCollections[0].title,
|
||||||
|
type: 'collection',
|
||||||
|
path: findChildren(parentCollections[0].parentID),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CollectionSearchNode[]>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a request
|
||||||
|
*
|
||||||
|
* @param id The ID of the request
|
||||||
|
* @returns The parent tree of the request
|
||||||
|
*/
|
||||||
|
private async fetchRequestParentTree(id: string) {
|
||||||
|
try {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
WITH RECURSIVE request_collection_tree AS (
|
||||||
|
SELECT tc.id, tc."parentID", tc.title
|
||||||
|
FROM "TeamCollection" AS tc
|
||||||
|
JOIN "TeamRequest" AS tr ON tc.id = tr."collectionID"
|
||||||
|
WHERE tr.id = ${id}
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT parent.id, parent."parentID", parent.title
|
||||||
|
FROM "TeamCollection" AS parent
|
||||||
|
JOIN request_collection_tree AS ct ON parent.id = ct."parentID"
|
||||||
|
)
|
||||||
|
SELECT * FROM request_collection_tree;
|
||||||
|
|
||||||
|
`;
|
||||||
|
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestParentTree = this.generateParentTree(res);
|
||||||
|
return E.right(requestParentTree);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all requests in a collection
|
||||||
|
*
|
||||||
|
* @param collectionID The Collection ID
|
||||||
|
* @returns A list of all requests in the collection
|
||||||
|
*/
|
||||||
|
private async getAllRequestsInCollection(collectionID: string) {
|
||||||
|
const dbTeamRequests = await this.prisma.teamRequest.findMany({
|
||||||
|
where: {
|
||||||
|
collectionID: collectionID,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
orderIndex: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamRequests = dbTeamRequests.map((tr) => {
|
||||||
|
return <TeamRequest>{
|
||||||
|
id: tr.id,
|
||||||
|
collectionID: tr.collectionID,
|
||||||
|
teamID: tr.teamID,
|
||||||
|
title: tr.title,
|
||||||
|
request: JSON.stringify(tr.request),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return teamRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Collection Tree for CLI
|
||||||
|
*
|
||||||
|
* @param parentID The parent Collection ID
|
||||||
|
* @returns Collection tree for CLI
|
||||||
|
*/
|
||||||
|
private async getCollectionTreeForCLI(parentID: string | null) {
|
||||||
|
const childCollections = await this.prisma.teamCollection.findMany({
|
||||||
|
where: { parentID },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: GetCollectionResponse[] = [];
|
||||||
|
|
||||||
|
for (const collection of childCollections) {
|
||||||
|
const folder: GetCollectionResponse = {
|
||||||
|
id: collection.id,
|
||||||
|
data: collection.data === null ? null : JSON.stringify(collection.data),
|
||||||
|
title: collection.title,
|
||||||
|
parentID: collection.parentID,
|
||||||
|
folders: await this.getCollectionTreeForCLI(collection.id),
|
||||||
|
requests: await this.getAllRequestsInCollection(collection.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
response.push(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Collection for CLI
|
||||||
|
*
|
||||||
|
* @param collectionID The Collection ID
|
||||||
|
* @param userUid The User UID
|
||||||
|
* @returns An Either of the Collection details
|
||||||
|
*/
|
||||||
|
async getCollectionForCLI(collectionID: string, userUid: string) {
|
||||||
|
try {
|
||||||
|
const collection = await this.prisma.teamCollection.findUniqueOrThrow({
|
||||||
|
where: { id: collectionID },
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
|
collection.teamID,
|
||||||
|
userUid,
|
||||||
|
);
|
||||||
|
if (!teamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
|
||||||
|
return E.right(<GetCollectionResponse>{
|
||||||
|
id: collection.id,
|
||||||
|
data: collection.data === null ? null : JSON.stringify(collection.data),
|
||||||
|
title: collection.title,
|
||||||
|
parentID: collection.parentID,
|
||||||
|
folders: await this.getCollectionTreeForCLI(collection.id),
|
||||||
|
requests: await this.getAllRequestsInCollection(collection.id),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,24 @@ import {
|
|||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
const mockPubSub = {
|
const mockPubSub = {
|
||||||
publish: jest.fn().mockResolvedValue(null),
|
publish: jest.fn().mockResolvedValue(null),
|
||||||
};
|
};
|
||||||
|
const mockTeamService = mockDeep<TeamService>();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const teamEnvironmentsService = new TeamEnvironmentsService(
|
const teamEnvironmentsService = new TeamEnvironmentsService(
|
||||||
mockPrisma,
|
mockPrisma,
|
||||||
mockPubSub as any,
|
mockPubSub as any,
|
||||||
|
mockTeamService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const teamEnvironment = {
|
const teamEnvironment = {
|
||||||
@@ -380,4 +385,47 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
expect(result).toEqual(0);
|
expect(result).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getTeamEnvironmentForCLI', () => {
|
||||||
|
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
||||||
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
|
teamEnvironment,
|
||||||
|
);
|
||||||
|
mockTeamService.getTeamMember.mockResolvedValue({
|
||||||
|
membershipID: 'sdc3sfdv',
|
||||||
|
userUid: '123454',
|
||||||
|
role: TeamMemberRole.OWNER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.getTeamEnvironmentForCLI(
|
||||||
|
teamEnvironment.id,
|
||||||
|
'123454',
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(teamEnvironment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
||||||
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
|
'RejectOnNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_MEMBER_NOT_FOUND if user not in same team', async () => {
|
||||||
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
|
teamEnvironment,
|
||||||
|
);
|
||||||
|
mockTeamService.getTeamMember.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.getTeamEnvironmentForCLI(
|
||||||
|
teamEnvironment.id,
|
||||||
|
'333',
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import { TeamEnvironment } from './team-environments.model';
|
|||||||
import {
|
import {
|
||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
|
import { TeamService } from 'src/team/team.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamEnvironmentsService {
|
export class TeamEnvironmentsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
|
private readonly teamService: TeamService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
TITLE_LENGTH = 3;
|
TITLE_LENGTH = 3;
|
||||||
@@ -242,4 +245,30 @@ export class TeamEnvironmentsService {
|
|||||||
});
|
});
|
||||||
return envCount;
|
return envCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details of a TeamEnvironment for CLI.
|
||||||
|
*
|
||||||
|
* @param id TeamEnvironment ID
|
||||||
|
* @param userUid User UID
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async getTeamEnvironmentForCLI(id: string, userUid: string) {
|
||||||
|
try {
|
||||||
|
const teamEnvironment =
|
||||||
|
await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
|
teamEnvironment.teamID,
|
||||||
|
userUid,
|
||||||
|
);
|
||||||
|
if (!teamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
|
||||||
|
return E.right(teamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { TeamService } from '../../team/team.service';
|
||||||
|
import { TeamMemberRole } from '../../team/team.model';
|
||||||
|
import {
|
||||||
|
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
|
||||||
|
BUG_AUTH_NO_USER_CTX,
|
||||||
|
BUG_TEAM_NO_TEAM_ID,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RESTTeamMemberGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly teamService: TeamService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||||
|
'requiresTeamRole',
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
if (!requireRoles)
|
||||||
|
throwHTTPErr({ message: BUG_TEAM_NO_REQUIRE_TEAM_ROLE, statusCode: 400 });
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const { user } = request;
|
||||||
|
if (user == undefined)
|
||||||
|
throwHTTPErr({ message: BUG_AUTH_NO_USER_CTX, statusCode: 400 });
|
||||||
|
|
||||||
|
const teamID = request.params.teamID;
|
||||||
|
if (!teamID)
|
||||||
|
throwHTTPErr({ message: BUG_TEAM_NO_TEAM_ID, statusCode: 400 });
|
||||||
|
|
||||||
|
const teamMember = await this.teamService.getTeamMember(teamID, user.uid);
|
||||||
|
if (!teamMember)
|
||||||
|
throwHTTPErr({ message: TEAM_MEMBER_NOT_FOUND, statusCode: 404 });
|
||||||
|
|
||||||
|
if (requireRoles.includes(teamMember.role)) return true;
|
||||||
|
|
||||||
|
throwHTTPErr({ message: TEAM_NOT_REQUIRED_ROLE, statusCode: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/hoppscotch-backend/src/types/AccessToken.ts
Normal file
7
packages/hoppscotch-backend/src/types/AccessToken.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type AccessToken = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
createdOn: Date;
|
||||||
|
lastUsedOn: Date;
|
||||||
|
expiresOn: null | Date;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Response type of results from the search query
|
||||||
|
export type CollectionSearchNode = {
|
||||||
|
/** Encodes the hierarchy of where the node is **/
|
||||||
|
path: CollectionSearchNode[];
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'request';
|
||||||
|
title: string;
|
||||||
|
method: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'collection';
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -4,12 +4,19 @@ export enum InfraConfigEnum {
|
|||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
|
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||||
|
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||||
|
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
|
||||||
|
GITHUB_SCOPE = 'GITHUB_SCOPE',
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL',
|
||||||
|
MICROSOFT_SCOPE = 'MICROSOFT_SCOPE',
|
||||||
|
MICROSOFT_TENANT = 'MICROSOFT_TENANT',
|
||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
|
||||||
@@ -17,20 +24,3 @@ export enum InfraConfigEnum {
|
|||||||
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InfraConfigEnumForClient {
|
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
|
||||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
|
||||||
|
|
||||||
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { HttpStatus } from '@nestjs/common';
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Custom interface to handle errors specific to Auth module
|
** Custom interface to handle errors for REST modules such as Auth, Admin modules
|
||||||
** Since its REST we need to return the HTTP status code along with the error message
|
** Since its REST we need to return the HTTP status code along with the error message
|
||||||
*/
|
*/
|
||||||
export type AuthError = {
|
export type RESTError = {
|
||||||
message: string;
|
message: string | Record<string, string>;
|
||||||
statusCode: HttpStatus;
|
statusCode: HttpStatus;
|
||||||
};
|
};
|
||||||
@@ -38,6 +38,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://example.com/photo.png',
|
photoURL: 'https://example.com/photo.png',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
currentGQLSession: null,
|
currentGQLSession: null,
|
||||||
currentRESTSession: null,
|
currentRESTSession: null,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const user: AuthUser = {
|
|||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export class User {
|
|||||||
})
|
})
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
nullable: true,
|
||||||
|
description: 'Date when the user last logged in',
|
||||||
|
})
|
||||||
|
lastLoggedOn: Date;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Date when the user account was created',
|
description: 'Date when the user account was created',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,6 +58,29 @@ export class UserResolver {
|
|||||||
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
||||||
return updatedUser.right;
|
return updatedUser.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => User, {
|
||||||
|
description: 'Update a users display name',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
|
async updateDisplayName(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
|
@Args({
|
||||||
|
name: 'updatedDisplayName',
|
||||||
|
description: 'New name of user',
|
||||||
|
type: () => String,
|
||||||
|
})
|
||||||
|
updatedDisplayName: string,
|
||||||
|
) {
|
||||||
|
const updatedUser = await this.userService.updateUserDisplayName(
|
||||||
|
user.uid,
|
||||||
|
updatedDisplayName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
||||||
|
return updatedUser.right;
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Delete an user account',
|
description: 'Delete an user account',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
JSON_INVALID,
|
||||||
|
USERS_NOT_FOUND,
|
||||||
|
USER_NOT_FOUND,
|
||||||
|
USER_SHORT_DISPLAY_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
@@ -37,6 +42,7 @@ const user: AuthUser = {
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +55,7 @@ const adminUser: AuthUser = {
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +69,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,6 +81,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -84,6 +93,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -98,6 +108,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,6 +120,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,6 +132,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -480,6 +493,34 @@ describe('UserService', () => {
|
|||||||
);
|
);
|
||||||
expect(result).toEqualLeft(USER_NOT_FOUND);
|
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
test('should resolve left and error when short display name is passed', async () => {
|
||||||
|
const newDisplayName = '';
|
||||||
|
const result = await userService.updateUserDisplayName(
|
||||||
|
user.uid,
|
||||||
|
newDisplayName,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(USER_SHORT_DISPLAY_NAME);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateUserLastLoggedOn', () => {
|
||||||
|
test('should resolve right and update user last logged on', async () => {
|
||||||
|
const currentTime = new Date();
|
||||||
|
mockPrisma.user.update.mockResolvedValueOnce({
|
||||||
|
...user,
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await userService.updateUserLastLoggedOn(user.uid);
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||||
|
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||||
|
|
||||||
|
const result = await userService.updateUserLastLoggedOn('invalidUserUid');
|
||||||
|
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchAllUsers', () => {
|
describe('fetchAllUsers', () => {
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import * as T from 'fp-ts/Task';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { pipe, constVoid } from 'fp-ts/function';
|
import { pipe, constVoid } from 'fp-ts/function';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
USERS_NOT_FOUND,
|
||||||
|
USER_NOT_FOUND,
|
||||||
|
USER_SHORT_DISPLAY_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
import { SessionType, User } from './user.model';
|
import { SessionType, User } from './user.model';
|
||||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
@@ -110,7 +114,7 @@ export class UserService {
|
|||||||
* @param userUid User uid
|
* @param userUid User uid
|
||||||
* @returns Either of User with updated refreshToken
|
* @returns Either of User with updated refreshToken
|
||||||
*/
|
*/
|
||||||
async UpdateUserRefreshToken(refreshTokenHash: string, userUid: string) {
|
async updateUserRefreshToken(refreshTokenHash: string, userUid: string) {
|
||||||
try {
|
try {
|
||||||
const user = await this.prisma.user.update({
|
const user = await this.prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -170,6 +174,7 @@ export class UserService {
|
|||||||
displayName: userDisplayName,
|
displayName: userDisplayName,
|
||||||
email: profile.emails[0].value,
|
email: profile.emails[0].value,
|
||||||
photoURL: userPhotoURL,
|
photoURL: userPhotoURL,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
providerAccounts: {
|
providerAccounts: {
|
||||||
create: {
|
create: {
|
||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
@@ -217,7 +222,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update User displayName and photoURL
|
* Update User displayName and photoURL when logged in via a SSO provider
|
||||||
*
|
*
|
||||||
* @param user User object
|
* @param user User object
|
||||||
* @param profile Data received from SSO provider on the users account
|
* @param profile Data received from SSO provider on the users account
|
||||||
@@ -232,6 +237,7 @@ export class UserService {
|
|||||||
data: {
|
data: {
|
||||||
displayName: !profile.displayName ? null : profile.displayName,
|
displayName: !profile.displayName ? null : profile.displayName,
|
||||||
photoURL: !profile.photos ? null : profile.photos[0].value,
|
photoURL: !profile.photos ? null : profile.photos[0].value,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return E.right(updatedUser);
|
return E.right(updatedUser);
|
||||||
@@ -285,12 +291,16 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a user's data
|
* Update a user's displayName
|
||||||
* @param userUID User UID
|
* @param userUID User UID
|
||||||
* @param displayName User's displayName
|
* @param displayName User's displayName
|
||||||
* @returns a Either of User or error
|
* @returns a Either of User or error
|
||||||
*/
|
*/
|
||||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||||
|
if (!displayName || displayName.length === 0) {
|
||||||
|
return E.left(USER_SHORT_DISPLAY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbUpdatedUser = await this.prisma.user.update({
|
const dbUpdatedUser = await this.prisma.user.update({
|
||||||
where: { uid: userUID },
|
where: { uid: userUID },
|
||||||
@@ -308,6 +318,22 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user's lastLoggedOn timestamp
|
||||||
|
* @param userUID User UID
|
||||||
|
*/
|
||||||
|
async updateUserLastLoggedOn(userUid: string) {
|
||||||
|
try {
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { uid: userUid },
|
||||||
|
data: { lastLoggedOn: new Date() },
|
||||||
|
});
|
||||||
|
return E.right(true);
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate and parse currentRESTSession and currentGQLSession
|
* Validate and parse currentRESTSession and currentGQLSession
|
||||||
* @param sessionData string of the session
|
* @param sessionData string of the session
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext } from '@nestjs/common';
|
import { ExecutionContext, HttpException } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { AuthProvider } from './auth/helper';
|
||||||
|
import { RESTError } from './types/RESTError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -27,6 +28,15 @@ export function throwErr(errMessage: string): never {
|
|||||||
throw new Error(errMessage);
|
throw new Error(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows throw to be used as an expression
|
||||||
|
* @param errMessage Message present in the error message
|
||||||
|
*/
|
||||||
|
export function throwHTTPErr(errorData: RESTError): never {
|
||||||
|
const { message, statusCode } = errorData;
|
||||||
|
throw new HttpException(message, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints the given value to log and returns the same value.
|
* Prints the given value to log and returns the same value.
|
||||||
* Used for debugging functional pipelines.
|
* Used for debugging functional pipelines.
|
||||||
@@ -173,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the URL is valid or not
|
||||||
|
* @param url The URL to validate
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export const validateUrl = (url: string) => {
|
||||||
|
const urlRegex = /^(http|https):\/\/[^ "]+$/;
|
||||||
|
return urlRegex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String to JSON parser
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @param {str} str The string to parse
|
||||||
@@ -230,3 +250,39 @@ export function checkEnvironmentAuthProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds escape backslashes to the input so that it can be used inside
|
||||||
|
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
|
||||||
|
* function.
|
||||||
|
*
|
||||||
|
* Eg. "100%" -> "100\\%"
|
||||||
|
*
|
||||||
|
* Source: https://stackoverflow.com/a/32648526
|
||||||
|
*/
|
||||||
|
export function escapeSqlLikeString(str: string) {
|
||||||
|
if (typeof str != 'string') return str;
|
||||||
|
|
||||||
|
return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
|
||||||
|
switch (char) {
|
||||||
|
case '\0':
|
||||||
|
return '\\0';
|
||||||
|
case '\x08':
|
||||||
|
return '\\b';
|
||||||
|
case '\x09':
|
||||||
|
return '\\t';
|
||||||
|
case '\x1a':
|
||||||
|
return '\\z';
|
||||||
|
case '\n':
|
||||||
|
return '\\n';
|
||||||
|
case '\r':
|
||||||
|
return '\\r';
|
||||||
|
case '"':
|
||||||
|
case "'":
|
||||||
|
case '\\':
|
||||||
|
case '%':
|
||||||
|
return '\\' + char; // prepends a backslash to backslash, percent,
|
||||||
|
// and double/single quotes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,11 +52,34 @@ hopp [options or commands] arguments
|
|||||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
- Before you install Hoppscotch CLI you need to make sure you have the dependencies it requires to run.
|
||||||
|
- **Windows & macOS**: You will need `node-gyp` installed. Find instructions here: https://github.com/nodejs/node-gyp
|
||||||
|
- **Debian/Ubuntu derivatives**:
|
||||||
|
```sh
|
||||||
|
sudo apt-get install python g++ build-essential
|
||||||
|
```
|
||||||
|
- **Alpine Linux**:
|
||||||
|
```sh
|
||||||
|
sudo apk add python3 make g++
|
||||||
|
```
|
||||||
|
- **Amazon Linux (AMI)**
|
||||||
|
```sh
|
||||||
|
sudo yum install gcc72 gcc72-c++
|
||||||
|
```
|
||||||
|
- **Arch Linux**
|
||||||
|
```sh
|
||||||
|
sudo pacman -S make gcc python
|
||||||
|
```
|
||||||
|
- **RHEL/Fedora derivatives**:
|
||||||
|
```sh
|
||||||
|
sudo dnf install python3 make gcc gcc-c++ zlib-devel brotli-devel openssl-devel libuv-devel
|
||||||
|
```
|
||||||
|
|
||||||
Install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
|
|
||||||
```
|
- Once the dependencies are installed, install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
|
||||||
npm i -g @hoppscotch/cli
|
```
|
||||||
```
|
npm i -g @hoppscotch/cli
|
||||||
|
```
|
||||||
|
|
||||||
## **Developing:**
|
## **Developing:**
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// * The entry point of the CLI
|
// * The entry point of the CLI
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
import { cli } from "../dist/index.js";
|
import { cli } from "../dist/index.js";
|
||||||
|
|
||||||
cli(process.argv);
|
import { spawnSync } from "child_process";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
|
||||||
|
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||||
|
|
||||||
|
// As per isolated-vm documentation, we need to supply `--no-node-snapshot` for node >= 20
|
||||||
|
// src: https://github.com/laverdet/isolated-vm?tab=readme-ov-file#requirements
|
||||||
|
if (nodeVersion >= 20 && !process.execArgv.includes("--no-node-snapshot")) {
|
||||||
|
const argCopy = cloneDeep(process.argv);
|
||||||
|
|
||||||
|
// Replace first argument with --no-node-snapshot
|
||||||
|
// We can get argv[0] from process.argv0
|
||||||
|
argCopy[0] = "--no-node-snapshot";
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
process.argv0,
|
||||||
|
argCopy,
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit with the same status code as the spawned process
|
||||||
|
process.exit(result.status ?? 0);
|
||||||
|
} else {
|
||||||
|
cli(process.argv);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||||
"homepage": "https://hoppscotch.io",
|
"homepage": "https://hoppscotch.io",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -41,30 +41,29 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.6",
|
"axios": "1.6.7",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "5.3.0",
|
||||||
"commander": "^11.1.0",
|
"commander": "11.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"isolated-vm": "4.7.2",
|
||||||
"qs": "^6.11.2",
|
"lodash-es": "4.17.21",
|
||||||
"zod": "^3.22.4"
|
"qs": "6.11.2",
|
||||||
|
"verzod": "0.2.2",
|
||||||
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "2.1.1",
|
||||||
"@swc/core": "^1.3.105",
|
"@swc/core": "1.4.2",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "29.5.12",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/qs": "^6.9.11",
|
"@types/qs": "6.9.12",
|
||||||
"fp-ts": "^2.16.2",
|
"fp-ts": "2.16.2",
|
||||||
"jest": "^29.7.0",
|
"jest": "29.7.0",
|
||||||
"lodash": "^4.17.21",
|
"prettier": "3.2.5",
|
||||||
"prettier": "^3.2.4",
|
"qs": "6.11.2",
|
||||||
"qs": "^6.11.2",
|
"ts-jest": "29.1.2",
|
||||||
"ts-jest": "^29.1.2",
|
"tsup": "8.0.2",
|
||||||
"tsup": "^8.0.1",
|
"typescript": "5.3.3"
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"verzod": "^0.2.2",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe("Test `hopp test <file>` command:", () => {
|
|||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("Supplied collection export file validations", () => {
|
describe("Supplied collection export file validations", () => {
|
||||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||||
@@ -66,6 +66,43 @@ describe("Test `hopp test <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Versioned entities", () => {
|
||||||
|
describe("Collections & Requests", () => {
|
||||||
|
const testFixtures = [
|
||||||
|
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
|
||||||
|
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
|
||||||
|
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
|
||||||
|
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
|
||||||
|
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
|
||||||
|
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Environments", () => {
|
||||||
|
const testFixtures = [
|
||||||
|
{ fileName: "env-v0.json", version: 0 },
|
||||||
|
{ fileName: "env-v1.json", version: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
testFixtures.forEach(({ fileName, version }) => {
|
||||||
|
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
|
||||||
|
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
|
||||||
|
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
@@ -75,7 +112,8 @@ describe("Test `hopp test <file>` command:", () => {
|
|||||||
|
|
||||||
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
const args = `test ${getTestJsonFilePath(
|
||||||
"collection-level-headers-auth-coll.json", "collection"
|
"collection-level-headers-auth-coll.json",
|
||||||
|
"collection"
|
||||||
)}`;
|
)}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
@@ -84,7 +122,8 @@ describe("Test `hopp test <file>` command:", () => {
|
|||||||
|
|
||||||
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
|
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
const args = `test ${getTestJsonFilePath(
|
||||||
"pre-req-script-env-var-persistence-coll.json", "collection"
|
"pre-req-script-env-var-persistence-coll.json",
|
||||||
|
"collection"
|
||||||
)}`;
|
)}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
@@ -106,7 +145,8 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
|
|
||||||
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
|
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
|
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
|
||||||
"notjson-coll.txt", "collection"
|
"notjson-coll.txt",
|
||||||
|
"collection"
|
||||||
)}`;
|
)}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -123,7 +163,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
|
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
|
||||||
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
|
const ENV_PATH = getTestJsonFilePath(
|
||||||
|
"malformed-envs.json",
|
||||||
|
"environment"
|
||||||
|
);
|
||||||
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -142,7 +185,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Successfully resolves values from the supplied environment export file", async () => {
|
test("Successfully resolves values from the supplied environment export file", async () => {
|
||||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
const TESTS_PATH = getTestJsonFilePath(
|
||||||
|
"env-flag-tests-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
|
|
||||||
@@ -151,8 +197,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Successfully resolves environment variables referenced in the request body", async () => {
|
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
"req-body-env-vars-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
|
const ENVS_PATH = getTestJsonFilePath(
|
||||||
|
"req-body-env-vars-envs.json",
|
||||||
|
"environment"
|
||||||
|
);
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
@@ -160,7 +212,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Works with shorth `-e` flag", async () => {
|
test("Works with shorth `-e` flag", async () => {
|
||||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
const TESTS_PATH = getTestJsonFilePath(
|
||||||
|
"env-flag-tests-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||||
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
|
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
|
||||||
|
|
||||||
@@ -169,7 +224,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Secret environment variables", () => {
|
describe("Secret environment variables", () => {
|
||||||
jest.setTimeout(10000);
|
jest.setTimeout(100000);
|
||||||
|
|
||||||
// Reads secret environment values from system environment
|
// Reads secret environment values from system environment
|
||||||
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
|
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
|
||||||
@@ -183,7 +238,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
secretHeaderValue: "secret-header-value",
|
secretHeaderValue: "secret-header-value",
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
|
"secret-envs-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
|
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
@@ -197,8 +255,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
|
|
||||||
// Prefers values specified in the environment export file over values set in the system environment
|
// Prefers values specified in the environment export file over values set in the system environment
|
||||||
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
|
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
"secret-envs-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
|
const ENVS_PATH = getTestJsonFilePath(
|
||||||
|
"secret-supplied-values-envs.json",
|
||||||
|
"environment"
|
||||||
|
);
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
const { error, stdout } = await runCLI(args);
|
const { error, stdout } = await runCLI(args);
|
||||||
@@ -212,9 +276,13 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
// Values set from the scripting context takes the highest precedence
|
// Values set from the scripting context takes the highest precedence
|
||||||
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
|
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath(
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
"secret-envs-persistence-coll.json", "collection"
|
"secret-envs-persistence-coll.json",
|
||||||
|
"collection"
|
||||||
|
);
|
||||||
|
const ENVS_PATH = getTestJsonFilePath(
|
||||||
|
"secret-supplied-values-envs.json",
|
||||||
|
"environment"
|
||||||
);
|
);
|
||||||
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
const { error, stdout } = await runCLI(args);
|
const { error, stdout } = await runCLI(args);
|
||||||
@@ -227,10 +295,12 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
|||||||
|
|
||||||
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
|
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath(
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
"secret-envs-persistence-scripting-coll.json", "collection"
|
"secret-envs-persistence-scripting-coll.json",
|
||||||
|
"collection"
|
||||||
);
|
);
|
||||||
const ENVS_PATH = getTestJsonFilePath(
|
const ENVS_PATH = getTestJsonFilePath(
|
||||||
"secret-envs-persistence-scripting-envs.json", "environment"
|
"secret-envs-persistence-scripting-envs.json",
|
||||||
|
"environment"
|
||||||
);
|
);
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { isRESTCollection } from "../../../utils/checks";
|
|
||||||
|
|
||||||
describe("isRESTCollection", () => {
|
|
||||||
test("Undefined collection value.", () => {
|
|
||||||
expect(isRESTCollection(undefined)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid id value.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: 1,
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid requests value.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: "1",
|
|
||||||
requests: null,
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid folders value.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: "1",
|
|
||||||
requests: [],
|
|
||||||
folders: undefined,
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid RESTCollection(s) in folders.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: "1",
|
|
||||||
requests: [],
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
v: 1,
|
|
||||||
name: "test1",
|
|
||||||
id: "2",
|
|
||||||
requests: undefined,
|
|
||||||
folders: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid HoppRESTRequest(s) in requests.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: "1",
|
|
||||||
requests: [{}],
|
|
||||||
folders: [],
|
|
||||||
})
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Valid RESTCollection.", () => {
|
|
||||||
expect(
|
|
||||||
isRESTCollection({
|
|
||||||
v: 1,
|
|
||||||
name: "test",
|
|
||||||
id: "1",
|
|
||||||
requests: [],
|
|
||||||
folders: [],
|
|
||||||
})
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "coll-v1",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "coll-v1-child",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"url": "https://echo.hoppscotch.io",
|
||||||
|
"path": "/get",
|
||||||
|
"headers": [
|
||||||
|
{ "key": "Inactive-Header", "value": "Inactive Header", "active": false },
|
||||||
|
{ "key": "Authorization", "value": "Bearer token123", "active": true }
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{ "key": "key", "value": "value", "active": true },
|
||||||
|
{ "key": "inactive-key", "value": "inactive-param", "active": false }
|
||||||
|
],
|
||||||
|
"name": "req-v0-II",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"contentType": "application/json",
|
||||||
|
"body": "",
|
||||||
|
"auth": "Bearer Token",
|
||||||
|
"bearerToken": "token123"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"url": "https://echo.hoppscotch.io",
|
||||||
|
"path": "/get",
|
||||||
|
"headers": [
|
||||||
|
{ "key": "Inactive-Header", "value": "Inactive Header", "active": false },
|
||||||
|
{ "key": "Authorization", "value": "Bearer token123", "active": true }
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{ "key": "key", "value": "value", "active": true },
|
||||||
|
{ "key": "inactive-key", "value": "inactive-param", "active": false }
|
||||||
|
],
|
||||||
|
"name": "req-v0",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"contentType": "application/json",
|
||||||
|
"body": "",
|
||||||
|
"auth": "Bearer Token",
|
||||||
|
"bearerToken": "token123"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "coll-v1",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "coll-v1-child",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v1-II",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v1",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "coll-v2",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "coll-v2-child",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v2-II",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
},
|
||||||
|
"requestVariables": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v2",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
},
|
||||||
|
"requestVariables": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "coll-v2",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "coll-v2-child",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "3",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v3-II",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
},
|
||||||
|
"requestVariables": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "3",
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Inactive-Header",
|
||||||
|
"value": "Inactive Header",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer token123",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "value",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "inactive-key",
|
||||||
|
"value": "inactive-param",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "req-v3",
|
||||||
|
"method": "GET",
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"authType": "bearer",
|
||||||
|
"authActive": true,
|
||||||
|
"token": "token123"
|
||||||
|
},
|
||||||
|
"requestVariables": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "CollectionA",
|
"name": "CollectionA",
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "FolderA",
|
"name": "FolderA",
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "FolderB",
|
"name": "FolderB",
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "FolderC",
|
"name": "FolderC",
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestD",
|
"name": "RequestD",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestC",
|
"name": "RequestC",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -67,13 +68,14 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "api-key",
|
"authType": "api-key",
|
||||||
"authActive": true,
|
"authActive": true,
|
||||||
"addTo": "Headers",
|
"addTo": "HEADERS",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"value": "test-key"
|
"value": "test-key"
|
||||||
},
|
},
|
||||||
@@ -88,7 +90,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -104,6 +106,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -132,6 +135,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -149,16 +153,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "CollectionB",
|
"name": "CollectionB",
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"v": 1,
|
"v": 2,
|
||||||
"name": "FolderA",
|
"name": "FolderA",
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -174,6 +178,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -186,7 +191,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -202,6 +207,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -218,4 +224,4 @@
|
|||||||
"token": "BearerToken"
|
"token": "BearerToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "<<URL>>",
|
"endpoint": "<<URL>>",
|
||||||
"name": "test1",
|
"name": "test1",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -13,20 +13,18 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||||
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -34,17 +32,15 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"v": 1,
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -12,20 +12,18 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -33,17 +31,15 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"v": 1,
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -13,20 +13,18 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -34,17 +32,15 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true,
|
"authActive": true
|
||||||
"addTo": "Headers",
|
|
||||||
"key": "",
|
|
||||||
"value": ""
|
|
||||||
},
|
},
|
||||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "sample-req",
|
"name": "sample-req",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
||||||
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
|
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": true },
|
"auth": { "authType": "inherit", "authActive": true },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"name": "test-request",
|
"name": "test-request",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||||
},
|
},
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
|
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "tests",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"endpoint": "<<baseURL>>",
|
||||||
|
"name": "",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"method": "GET",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||||
|
"body": {
|
||||||
|
"contentType": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"requestVariables": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,9 +4,15 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": {
|
||||||
"body": { "body": null, "contentType": null },
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -17,13 +23,17 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"endpoint": "<<baseURL>>/headers",
|
"requestVariables": [],
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
"endpoint": "<<echoHoppBaseURL>>/headers",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
"contentType": "application/json"
|
"contentType": "application/json"
|
||||||
@@ -32,14 +42,21 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/post",
|
"requestVariables": [],
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"endpoint": "<<echoHoppBaseURL>>/post",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(JSON.parse(pw.response.body.data).secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": {
|
||||||
"body": { "body": null, "contentType": null },
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
"name": "test-secret-query-params",
|
"name": "test-secret-query-params",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [
|
"params": [
|
||||||
@@ -50,29 +67,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/get",
|
"requestVariables": [],
|
||||||
|
"endpoint": "<<echoHoppBaseURL>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
"username": "<<secretBasicAuthUsername>>",
|
"username": "<<secretBasicAuthUsername>>",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
},
|
},
|
||||||
"body": { "body": null, "contentType": null },
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
"name": "test-secret-basic-auth",
|
"name": "test-secret-basic-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"requestVariables": [],
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"endpoint": "<<httpbinBaseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n // The endpoint at times results in a `502` bad gateway\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -80,28 +102,42 @@
|
|||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
},
|
},
|
||||||
"body": { "body": null, "contentType": null },
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
"name": "test-secret-bearer-auth",
|
"name": "test-secret-bearer-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"requestVariables": [],
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"endpoint": "<<httpbinBaseURL>>/bearer",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n // Safeguard to prevent test failures due to the endpoint\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": {
|
||||||
"body": { "body": null, "contentType": null },
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
"name": "test-secret-fallback",
|
"name": "test-secret-fallback",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>",
|
"requestVariables": [],
|
||||||
|
"endpoint": "<<echoHoppBaseURL>>",
|
||||||
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": false },
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": false
|
||||||
|
},
|
||||||
"headers": []
|
"headers": []
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"v": 2,
|
"v": 2,
|
||||||
"name": "secret-envs-setters-coll",
|
"name": "secret-envs-persistence-coll",
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -23,12 +24,12 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"endpoint": "<<baseURL>>/headers",
|
"endpoint": "<<echoHoppBaseURL>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"name": "test-secret-headers-overrides",
|
"name": "test-secret-headers-overrides",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -47,12 +49,12 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"endpoint": "<<baseURL>>/headers",
|
"endpoint": "<<echoHoppBaseURL>>",
|
||||||
"testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})",
|
"testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})",
|
||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -64,13 +66,14 @@
|
|||||||
"name": "test-secret-body",
|
"name": "test-secret-body",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/post",
|
"endpoint": "<<echoHoppBaseURL>>/post",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(JSON.parse(pw.response.body.data).secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -88,13 +91,14 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/get",
|
"endpoint": "<<echoHoppBaseURL>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
@@ -108,13 +112,14 @@
|
|||||||
"name": "test-secret-basic-auth",
|
"name": "test-secret-basic-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"endpoint": "<<httpbinBaseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n // The endpoint at times results in a `502` bad gateway\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -129,9 +134,10 @@
|
|||||||
"name": "test-secret-bearer-auth",
|
"name": "test-secret-bearer-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"endpoint": "<<httpbinBaseURL>>/bearer",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n // Safeguard to prevent test failures due to the endpoint\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
"preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
"preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -140,4 +146,4 @@
|
|||||||
"authActive": false
|
"authActive": false
|
||||||
},
|
},
|
||||||
"headers": []
|
"headers": []
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "3",
|
||||||
"endpoint": "https://httpbin.org/post",
|
"endpoint": "https://echo.hoppscotch.io/post",
|
||||||
"name": "req",
|
"name": "req",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
@@ -18,11 +18,12 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")",
|
"preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")",
|
||||||
"testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
|
"testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request script\", () => {\n pw.expect(pw.response.body.headers[\"custom-header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request script\", () => {\n pw.expect(JSON.parse(pw.response.body.data).key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": false },
|
"auth": { "authType": "inherit", "authActive": false },
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "env-v0",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "https://echo.hoppscotch.io"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "env-v0",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "https://echo.hoppscotch.io",
|
||||||
|
"secret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -32,7 +32,12 @@
|
|||||||
"secret": true
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "baseURL",
|
"key": "echoHoppBaseURL",
|
||||||
|
"value": "https://echo.hoppscotch.io",
|
||||||
|
"secret": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "httpbinBaseURL",
|
||||||
"value": "https://httpbin.org",
|
"value": "https://httpbin.org",
|
||||||
"secret": false
|
"secret": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,12 @@
|
|||||||
"secret": true
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "baseURL",
|
"key": "echoHoppBaseURL",
|
||||||
|
"value": "https://echo.hoppscotch.io",
|
||||||
|
"secret": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "httpbinBaseURL",
|
||||||
"value": "https://httpbin.org",
|
"value": "https://httpbin.org",
|
||||||
"secret": false
|
"secret": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import { resolve } from "path";
|
|||||||
|
|
||||||
import { ExecResponse } from "./types";
|
import { ExecResponse } from "./types";
|
||||||
|
|
||||||
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
|
export const runCLI = (args: string, options = {}): Promise<ExecResponse> => {
|
||||||
{
|
const CLI_PATH = resolve(__dirname, "../../bin/hopp.js");
|
||||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
const command = `node ${CLI_PATH} ${args}`;
|
||||||
const command = `node ${CLI_PATH} ${args}`
|
|
||||||
|
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
exec(command, options, (error, stdout, stderr) =>
|
||||||
);
|
resolve({ error, stdout, stderr })
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const trimAnsi = (target: string) => {
|
export const trimAnsi = (target: string) => {
|
||||||
const ansiRegex =
|
const ansiRegex =
|
||||||
@@ -25,12 +26,18 @@ export const getErrorCode = (out: string) => {
|
|||||||
return ansiTrimmedStr.split(" ")[0];
|
return ansiTrimmedStr.split(" ")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
export const getTestJsonFilePath = (
|
||||||
|
file: string,
|
||||||
|
kind: "collection" | "environment"
|
||||||
|
) => {
|
||||||
const kindDir = {
|
const kindDir = {
|
||||||
collection: "collections",
|
collection: "collections",
|
||||||
environment: "environments",
|
environment: "environments",
|
||||||
}[kind];
|
}[kind];
|
||||||
|
|
||||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
const filePath = resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../src/__tests__/samples/${kindDir}/${file}`
|
||||||
|
);
|
||||||
return filePath;
|
return filePath;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import * as E from "fp-ts/Either";
|
import * as E from "fp-ts/Either";
|
||||||
|
|
||||||
import { version } from "../package.json";
|
import { version } from "../package.json";
|
||||||
import { test } from "./commands/test";
|
import { test } from "./commands/test";
|
||||||
import { handleError } from "./handlers/error";
|
import { handleError } from "./handlers/error";
|
||||||
@@ -20,7 +21,7 @@ const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
|||||||
"https://docs.hoppscotch.io/documentation/clients/cli"
|
"https://docs.hoppscotch.io/documentation/clients/cli"
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command();
|
||||||
|
|
||||||
program
|
program
|
||||||
.name("hopp")
|
.name("hopp")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { error } from "../../types/errors";
|
|||||||
import {
|
import {
|
||||||
HoppEnvKeyPairObject,
|
HoppEnvKeyPairObject,
|
||||||
HoppEnvPair,
|
HoppEnvPair,
|
||||||
HoppEnvs
|
HoppEnvs,
|
||||||
} from "../../types/request";
|
} from "../../types/request";
|
||||||
import { readJsonFile } from "../../utils/mutators";
|
import { readJsonFile } from "../../utils/mutators";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ import { readJsonFile } from "../../utils/mutators";
|
|||||||
*/
|
*/
|
||||||
export async function parseEnvsData(path: string) {
|
export async function parseEnvsData(path: string) {
|
||||||
const contents = await readJsonFile(path);
|
const contents = await readJsonFile(path);
|
||||||
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
|
||||||
|
|
||||||
// The legacy key-value pair format that is still supported
|
// The legacy key-value pair format that is still supported
|
||||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||||
@@ -26,7 +26,9 @@ export async function parseEnvsData(path: string) {
|
|||||||
const HoppEnvExportObjectResult = Environment.safeParse(contents);
|
const HoppEnvExportObjectResult = Environment.safeParse(contents);
|
||||||
|
|
||||||
// Shape of the bulk environment export object that is exported from the app
|
// Shape of the bulk environment export object that is exported from the app
|
||||||
const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
|
const HoppBulkEnvExportObjectResult = z
|
||||||
|
.array(entityReference(Environment))
|
||||||
|
.safeParse(contents);
|
||||||
|
|
||||||
// CLI doesnt support bulk environments export
|
// CLI doesnt support bulk environments export
|
||||||
// Hence we check for this case and throw an error if it matches the format
|
// Hence we check for this case and throw an error if it matches the format
|
||||||
@@ -36,13 +38,16 @@ export async function parseEnvsData(path: string) {
|
|||||||
|
|
||||||
// Checks if the environment file is of the correct format
|
// Checks if the environment file is of the correct format
|
||||||
// If it doesnt match either of them, we throw an error
|
// If it doesnt match either of them, we throw an error
|
||||||
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
if (
|
||||||
|
!HoppEnvKeyPairResult.success &&
|
||||||
|
HoppEnvExportObjectResult.type === "err"
|
||||||
|
) {
|
||||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HoppEnvKeyPairResult.success) {
|
if (HoppEnvKeyPairResult.success) {
|
||||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||||
envPairs.push({ key, value });
|
envPairs.push({ key, value, secret: false });
|
||||||
}
|
}
|
||||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
|
||||||
import * as A from "fp-ts/Array";
|
|
||||||
import { CommanderError } from "commander";
|
import { CommanderError } from "commander";
|
||||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||||
|
|
||||||
@@ -14,48 +12,6 @@ export const hasProperty = <P extends PropertyKey>(
|
|||||||
prop: P
|
prop: P
|
||||||
): target is Record<P, unknown> => prop in target;
|
): target is Record<P, unknown> => prop in target;
|
||||||
|
|
||||||
/**
|
|
||||||
* Typeguard to check valid Hoppscotch REST Collection.
|
|
||||||
* @param param The object to be checked.
|
|
||||||
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
|
|
||||||
* False, otherwise.
|
|
||||||
*/
|
|
||||||
export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
|
||||||
if (!!param && typeof param === "object") {
|
|
||||||
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!hasProperty(param, "name") || typeof param.name !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (hasProperty(param, "id") && typeof param.id !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!hasProperty(param, "requests") || !Array.isArray(param.requests)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// Checks each requests array to be valid HoppRESTRequest.
|
|
||||||
const checkRequests = A.every(isHoppRESTRequest)(param.requests);
|
|
||||||
if (!checkRequests) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasProperty(param, "folders") || !Array.isArray(param.folders)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// Checks each folder to be valid REST collection.
|
|
||||||
const checkFolders = A.every(isRESTCollection)(param.folders);
|
|
||||||
if (!checkFolders) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if given error data is of type HoppCLIError, based on existence
|
* Checks if given error data is of type HoppCLIError, based on existence
|
||||||
* of code property.
|
* of code property.
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
|
|||||||
* path of each request within collection-json file, failed-tests-report, errors,
|
* path of each request within collection-json file, failed-tests-report, errors,
|
||||||
* total execution duration for requests, pre-request-scripts, test-scripts.
|
* total execution duration for requests, pre-request-scripts, test-scripts.
|
||||||
* @returns True, if collection runner executed without any errors or failed test-cases.
|
* @returns True, if collection runner executed without any errors or failed test-cases.
|
||||||
* False, if errors occured or test-cases failed.
|
* False, if errors occurred or test-cases failed.
|
||||||
*/
|
*/
|
||||||
export const collectionsRunnerResult = (
|
export const collectionsRunnerResult = (
|
||||||
requestsReport: RequestReport[]
|
requestsReport: RequestReport[]
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const printTestsMetrics = (testsMetrics: TestMetrics) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints details of each reported error for a request with error code.
|
* Prints details of each reported error for a request with error code.
|
||||||
* @param path Request's path in collection for which errors occured.
|
* @param path Request's path in collection for which errors occurred.
|
||||||
* @param errorsReport List of errors reported.
|
* @param errorsReport List of errors reported.
|
||||||
*/
|
*/
|
||||||
export const printErrorsReport = (
|
export const printErrorsReport = (
|
||||||
|
|||||||
@@ -1,8 +1,46 @@
|
|||||||
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { FormDataEntry } from "../types/request";
|
import { entityReference } from "verzod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { error } from "../types/errors";
|
import { error } from "../types/errors";
|
||||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
import { FormDataEntry } from "../types/request";
|
||||||
import { HoppCollection } from "@hoppscotch/data";
|
import { isHoppErrnoException } from "./checks";
|
||||||
|
|
||||||
|
const getValidRequests = (
|
||||||
|
collections: HoppCollection[],
|
||||||
|
collectionFilePath: string
|
||||||
|
) => {
|
||||||
|
return collections.map((collection) => {
|
||||||
|
// Validate requests using zod schema
|
||||||
|
const requestSchemaParsedResult = z
|
||||||
|
.array(entityReference(HoppRESTRequest))
|
||||||
|
.safeParse(collection.requests);
|
||||||
|
|
||||||
|
// Handle validation errors
|
||||||
|
if (!requestSchemaParsedResult.success) {
|
||||||
|
throw error({
|
||||||
|
code: "MALFORMED_COLLECTION",
|
||||||
|
path: collectionFilePath,
|
||||||
|
data: "Please check the collection data.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively validate requests in nested folders
|
||||||
|
if (collection.folders.length > 0) {
|
||||||
|
collection.folders = getValidRequests(
|
||||||
|
collection.folders,
|
||||||
|
collectionFilePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return validated collection
|
||||||
|
return {
|
||||||
|
...collection,
|
||||||
|
requests: requestSchemaParsedResult.data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses array of FormDataEntry to FormData.
|
* Parses array of FormDataEntry to FormData.
|
||||||
@@ -67,7 +105,11 @@ export async function parseCollectionData(
|
|||||||
? contents
|
? contents
|
||||||
: [contents];
|
: [contents];
|
||||||
|
|
||||||
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
const collectionSchemaParsedResult = z
|
||||||
|
.array(entityReference(HoppCollection))
|
||||||
|
.safeParse(maybeArrayOfCollections);
|
||||||
|
|
||||||
|
if (!collectionSchemaParsedResult.success) {
|
||||||
throw error({
|
throw error({
|
||||||
code: "MALFORMED_COLLECTION",
|
code: "MALFORMED_COLLECTION",
|
||||||
path,
|
path,
|
||||||
@@ -75,5 +117,5 @@ export async function parseCollectionData(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return maybeArrayOfCollections as HoppCollection[];
|
return getValidRequests(collectionSchemaParsedResult.data, path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,27 +109,40 @@ export function getEffectiveRESTRequest(
|
|||||||
key: "Authorization",
|
key: "Authorization",
|
||||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (request.auth.authType === "bearer") {
|
||||||
request.auth.authType === "bearer" ||
|
|
||||||
request.auth.authType === "oauth-2"
|
|
||||||
) {
|
|
||||||
effectiveFinalHeaders.push({
|
effectiveFinalHeaders.push({
|
||||||
active: true,
|
active: true,
|
||||||
key: "Authorization",
|
key: "Authorization",
|
||||||
value: `Bearer ${parseTemplateString(
|
value: `Bearer ${parseTemplateString(request.auth.token, envVariables)}`,
|
||||||
request.auth.token,
|
|
||||||
envVariables
|
|
||||||
)}`,
|
|
||||||
});
|
});
|
||||||
|
} else if (request.auth.authType === "oauth-2") {
|
||||||
|
const { addTo } = request.auth;
|
||||||
|
|
||||||
|
if (addTo === "HEADERS") {
|
||||||
|
effectiveFinalHeaders.push({
|
||||||
|
active: true,
|
||||||
|
key: "Authorization",
|
||||||
|
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, envVariables)}`,
|
||||||
|
});
|
||||||
|
} else if (addTo === "QUERY_PARAMS") {
|
||||||
|
effectiveFinalParams.push({
|
||||||
|
active: true,
|
||||||
|
key: "access_token",
|
||||||
|
value: parseTemplateString(
|
||||||
|
request.auth.grantTypeInfo.token,
|
||||||
|
envVariables
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (request.auth.authType === "api-key") {
|
} else if (request.auth.authType === "api-key") {
|
||||||
const { key, value, addTo } = request.auth;
|
const { key, value, addTo } = request.auth;
|
||||||
if (addTo === "Headers") {
|
if (addTo === "HEADERS") {
|
||||||
effectiveFinalHeaders.push({
|
effectiveFinalHeaders.push({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(key, envVariables),
|
key: parseTemplateString(key, envVariables),
|
||||||
value: parseTemplateString(value, envVariables),
|
value: parseTemplateString(value, envVariables),
|
||||||
});
|
});
|
||||||
} else if (addTo === "Query params") {
|
} else if (addTo === "QUERY_PARAMS") {
|
||||||
effectiveFinalParams.push({
|
effectiveFinalParams.push({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(key, envVariables),
|
key: parseTemplateString(key, envVariables),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user