Compare commits
101 Commits
feat/admin
...
2024.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
55a94bdccc | ||
|
|
faab1d20fd | ||
|
|
bd406616ec | ||
|
|
6827e97ec5 | ||
|
|
10d2048975 | ||
|
|
291f18591e | ||
|
|
342532c9b1 | ||
|
|
cf039c482a | ||
|
|
ded2725116 | ||
|
|
4bd54b12cd | ||
|
|
ed6e9b6954 | ||
|
|
dfdd44b4ed | ||
|
|
fc34871dae | ||
|
|
45b532747e | ||
|
|
de4635df23 | ||
|
|
41bad1f3dc | ||
|
|
ecca3d2032 | ||
|
|
47226be6d0 | ||
|
|
6a0e73fdec | ||
|
|
672ee69b2c | ||
|
|
c0fae79678 | ||
|
|
5bcc38e36b | ||
|
|
00862eb192 | ||
|
|
16803acb26 | ||
|
|
3911c9cd1f | ||
|
|
0028f6e878 | ||
|
|
0ba33ec187 | ||
|
|
d7cdeb796a |
15
.github/workflows/tests.yml
vendored
15
.github/workflows/tests.yml
vendored
@@ -17,22 +17,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
run_install: true
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Run tests
|
||||
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
|
||||
|
||||
@@ -118,7 +118,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||
# - DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||
- PORT=3000
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
|
||||
@@ -9,6 +9,10 @@ curlCheck() {
|
||||
fi
|
||||
}
|
||||
|
||||
curlCheck "http://localhost:3000"
|
||||
curlCheck "http://localhost:3100"
|
||||
curlCheck "http://localhost:3170/ping"
|
||||
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
|
||||
curlCheck "http://localhost:80/backend/ping"
|
||||
else
|
||||
curlCheck "http://localhost:3000"
|
||||
curlCheck "http://localhost:3100"
|
||||
curlCheck "http://localhost:3170/ping"
|
||||
fi
|
||||
|
||||
14
package.json
14
package.json
@@ -23,13 +23,13 @@
|
||||
"./packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@commitlint/cli": "16.3.0",
|
||||
"@commitlint/config-conventional": "16.2.4",
|
||||
"@hoppscotch/ui": "0.1.0",
|
||||
"@types/node": "17.0.27",
|
||||
"cross-env": "^7.0.3",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^7.0.4",
|
||||
"cross-env": "7.0.3",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
"lint-staged": "12.4.0"
|
||||
},
|
||||
"pnpm": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"vue": "3.3.9"
|
||||
},
|
||||
"packageExtensions": {
|
||||
"httpsnippet@^3.0.1": {
|
||||
"httpsnippet@3.0.1": {
|
||||
"peerDependencies": {
|
||||
"ajv": "6.12.3"
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.9.3",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lezer/lr": "^1.3.14"
|
||||
"@lezer/lr": "1.3.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.1",
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^3.29.3",
|
||||
"rollup-plugin-dts": "^6.0.2",
|
||||
"rollup-plugin-ts": "^3.4.5",
|
||||
"typescript": "^5.2.2"
|
||||
"@lezer/generator": "1.5.1",
|
||||
"mocha": "9.2.2",
|
||||
"rollup": "3.29.4",
|
||||
"rollup-plugin-dts": "6.0.2",
|
||||
"rollup-plugin-ts": "3.4.5",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"assets": [
|
||||
"**/*.hbs"
|
||||
],
|
||||
"assets": [{ "include": "mailer/templates/**/*", "outDir": "dist" }],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.12.3",
|
||||
"version": "2024.3.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -24,80 +24,83 @@
|
||||
"do-test": "pnpm run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.9.4",
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/apollo": "^12.0.9",
|
||||
"@nestjs/common": "^10.2.6",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.6",
|
||||
"@nestjs/graphql": "^12.0.9",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.2.6",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"argon2": "^0.30.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.3",
|
||||
"fp-ts": "^2.13.1",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-query-complexity": "^0.12.0",
|
||||
"graphql-redis-subscriptions": "^2.6.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"io-ts": "^2.2.16",
|
||||
"luxon": "^3.2.1",
|
||||
"nodemailer": "^6.9.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-microsoft": "^1.0.0",
|
||||
"prisma": "^5.8.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.6.0"
|
||||
"@apollo/server": "4.9.5",
|
||||
"@nestjs-modules/mailer": "1.9.1",
|
||||
"@nestjs/apollo": "12.0.9",
|
||||
"@nestjs/common": "10.2.7",
|
||||
"@nestjs/config": "3.1.1",
|
||||
"@nestjs/core": "10.2.7",
|
||||
"@nestjs/graphql": "12.0.9",
|
||||
"@nestjs/jwt": "10.1.1",
|
||||
"@nestjs/passport": "10.0.2",
|
||||
"@nestjs/platform-express": "10.2.7",
|
||||
"@nestjs/schedule": "4.0.1",
|
||||
"@nestjs/throttler": "5.0.1",
|
||||
"@prisma/client": "5.8.1",
|
||||
"argon2": "0.30.3",
|
||||
"bcrypt": "5.1.0",
|
||||
"cookie": "0.5.0",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cron": "3.1.6",
|
||||
"express": "4.18.2",
|
||||
"express-session": "1.17.3",
|
||||
"fp-ts": "2.13.1",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-query-complexity": "0.12.0",
|
||||
"graphql-redis-subscriptions": "2.6.0",
|
||||
"graphql-subscriptions": "2.0.0",
|
||||
"handlebars": "4.7.7",
|
||||
"io-ts": "2.2.16",
|
||||
"luxon": "3.2.1",
|
||||
"nodemailer": "6.9.1",
|
||||
"passport": "0.6.0",
|
||||
"passport-github2": "0.1.12",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.1",
|
||||
"passport-local": "1.0.0",
|
||||
"passport-microsoft": "1.0.0",
|
||||
"posthog-node": "3.6.3",
|
||||
"prisma": "5.8.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rimraf": "3.0.2",
|
||||
"rxjs": "7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.6",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@types/argon2": "^0.15.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"@types/node": "^18.11.10",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/passport-github2": "^1.2.5",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-microsoft": "^0.0.0",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.4.1",
|
||||
"jest-mock-extended": "^3.0.1",
|
||||
"@nestjs/cli": "10.2.1",
|
||||
"@nestjs/schematics": "10.0.3",
|
||||
"@nestjs/testing": "10.2.7",
|
||||
"@relmify/jest-fp-ts": "2.0.2",
|
||||
"@types/argon2": "0.15.0",
|
||||
"@types/bcrypt": "5.0.0",
|
||||
"@types/cookie": "0.5.1",
|
||||
"@types/cookie-parser": "1.4.3",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/luxon": "3.2.0",
|
||||
"@types/node": "18.11.10",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/passport-github2": "1.2.5",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
"@types/passport-jwt": "3.0.8",
|
||||
"@types/passport-microsoft": "0.0.0",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "5.45.0",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"jest": "29.4.1",
|
||||
"jest-mock-extended": "3.0.1",
|
||||
"jwt": "link:@types/nestjs/jwt",
|
||||
"prettier": "^2.8.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.2",
|
||||
"prettier": "2.8.4",
|
||||
"source-map-support": "0.5.21",
|
||||
"supertest": "6.3.2",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "4.1.1",
|
||||
"typescript": "^4.9.3"
|
||||
"typescript": "4.9.3"
|
||||
},
|
||||
"jest": {
|
||||
"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);
|
||||
|
||||
@@ -41,31 +41,31 @@ model TeamInvitation {
|
||||
}
|
||||
|
||||
model TeamCollection {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
parentID String?
|
||||
data Json?
|
||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||
requests TeamRequest[]
|
||||
teamID String
|
||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||
title String
|
||||
orderIndex Int
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
model TeamRequest {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
collectionID String
|
||||
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
||||
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
||||
teamID String
|
||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||
title String
|
||||
request Json
|
||||
orderIndex Int
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
model Shortcode {
|
||||
|
||||
@@ -27,9 +27,7 @@ import {
|
||||
} from './input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Admin)
|
||||
@@ -49,203 +47,6 @@ export class AdminResolver {
|
||||
return admin;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all admin users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async admins() {
|
||||
const admins = await this.adminService.fetchAdmins();
|
||||
return admins;
|
||||
}
|
||||
@ResolveField(() => User, {
|
||||
description: 'Returns a user info by UID',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async userInfo(
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
type: () => ID,
|
||||
description: 'The user UID',
|
||||
})
|
||||
userUid: string,
|
||||
): Promise<AuthUser> {
|
||||
const user = await this.adminService.fetchUserInfo(userUid);
|
||||
if (E.isLeft(user)) throwErr(user.left);
|
||||
return user.right;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<AuthUser[]> {
|
||||
const users = await this.adminService.fetchUsers(args.cursor, args.take);
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [Team], {
|
||||
description: 'Returns a list of all the teams in the infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async allTeams(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<Team[]> {
|
||||
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
|
||||
return teams;
|
||||
}
|
||||
@ResolveField(() => Team, {
|
||||
description: 'Returns a team info by ID when requested by Admin',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamInfo(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which info to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<Team> {
|
||||
const team = await this.adminService.getTeamInfo(teamID);
|
||||
if (E.isLeft(team)) throwErr(team.left);
|
||||
return team.right;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the members in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async membersCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
nullable: false,
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
|
||||
return teamMembersCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored collections in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async collectionCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
|
||||
return teamCollCount;
|
||||
}
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored requests in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async requestCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
|
||||
return teamReqCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored environments in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async environmentCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const envsCount = await this.adminService.environmentCountInTeam(teamID);
|
||||
return envsCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => [TeamInvitation], {
|
||||
description: 'Return all the pending invitations in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async pendingInvitationCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
const invitations = await this.adminService.pendingInvitationCountInTeam(
|
||||
teamID,
|
||||
);
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Users in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async usersCount() {
|
||||
return this.adminService.getUsersCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Teams in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamsCount() {
|
||||
return this.adminService.getTeamsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Collections in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamCollectionsCount() {
|
||||
return this.adminService.getTeamCollectionsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Requests in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamRequestsCount() {
|
||||
return this.adminService.getTeamRequestsCount();
|
||||
}
|
||||
|
||||
/* Mutations */
|
||||
|
||||
@Mutation(() => InvitedUser, {
|
||||
@@ -269,8 +70,26 @@ export class AdminResolver {
|
||||
return invitedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Revoke a user invites by invitee emails',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async revokeUserInvitationsByAdmin(
|
||||
@Args({
|
||||
name: 'inviteeEmails',
|
||||
description: 'Invitee Emails',
|
||||
type: () => [String],
|
||||
})
|
||||
inviteeEmails: string[],
|
||||
): Promise<boolean> {
|
||||
const invite = await this.adminService.revokeUserInvitations(inviteeEmails);
|
||||
if (E.isLeft(invite)) throwErr(invite.left);
|
||||
return invite.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete an user account from infra',
|
||||
deprecationReason: 'Use removeUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserByAdmin(
|
||||
@@ -281,12 +100,33 @@ export class AdminResolver {
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||
return invitedUser.right;
|
||||
const removedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
||||
return removedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => [UserDeletionResult], {
|
||||
description: 'Delete user accounts from infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUsersByAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<UserDeletionResult[]> {
|
||||
const deletionResults = await this.adminService.removeUserAccounts(
|
||||
userUIDs,
|
||||
);
|
||||
if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
|
||||
return deletionResults.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make user an admin',
|
||||
deprecationReason: 'Use makeUsersAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUserAdmin(
|
||||
@@ -302,8 +142,51 @@ export class AdminResolver {
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make users an admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUsersAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.makeUsersAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Update user display name',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async updateUserDisplayNameByAdmin(
|
||||
@Args({
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUID: string,
|
||||
@Args({
|
||||
name: 'displayName',
|
||||
description: 'users display name',
|
||||
})
|
||||
displayName: string,
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.updateUserDisplayName(
|
||||
userUID,
|
||||
displayName,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove user as admin',
|
||||
deprecationReason: 'Use demoteUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserAsAdmin(
|
||||
@@ -319,6 +202,23 @@ export class AdminResolver {
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove users as admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async demoteUsersByAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.demoteUsersByAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Team, {
|
||||
description:
|
||||
'Create a new team by providing the user uid to nominate as Team owner',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AdminService } from './admin.service';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { InvitedUsers } from '@prisma/client';
|
||||
import { InvitedUsers, User as DbUser } from '@prisma/client';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { TeamService } from '../team/team.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
@@ -13,10 +13,15 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import * as E from 'fp-ts/Either';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -58,20 +63,87 @@ const invitedUsers: InvitedUsers[] = [
|
||||
invitedOn: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const dbAdminUsers: DbUser[] = [
|
||||
{
|
||||
uid: 'uid 1',
|
||||
displayName: 'displayName',
|
||||
email: 'email@email.com',
|
||||
photoURL: 'photoURL',
|
||||
isAdmin: true,
|
||||
refreshToken: 'refreshToken',
|
||||
currentRESTSession: '',
|
||||
currentGQLSession: '',
|
||||
createdOn: new Date(),
|
||||
},
|
||||
{
|
||||
uid: 'uid 2',
|
||||
displayName: 'displayName',
|
||||
email: 'email@email.com',
|
||||
photoURL: 'photoURL',
|
||||
isAdmin: true,
|
||||
refreshToken: 'refreshToken',
|
||||
currentRESTSession: '',
|
||||
currentGQLSession: '',
|
||||
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('fetchInvitedUsers', () => {
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
test('should resolve right and apply pagination correctly', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||
// @ts-ignore
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 5, skip: 2 };
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
|
||||
expect(mockPrisma.invitedUsers.findMany).toHaveBeenCalledWith({
|
||||
...paginationArgs,
|
||||
orderBy: {
|
||||
invitedOn: 'desc',
|
||||
},
|
||||
where: {
|
||||
NOT: {
|
||||
inviteeEmail: {
|
||||
in: [dbAdminUsers[0].email],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||
// @ts-ignore
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
expect(results).toEqual(invitedUsers);
|
||||
});
|
||||
test('should resolve left and return an empty array if invited users not found', async () => {
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
||||
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -134,6 +206,58 @@ describe('AdminService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeUserInvitations', () => {
|
||||
test('should resolve left and return error if email not invited', async () => {
|
||||
mockPrisma.invitedUsers.deleteMany.mockRejectedValueOnce(
|
||||
'RecordNotFound',
|
||||
);
|
||||
|
||||
const result = await adminService.revokeUserInvitations([
|
||||
'test@gmail.com',
|
||||
]);
|
||||
|
||||
expect(result).toEqualLeft(USER_INVITATION_DELETION_FAILED);
|
||||
});
|
||||
|
||||
test('should resolve right and return deleted invitee email', async () => {
|
||||
const adminUid = 'adminUid';
|
||||
mockPrisma.invitedUsers.deleteMany.mockResolvedValueOnce({ count: 1 });
|
||||
|
||||
const result = await adminService.revokeUserInvitations([
|
||||
invitedUsers[0].inviteeEmail,
|
||||
]);
|
||||
|
||||
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
|
||||
},
|
||||
});
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUsersAsAdmin', () => {
|
||||
test('should resolve right and make admins to users', async () => {
|
||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(
|
||||
await adminService.demoteUsersByAdmin([dbAdminUsers[0].uid]),
|
||||
).toEqualRight(true);
|
||||
});
|
||||
|
||||
test('should resolve left and return error if only one admin in the infra', async () => {
|
||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(
|
||||
await adminService.demoteUsersByAdmin(
|
||||
dbAdminUsers.map((user) => user.uid),
|
||||
),
|
||||
).toEqualLeft(ONLY_ONE_ADMIN_ACCOUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsersCount', () => {
|
||||
test('should return count of all users in the organization', async () => {
|
||||
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
||||
|
||||
@@ -6,13 +6,16 @@ import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { validateEmail } from '../utils';
|
||||
import {
|
||||
ADMIN_CAN_NOT_BE_DELETED,
|
||||
DUPLICATE_EMAIL,
|
||||
EMAIL_FAILED,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
USERS_NOT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_IS_ADMIN,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
@@ -26,6 +29,8 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -48,12 +53,30 @@ export class AdminService {
|
||||
* @param cursorID Users uid
|
||||
* @param take number of users to fetch
|
||||
* @returns an Either of array of user or error
|
||||
* @deprecated use fetchUsersV2 instead
|
||||
*/
|
||||
async fetchUsers(cursorID: string, take: number) {
|
||||
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the infra.
|
||||
* @param searchString search on users displayName or email
|
||||
* @param paginationOption pagination options
|
||||
* @returns an Either of array of user or error
|
||||
*/
|
||||
async fetchUsersV2(
|
||||
searchString: string,
|
||||
paginationOption: OffsetPaginationArgs,
|
||||
) {
|
||||
const allUsers = await this.userService.fetchAllUsersV2(
|
||||
searchString,
|
||||
paginationOption,
|
||||
);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to join the infra.
|
||||
* @param adminUID Admin's UID
|
||||
@@ -110,14 +133,68 @@ export class AdminService {
|
||||
return E.right(invitedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the display name of a user
|
||||
* @param userUid Who's display name is being updated
|
||||
* @param displayName New display name of the user
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async updateUserDisplayName(userUid: string, displayName: string) {
|
||||
const updatedUser = await this.userService.updateUserDisplayName(
|
||||
userUid,
|
||||
displayName,
|
||||
);
|
||||
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke infra level user invitations
|
||||
* @param inviteeEmails Invitee's emails
|
||||
* @param adminUid Admin Uid
|
||||
* @returns an Either of boolean or error string
|
||||
*/
|
||||
async revokeUserInvitations(inviteeEmails: string[]) {
|
||||
try {
|
||||
await this.prisma.invitedUsers.deleteMany({
|
||||
where: {
|
||||
inviteeEmail: { in: inviteeEmails },
|
||||
},
|
||||
});
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_INVITATION_DELETION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of invited users by the admin.
|
||||
* @returns an Either of array of `InvitedUser` object or error
|
||||
*/
|
||||
async fetchInvitedUsers() {
|
||||
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
||||
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
||||
const userEmailObjs = await this.prisma.user.findMany({
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const users: InvitedUser[] = invitedUsers.map(
|
||||
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
|
||||
take: paginationOption.take,
|
||||
skip: paginationOption.skip,
|
||||
orderBy: {
|
||||
invitedOn: 'desc',
|
||||
},
|
||||
where: {
|
||||
NOT: {
|
||||
inviteeEmail: {
|
||||
in: userEmailObjs.map((user) => user.email),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const users: InvitedUser[] = pendingInvitedUsers.map(
|
||||
(user) => <InvitedUser>{ ...user },
|
||||
);
|
||||
|
||||
@@ -337,6 +414,7 @@ export class AdminService {
|
||||
* Remove a user account by UID
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use removeUserAccounts instead
|
||||
*/
|
||||
async removeUserAccount(userUid: string) {
|
||||
const user = await this.userService.findUserById(userUid);
|
||||
@@ -349,10 +427,73 @@ export class AdminService {
|
||||
return E.right(delUser.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user (not Admin) accounts by UIDs
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserAccounts(userUIDs: string[]) {
|
||||
const userDeleteResult: UserDeletionResult[] = [];
|
||||
|
||||
// step 1: fetch all users
|
||||
const allUsersList = await this.userService.findUsersByIds(userUIDs);
|
||||
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
|
||||
|
||||
// step 2: admin user can not be deleted without removing admin status/role
|
||||
allUsersList.forEach((user) => {
|
||||
if (user.isAdmin) {
|
||||
userDeleteResult.push({
|
||||
userUID: user.uid,
|
||||
isDeleted: false,
|
||||
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
|
||||
let deletedUserEmails: string[] = [];
|
||||
|
||||
// step 3: delete non-admin users
|
||||
const deletionPromises = nonAdminUsers.map((user) => {
|
||||
return this.userService
|
||||
.deleteUserByUID(user)()
|
||||
.then((res) => {
|
||||
if (E.isLeft(res)) {
|
||||
return {
|
||||
userUID: user.uid,
|
||||
isDeleted: false,
|
||||
errorMessage: res.left,
|
||||
} as UserDeletionResult;
|
||||
}
|
||||
|
||||
deletedUserEmails.push(user.email);
|
||||
return {
|
||||
userUID: user.uid,
|
||||
isDeleted: true,
|
||||
errorMessage: null,
|
||||
} as UserDeletionResult;
|
||||
});
|
||||
});
|
||||
const promiseResult = await Promise.allSettled(deletionPromises);
|
||||
|
||||
// step 4: revoke all the invites sent to the deleted users
|
||||
await this.revokeUserInvitations(deletedUserEmails);
|
||||
|
||||
// step 5: return the result
|
||||
promiseResult.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
userDeleteResult.push(result.value);
|
||||
}
|
||||
});
|
||||
|
||||
return E.right(userDeleteResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a user an admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use makeUsersAdmin instead
|
||||
*/
|
||||
async makeUserAdmin(userUID: string) {
|
||||
const admin = await this.userService.makeAdmin(userUID);
|
||||
@@ -360,10 +501,22 @@ export class AdminService {
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make users to admin
|
||||
* @param userUid User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async makeUsersAdmin(userUIDs: string[]) {
|
||||
const isUpdated = await this.userService.makeAdmins(userUIDs);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user as admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use demoteUsersByAdmin instead
|
||||
*/
|
||||
async removeUserAsAdmin(userUID: string) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
@@ -374,6 +527,26 @@ export class AdminService {
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove users as admin
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async demoteUsersByAdmin(userUIDs: string[]) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
|
||||
const remainingAdmins = adminUsers.filter(
|
||||
(adminUser) => !userUIDs.includes(adminUser.uid),
|
||||
);
|
||||
if (remainingAdmins.length < 1) {
|
||||
return E.left(ONLY_ONE_ADMIN_ACCOUNT);
|
||||
}
|
||||
|
||||
const isUpdated = await this.userService.removeUsersAsAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(isUpdated.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Users in org
|
||||
* @returns number of users in the org
|
||||
|
||||
@@ -17,7 +17,10 @@ import { AuthUser } from 'src/types/AuthUser';
|
||||
import { throwErr } from 'src/utils';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { Admin } from './admin.model';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import {
|
||||
OffsetPaginationArgs,
|
||||
PaginationArgs,
|
||||
} from 'src/types/input-types.args';
|
||||
import { InvitedUser } from './invited-user.model';
|
||||
import { Team } from 'src/team/team.model';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
@@ -29,7 +32,8 @@ import {
|
||||
EnableAndDisableSSOArgs,
|
||||
InfraConfigArgs,
|
||||
} 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';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Infra)
|
||||
@@ -76,6 +80,7 @@ export class InfraResolver {
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use allUsersV2 instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
||||
@@ -83,11 +88,33 @@ export class InfraResolver {
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsersV2(
|
||||
@Args({
|
||||
name: 'searchString',
|
||||
nullable: true,
|
||||
description: 'Search on users displayName or email',
|
||||
})
|
||||
searchString: string,
|
||||
@Args() paginationOption: OffsetPaginationArgs,
|
||||
): Promise<AuthUser[]> {
|
||||
const users = await this.adminService.fetchUsersV2(
|
||||
searchString,
|
||||
paginationOption,
|
||||
);
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
})
|
||||
async invitedUsers(): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
async invitedUsers(
|
||||
@Args() args: OffsetPaginationArgs,
|
||||
): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers(args);
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -247,10 +274,10 @@ export class InfraResolver {
|
||||
async infraConfigs(
|
||||
@Args({
|
||||
name: 'configNames',
|
||||
type: () => [InfraConfigEnumForClient],
|
||||
type: () => [InfraConfigEnum],
|
||||
description: 'Configs to fetch',
|
||||
})
|
||||
names: InfraConfigEnumForClient[],
|
||||
names: InfraConfigEnum[],
|
||||
) {
|
||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||
@@ -284,6 +311,25 @@ export class InfraResolver {
|
||||
return updatedRes.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Enable or disable analytics collection',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async toggleAnalyticsCollection(
|
||||
@Args({
|
||||
name: 'status',
|
||||
type: () => ServiceStatus,
|
||||
description: 'Toggle analytics collection',
|
||||
})
|
||||
analyticsCollectionStatus: ServiceStatus,
|
||||
) {
|
||||
const res = await this.infraConfigService.toggleAnalyticsCollection(
|
||||
analyticsCollectionStatus,
|
||||
);
|
||||
if (E.isLeft(res)) throwErr(res.left);
|
||||
return res.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Reset Infra Configs with default values (.env)',
|
||||
})
|
||||
@@ -306,7 +352,9 @@ export class InfraResolver {
|
||||
})
|
||||
providerInfo: EnableAndDisableSSOArgs[],
|
||||
) {
|
||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
|
||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
|
||||
providerInfo,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||
import { loadInfraConfiguration } from './infra-config/helper';
|
||||
import { MailerModule } from './mailer/mailer.module';
|
||||
import { PosthogModule } from './posthog/posthog.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
PosthogModule,
|
||||
ScheduleModule.forRoot(),
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -18,12 +18,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||
import {
|
||||
AuthProvider,
|
||||
authCookieHandler,
|
||||
authProviderCheck,
|
||||
throwHTTPErr,
|
||||
} from './helper';
|
||||
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
|
||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||
@@ -31,6 +26,7 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'auth', version: '1' })
|
||||
|
||||
@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
|
||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||
import { AuthProvider, authProviderCheck } from './helper';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||
})
|
||||
export class AuthModule {
|
||||
static async register() {
|
||||
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
|
||||
if (!isInfraConfigPopulated) {
|
||||
return { module: AuthModule };
|
||||
}
|
||||
|
||||
const env = await loadInfraConfiguration();
|
||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
RefreshTokenPayload,
|
||||
} from 'src/types/AuthTokens';
|
||||
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 { VerificationToken } from '@prisma/client';
|
||||
import { Origin } from './helper';
|
||||
@@ -117,7 +117,7 @@ export class AuthService {
|
||||
userUid,
|
||||
);
|
||||
if (E.isLeft(updatedUser))
|
||||
return E.left(<AuthError>{
|
||||
return E.left(<RESTError>{
|
||||
message: updatedUser.left,
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
});
|
||||
@@ -255,7 +255,7 @@ export class AuthService {
|
||||
*/
|
||||
async verifyMagicLinkTokens(
|
||||
magicLinkIDTokens: VerifyMagicDto,
|
||||
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
|
||||
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
|
||||
const passwordlessTokens = await this.validatePasswordlessTokens(
|
||||
magicLinkIDTokens,
|
||||
);
|
||||
@@ -373,7 +373,7 @@ export class AuthService {
|
||||
if (usersCount === 1) {
|
||||
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
||||
if (E.isLeft(elevatedUser))
|
||||
return E.left(<AuthError>{
|
||||
return E.left(<RESTError>{
|
||||
message: elevatedUser.left,
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { AuthProvider, authProviderCheck } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
|
||||
@Injectable()
|
||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { AuthProvider, authProviderCheck } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { AuthProvider, authProviderCheck } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftSSOGuard
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AuthError } from 'src/types/AuthError';
|
||||
import { AuthTokens } from 'src/types/AuthTokens';
|
||||
import { Response } from 'express';
|
||||
import * as cookie from 'cookie';
|
||||
@@ -25,15 +24,6 @@ export enum AuthProvider {
|
||||
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
|
||||
* @param res Express Response Object
|
||||
|
||||
@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
||||
scope: [configService.get('GITHUB_SCOPE')],
|
||||
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
|
||||
scope: [configService.get('INFRA.GITHUB_SCOPE')],
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
||||
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
|
||||
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
|
||||
passReqToCallback: true,
|
||||
store: true,
|
||||
});
|
||||
|
||||
@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
||||
tenant: configService.get('MICROSOFT_TENANT'),
|
||||
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
|
||||
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
|
||||
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ export const DUPLICATE_EMAIL = 'email/both_emails_cannot_be_same' as const;
|
||||
export const ONLY_ONE_ADMIN_ACCOUNT =
|
||||
'admin/only_one_admin_account_found' as const;
|
||||
|
||||
/**
|
||||
* Admin user can not be deleted
|
||||
* To delete the admin user, first make the Admin user a normal user
|
||||
* (AdminService)
|
||||
*/
|
||||
export const ADMIN_CAN_NOT_BE_DELETED =
|
||||
'admin/admin_can_not_be_deleted' as const;
|
||||
|
||||
/**
|
||||
* Token Authorization failed (Check 'Authorization' Header)
|
||||
* (GqlAuthGuard)
|
||||
@@ -76,6 +84,12 @@ export const USER_ALREADY_INVITED = 'admin/user_already_invited' 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
|
||||
* (UserService)
|
||||
@@ -99,6 +113,13 @@ export const USER_IS_OWNER = 'user/is_owner' as const;
|
||||
*/
|
||||
export const USER_IS_ADMIN = 'user/is_admin' as const;
|
||||
|
||||
/**
|
||||
* User invite deletion failure error due to invitation not found
|
||||
* (AdminService)
|
||||
*/
|
||||
export const USER_INVITATION_DELETION_FAILED =
|
||||
'user/invitation_deletion_failed' as const;
|
||||
|
||||
/**
|
||||
* Teams not found
|
||||
* (TeamsService)
|
||||
@@ -213,6 +234,12 @@ export const TEAM_COL_NOT_SAME_PARENT =
|
||||
export const TEAM_COL_SAME_NEXT_COLL =
|
||||
'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
|
||||
* (TeamCollectionService)
|
||||
@@ -268,6 +295,13 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||
export const TEAM_COLL_DATA_INVALID =
|
||||
'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
|
||||
* (GqlRequestTeamMemberGuard)
|
||||
@@ -293,6 +327,19 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
|
||||
*/
|
||||
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
|
||||
* (AuthService)
|
||||
@@ -690,9 +737,27 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||
'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
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const DATABASE_TABLE_NOT_EXIST =
|
||||
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
||||
|
||||
/**
|
||||
* PostHog client is not initialized
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||
|
||||
/**
|
||||
* Inputs supplied are invalid
|
||||
*/
|
||||
export const INVALID_PARAMS = 'invalid_parameters' as const;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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 { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export enum ServiceStatus {
|
||||
ENABLE = 'ENABLE',
|
||||
@@ -13,14 +17,21 @@ const AuthProviderConfigurations = {
|
||||
[AuthProvider.GOOGLE]: [
|
||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||
InfraConfigEnum.GOOGLE_SCOPE,
|
||||
],
|
||||
[AuthProvider.GITHUB]: [
|
||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||
InfraConfigEnum.GITHUB_SCOPE,
|
||||
],
|
||||
[AuthProvider.MICROSOFT]: [
|
||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||
InfraConfigEnum.MICROSOFT_TENANT,
|
||||
],
|
||||
[AuthProvider.EMAIL]: [
|
||||
InfraConfigEnum.MAILER_SMTP_URL,
|
||||
@@ -53,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
|
||||
* (Docker will re-start the app)
|
||||
@@ -104,3 +248,12 @@ export function getConfiguredSSOProviders() {
|
||||
|
||||
return configuredAuthProviders.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hashed valued for analytics
|
||||
* @returns Generated hashed value
|
||||
*/
|
||||
export function generateAnalyticsUserId() {
|
||||
const hashedUserID = randomBytes(20).toString('hex');
|
||||
return hashedUserID;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { InfraConfigService } from './infra-config.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
||||
import { throwHTTPErr } from 'src/auth/helper';
|
||||
import { AuthError } from 'src/types/AuthError';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { RESTError } from 'src/types/RESTError';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'site', version: '1' })
|
||||
@@ -17,11 +17,11 @@ export class SiteController {
|
||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||
async fetchSetupInfo() {
|
||||
const status = await this.infraConfigService.get(
|
||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
);
|
||||
|
||||
if (E.isLeft(status))
|
||||
throwHTTPErr(<AuthError>{
|
||||
throwHTTPErr(<RESTError>{
|
||||
message: status.left,
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
});
|
||||
@@ -32,13 +32,13 @@ export class SiteController {
|
||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||
async setSetupAsComplete() {
|
||||
const res = await this.infraConfigService.update(
|
||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
false.toString(),
|
||||
false,
|
||||
);
|
||||
|
||||
if (E.isLeft(res))
|
||||
throwHTTPErr(<AuthError>{
|
||||
throwHTTPErr(<RESTError>{
|
||||
message: res.left,
|
||||
statusCode: HttpStatus.FORBIDDEN,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from './helper';
|
||||
|
||||
@ObjectType()
|
||||
@@ -8,7 +8,7 @@ export class InfraConfig {
|
||||
@Field({
|
||||
description: 'Infra Config Name',
|
||||
})
|
||||
name: InfraConfigEnumForClient;
|
||||
name: InfraConfigEnum;
|
||||
|
||||
@Field({
|
||||
description: 'Infra Config Value',
|
||||
@@ -16,7 +16,7 @@ export class InfraConfig {
|
||||
value: string;
|
||||
}
|
||||
|
||||
registerEnumType(InfraConfigEnumForClient, {
|
||||
registerEnumType(InfraConfigEnum, {
|
||||
name: 'InfraConfigEnum',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfigService } from './infra-config.service';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import {
|
||||
InfraConfigEnum,
|
||||
InfraConfigEnumForClient,
|
||||
} from 'src/types/InfraConfig';
|
||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
||||
INFRA_CONFIG_NOT_FOUND,
|
||||
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||
INFRA_CONFIG_UPDATE_FAILED,
|
||||
} from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as helper from './helper';
|
||||
import { InfraConfig as dbInfraConfig } from '@prisma/client';
|
||||
import { InfraConfig } from './infra-config.model';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
|
||||
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(() => {
|
||||
mockReset(mockPrisma);
|
||||
});
|
||||
|
||||
describe('InfraConfigService', () => {
|
||||
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 () => {
|
||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
|
||||
|
||||
describe('get', () => {
|
||||
it('should get the infra config', async () => {
|
||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
|
||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
|
||||
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,23 +3,30 @@ import { InfraConfig } from './infra-config.model';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import {
|
||||
InfraConfigEnum,
|
||||
InfraConfigEnumForClient,
|
||||
} from 'src/types/InfraConfig';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import {
|
||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
DATABASE_TABLE_NOT_EXIST,
|
||||
INFRA_CONFIG_INVALID_INPUT,
|
||||
INFRA_CONFIG_NOT_FOUND,
|
||||
INFRA_CONFIG_NOT_LISTED,
|
||||
INFRA_CONFIG_RESET_FAILED,
|
||||
INFRA_CONFIG_UPDATE_FAILED,
|
||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||
} from 'src/errors';
|
||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||
import {
|
||||
throwErr,
|
||||
validateSMTPEmail,
|
||||
validateSMTPUrl,
|
||||
validateUrl,
|
||||
} from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
||||
import {
|
||||
ServiceStatus,
|
||||
getDefaultInfraConfigs,
|
||||
getMissingInfraConfigEntries,
|
||||
stopApp,
|
||||
} from './helper';
|
||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@@ -30,82 +37,31 @@ export class InfraConfigService implements OnModuleInit {
|
||||
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() {
|
||||
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.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
|
||||
* @description This function create rows 'infra_config' in very first time (only once)
|
||||
*/
|
||||
async initializeInfraConfigTable() {
|
||||
try {
|
||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
||||
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),
|
||||
);
|
||||
const propsToInsert = await getMissingInfraConfigEntries();
|
||||
|
||||
if (propsToInsert.length > 0) {
|
||||
await this.prisma.infraConfig.createMany({ data: propsToInsert });
|
||||
@@ -156,11 +112,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async update(
|
||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
||||
value: string,
|
||||
restartEnabled = false,
|
||||
) {
|
||||
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
|
||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||
|
||||
@@ -184,6 +136,11 @@ export class InfraConfigService implements OnModuleInit {
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
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);
|
||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||
|
||||
@@ -217,12 +174,26 @@ export class InfraConfigService implements OnModuleInit {
|
||||
) {
|
||||
switch (service) {
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||
@@ -231,6 +202,22 @@ export class InfraConfigService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or Disable Analytics Collection
|
||||
*
|
||||
* @param status Status to enable or disable
|
||||
* @returns Boolean of status of analytics collection
|
||||
*/
|
||||
async toggleAnalyticsCollection(status: ServiceStatus) {
|
||||
const isUpdated = await this.update(
|
||||
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
||||
);
|
||||
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(isUpdated.right.value === 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or Disable SSO for login/signup
|
||||
* @param provider Auth Provider to enable or disable
|
||||
@@ -281,7 +268,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||
* @param name Name of the InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async get(name: InfraConfigEnumForClient) {
|
||||
async get(name: InfraConfigEnum) {
|
||||
try {
|
||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||
where: { name },
|
||||
@@ -296,9 +283,18 @@ export class InfraConfigService implements OnModuleInit {
|
||||
/**
|
||||
* Get InfraConfigs by names
|
||||
* @param names Names of the InfraConfigs
|
||||
* @param checkDisallowedKeys If true, check if the names are allowed to fetch by client
|
||||
* @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 {
|
||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||
where: { name: { in: names } },
|
||||
@@ -324,25 +320,28 @@ export class InfraConfigService implements OnModuleInit {
|
||||
* Reset all the InfraConfigs to their default values (from .env)
|
||||
*/
|
||||
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 {
|
||||
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({
|
||||
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({
|
||||
data: [
|
||||
...updatedInfraConfigDefaultObjs,
|
||||
{
|
||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
data: updatedInfraConfigDefaultObjs,
|
||||
});
|
||||
|
||||
stopApp();
|
||||
@@ -358,36 +357,60 @@ export class InfraConfigService implements OnModuleInit {
|
||||
*/
|
||||
validateEnvValues(
|
||||
infraConfigs: {
|
||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
||||
name: InfraConfigEnum;
|
||||
value: string;
|
||||
}[],
|
||||
) {
|
||||
for (let i = 0; i < infraConfigs.length; i++) {
|
||||
switch (infraConfigs[i].name) {
|
||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
||||
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
||||
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
||||
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
||||
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
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);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
||||
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
||||
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from './helper';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@InputType()
|
||||
export class InfraConfigArgs {
|
||||
@Field(() => InfraConfigEnumForClient, {
|
||||
@Field(() => InfraConfigEnum, {
|
||||
description: 'Infra Config Name',
|
||||
})
|
||||
name: InfraConfigEnumForClient;
|
||||
name: InfraConfigEnum;
|
||||
|
||||
@Field({
|
||||
description: 'Infra Config Value',
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MailerService {
|
||||
): string {
|
||||
switch (mailDesc.template) {
|
||||
case 'team-invitation':
|
||||
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
||||
return `A user has invited you to join a team workspace in Hoppscotch`;
|
||||
|
||||
case 'user-invitation':
|
||||
return 'Sign in to Hoppscotch';
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
@@ -458,7 +464,7 @@
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Hi there,</h1>
|
||||
<p>{{invitee}} with {{invite_team_name}} has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
||||
<p><a class="nohighlight" name="invitee" href="#">{{invitee}}</a> with <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a> has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
||||
<!-- Action -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
@@ -484,7 +490,7 @@
|
||||
Welcome aboard, <br />
|
||||
Your friends at Hoppscotch
|
||||
</p>
|
||||
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, just ignore this email.</p>
|
||||
<p><strong>P.S.</strong> If you don't associate with <a class="nohighlight" name="invitee" href="#">{{invitee}}</a> or <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a>, just ignore this email.</p>
|
||||
<!-- Sub copy -->
|
||||
<table class="body-sub">
|
||||
<tr>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
@@ -22,19 +22,25 @@
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
@@ -47,13 +53,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -61,7 +67,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -69,7 +75,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -77,12 +83,12 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
@@ -91,25 +97,25 @@
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
@@ -124,7 +130,7 @@
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
@@ -132,7 +138,7 @@
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
@@ -140,7 +146,7 @@
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
@@ -148,21 +154,21 @@
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -171,31 +177,31 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -206,33 +212,33 @@
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -241,7 +247,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -250,50 +256,50 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -303,7 +309,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -313,16 +319,16 @@
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
@@ -331,7 +337,7 @@
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -340,7 +346,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -350,7 +356,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -360,11 +366,11 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
@@ -374,25 +380,25 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PosthogService } from './posthog.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [PosthogService],
|
||||
})
|
||||
export class PosthogModule {}
|
||||
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { CronJob } from 'cron';
|
||||
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
|
||||
import { throwErr } from 'src/utils';
|
||||
@Injectable()
|
||||
export class PosthogService {
|
||||
private postHogClient: PostHog;
|
||||
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
|
||||
console.log('Initializing PostHog');
|
||||
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
|
||||
host: 'https://eu.posthog.com',
|
||||
});
|
||||
|
||||
// Schedule the cron job only if analytics collection is allowed
|
||||
this.scheduleCronJob();
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleCronJob() {
|
||||
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
|
||||
await this.capture();
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob('captureAnalytics', job);
|
||||
job.start();
|
||||
}
|
||||
|
||||
async capture() {
|
||||
if (!this.postHogClient) {
|
||||
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
this.postHogClient.capture({
|
||||
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
|
||||
event: 'sh_instance',
|
||||
properties: {
|
||||
type: 'COMMUNITY',
|
||||
total_user_count: await this.prismaService.user.count(),
|
||||
total_workspace_count: await this.prismaService.team.count(),
|
||||
version: this.configService.get('npm_package_version'),
|
||||
},
|
||||
});
|
||||
console.log('Sent event to PostHog');
|
||||
}
|
||||
}
|
||||
14
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
14
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -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 { UserModule } from '../user/user.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
import { TeamCollectionController } from './team-collection.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
||||
@@ -15,5 +16,6 @@ import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
GqlCollectionTeamMemberGuard,
|
||||
],
|
||||
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
||||
controllers: [TeamCollectionController],
|
||||
})
|
||||
export class TeamCollectionModule {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TeamCollection } from './team-collection.model';
|
||||
import {
|
||||
@@ -14,14 +14,21 @@ import {
|
||||
TEAM_COL_SAME_NEXT_COLL,
|
||||
TEAM_COL_REORDERING_FAILED,
|
||||
TEAM_COLL_DATA_INVALID,
|
||||
TEAM_REQ_SEARCH_FAILED,
|
||||
TEAM_COL_SEARCH_FAILED,
|
||||
TEAM_REQ_PARENT_TREE_GEN_FAILED,
|
||||
TEAM_COLL_PARENT_TREE_GEN_FAILED,
|
||||
} from '../errors';
|
||||
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 O from 'fp-ts/Option';
|
||||
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||
import { stringToJson } from 'src/utils';
|
||||
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
|
||||
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
|
||||
import { RESTError } from 'src/types/RESTError';
|
||||
|
||||
@Injectable()
|
||||
export class TeamCollectionService {
|
||||
@@ -1056,4 +1063,285 @@ export class TeamCollectionService {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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,30 +4,23 @@ export enum InfraConfigEnum {
|
||||
|
||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
|
||||
|
||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
|
||||
GITHUB_SCOPE = 'GITHUB_SCOPE',
|
||||
|
||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||
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',
|
||||
|
||||
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',
|
||||
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
*/
|
||||
export type AuthError = {
|
||||
export type RESTError = {
|
||||
message: string;
|
||||
statusCode: HttpStatus;
|
||||
};
|
||||
@@ -17,3 +17,21 @@ export class PaginationArgs {
|
||||
})
|
||||
take: number;
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class OffsetPaginationArgs {
|
||||
@Field({
|
||||
nullable: true,
|
||||
defaultValue: 0,
|
||||
description: 'Number of items to skip',
|
||||
})
|
||||
skip: number;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
defaultValue: 10,
|
||||
description: 'Number of items to fetch',
|
||||
})
|
||||
take: number;
|
||||
}
|
||||
|
||||
@@ -56,3 +56,22 @@ export enum SessionType {
|
||||
registerEnumType(SessionType, {
|
||||
name: 'SessionType',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class UserDeletionResult {
|
||||
@Field(() => ID, {
|
||||
description: 'UID of the user',
|
||||
})
|
||||
userUID: string;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Flag to determine if user deletion was successful or not',
|
||||
})
|
||||
isDeleted: Boolean;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
description: 'Error message if user deletion was not successful',
|
||||
})
|
||||
errorMessage: String;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,29 @@ export class UserResolver {
|
||||
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
||||
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, {
|
||||
description: 'Delete an user account',
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { JSON_INVALID, 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 { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
@@ -176,6 +181,26 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUsersByIds', () => {
|
||||
test('should successfully return users given valid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.findUsersByIds([
|
||||
'123344',
|
||||
'5555',
|
||||
'6666',
|
||||
]);
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
test('should return empty array of users given a invalid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.findUsersByIds(['sdcvbdbr']);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUserViaMagicLink', () => {
|
||||
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
||||
mockPrisma.user.create.mockResolvedValueOnce(user);
|
||||
@@ -414,6 +439,62 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserDisplayName', () => {
|
||||
test('should resolve right and update user display name', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
user.uid,
|
||||
newDisplayName,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
});
|
||||
});
|
||||
test('should resolve right and publish user updated subscription', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
await userService.updateUserDisplayName(user.uid, user.displayName);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user/${user.uid}/updated`,
|
||||
{
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
},
|
||||
);
|
||||
});
|
||||
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
'invalidUserUid',
|
||||
user.displayName,
|
||||
);
|
||||
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('fetchAllUsers', () => {
|
||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
@@ -435,6 +516,36 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllUsersV2', () => {
|
||||
test('should resolve right and return first 20 users when searchString is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2(null, {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve right and return next 20 users when searchString is provided', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('.com', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve left and return an empty array when users not found', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('Unknown entry', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAdminUsers', () => {
|
||||
test('should return a list of admin users', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
||||
@@ -556,4 +667,17 @@ describe('UserService', () => {
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUsersAsAdmin', () => {
|
||||
test('should resolve right and return true for valid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 1 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
test('should resolve right and return false for invalid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 0 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualLeft(USERS_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,18 @@ import * as T from 'fp-ts/Task';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { pipe, constVoid } from 'fp-ts/function';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { 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 { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
||||
import { UserDataHandler } from './user.data.handler';
|
||||
import { User as DbUser } from '@prisma/client';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -88,6 +93,20 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users with given IDs
|
||||
* @param userUIDs User IDs
|
||||
* @returns Array of found Users
|
||||
*/
|
||||
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
uid: { in: userUIDs },
|
||||
},
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update User with new generated hashed refresh token
|
||||
*
|
||||
@@ -269,6 +288,34 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's data
|
||||
* @param userUID User UID
|
||||
* @param displayName User's displayName
|
||||
* @returns a Either of User or error
|
||||
*/
|
||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||
if (!displayName || displayName.length === 0) {
|
||||
return E.left(USER_SHORT_DISPLAY_NAME);
|
||||
}
|
||||
|
||||
try {
|
||||
const dbUpdatedUser = await this.prisma.user.update({
|
||||
where: { uid: userUID },
|
||||
data: { displayName },
|
||||
});
|
||||
|
||||
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
|
||||
|
||||
// Publish subscription for user updates
|
||||
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
|
||||
|
||||
return E.right(updatedUser);
|
||||
} catch (error) {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse currentRESTSession and currentGQLSession
|
||||
* @param sessionData string of the session
|
||||
@@ -286,6 +333,7 @@ export class UserService {
|
||||
* @param cursorID string of userUID or null
|
||||
* @param take number of users to query
|
||||
* @returns an array of `User` object
|
||||
* @deprecated use fetchAllUsersV2 instead
|
||||
*/
|
||||
async fetchAllUsers(cursorID: string, take: number) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
@@ -296,6 +344,43 @@ export class UserService {
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the `User` table based on cursor
|
||||
* @param searchString search on user's displayName or email
|
||||
* @param paginationOption pagination options
|
||||
* @returns an array of `User` object
|
||||
*/
|
||||
async fetchAllUsersV2(
|
||||
searchString: string,
|
||||
paginationOption: OffsetPaginationArgs,
|
||||
) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
skip: paginationOption.skip,
|
||||
take: paginationOption.take,
|
||||
where: searchString
|
||||
? {
|
||||
OR: [
|
||||
{
|
||||
displayName: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: [{ isAdmin: 'desc' }, { displayName: 'asc' }],
|
||||
});
|
||||
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the number of users in db
|
||||
* @returns a count (Int) of user records in DB
|
||||
@@ -326,6 +411,23 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users to admins by toggling isAdmin param to true
|
||||
* @param userUID user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async makeAdmins(userUIDs: string[]) {
|
||||
try {
|
||||
await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: true },
|
||||
});
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the admin users
|
||||
* @returns an array of admin users
|
||||
@@ -444,4 +546,22 @@ export class UserService {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users from an admin by toggling isAdmin param to false
|
||||
* @param userUIDs user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async removeUsersAsAdmin(userUIDs: string[]) {
|
||||
const data = await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: false },
|
||||
});
|
||||
|
||||
if (data.count === 0) {
|
||||
return E.left(USERS_NOT_FOUND);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { ExecutionContext, HttpException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { pipe } from 'fp-ts/lib/function';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
JSON_INVALID,
|
||||
} from './errors';
|
||||
import { AuthProvider } from './auth/helper';
|
||||
import { RESTError } from './types/RESTError';
|
||||
|
||||
/**
|
||||
* A workaround to throw an exception in an expression.
|
||||
@@ -27,6 +28,15 @@ export function throwErr(errMessage: string): never {
|
||||
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.
|
||||
* Used for debugging functional pipelines.
|
||||
@@ -173,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
|
||||
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
|
||||
* @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"`
|
||||
|
||||
## 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:
|
||||
```
|
||||
npm i -g @hoppscotch/cli
|
||||
```
|
||||
|
||||
- Once the dependencies are installed, install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
|
||||
```
|
||||
npm i -g @hoppscotch/cli
|
||||
```
|
||||
|
||||
## **Developing:**
|
||||
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
// * The entry point of the CLI
|
||||
// @ts-check
|
||||
|
||||
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",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.0",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"type": "module",
|
||||
@@ -41,26 +41,29 @@
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"axios": "^1.6.6",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qs": "^6.11.2",
|
||||
"zod": "^3.22.4"
|
||||
"axios": "1.6.7",
|
||||
"chalk": "5.3.0",
|
||||
"commander": "11.1.0",
|
||||
"isolated-vm": "4.7.2",
|
||||
"lodash-es": "4.17.21",
|
||||
"qs": "6.11.2",
|
||||
"verzod": "0.2.2",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@swc/core": "^1.3.105",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"fp-ts": "^2.16.2",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.4",
|
||||
"ts-jest": "^29.1.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"@relmify/jest-fp-ts": "2.1.1",
|
||||
"@swc/core": "1.4.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/qs": "6.9.12",
|
||||
"fp-ts": "2.16.2",
|
||||
"jest": "29.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"qs": "6.11.2",
|
||||
"ts-jest": "29.1.2",
|
||||
"tsup": "8.0.2",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,138 +3,317 @@ import { ExecException } from "child_process";
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
describe("Test `hopp test <file>` command:", () => {
|
||||
describe("Argument parsing", () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Some errors occured (exit code 1).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
||||
describe("Supplied collection export file validations", () => {
|
||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
|
||||
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
||||
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"collection-level-headers-auth-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"pre-req-script-env-var-persistence-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
describe("Supplied environment export file validations", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
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(
|
||||
"notjson-coll.txt",
|
||||
"collection"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env notfound.json`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
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 args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
|
||||
});
|
||||
|
||||
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
|
||||
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
|
||||
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
|
||||
});
|
||||
});
|
||||
|
||||
test("ENV file not JSON type.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("ENV file not found.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env notfound.json`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
||||
test("Successfully resolves values from the supplied environment export file", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"env-flag-tests-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Correctly resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json");
|
||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json");
|
||||
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"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 { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Works with shorth `-e` flag", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"env-flag-tests-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
describe("Secret environment variables", () => {
|
||||
jest.setTimeout(100000);
|
||||
|
||||
// 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 () => {
|
||||
const env = {
|
||||
...process.env,
|
||||
secretBearerToken: "test-token",
|
||||
secretBasicAuthUsername: "test-user",
|
||||
secretBasicAuthPassword: "test-pass",
|
||||
secretQueryParamValue: "secret-query-param-value",
|
||||
secretBodyValue: "secret-body-value",
|
||||
secretHeaderValue: "secret-header-value",
|
||||
};
|
||||
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error, stdout } = await runCLI(args, { env });
|
||||
|
||||
expect(stdout).toContain(
|
||||
"https://httpbin.org/basic-auth/*********/*********"
|
||||
);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-supplied-values-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error, stdout } = await runCLI(args);
|
||||
|
||||
expect(stdout).toContain(
|
||||
"https://httpbin.org/basic-auth/*********/*********"
|
||||
);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-persistence-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-supplied-values-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error, stdout } = await runCLI(args);
|
||||
|
||||
expect(stdout).toContain(
|
||||
"https://httpbin.org/basic-auth/*********/*********"
|
||||
);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
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(
|
||||
"secret-envs-persistence-scripting-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-envs-persistence-scripting-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("No value passed to delay flag.", async () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -142,7 +321,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Invalid value passed to delay flag.", async () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -150,10 +329,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Valid value passed to delay flag.", async () => {
|
||||
test("Successfully performs delayed request execution for a valid delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay 1`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Works with the short `-d` flag", async () => {
|
||||
const args = `${VALID_TEST_ARGS} -d 1`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderA",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderC",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestD",
|
||||
"params": [],
|
||||
@@ -40,7 +40,8 @@
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
@@ -52,7 +53,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestC",
|
||||
"params": [],
|
||||
@@ -67,13 +68,14 @@
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "api-key",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"addTo": "HEADERS",
|
||||
"key": "key",
|
||||
"value": "test-key"
|
||||
},
|
||||
@@ -88,7 +90,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
@@ -104,6 +106,7 @@
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
@@ -116,7 +119,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
@@ -132,6 +135,7 @@
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
@@ -149,16 +153,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "CollectionB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderA",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
@@ -174,6 +178,7 @@
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
@@ -186,7 +191,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
@@ -202,6 +207,7 @@
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
@@ -218,4 +224,4 @@
|
||||
"token": "BearerToken"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "<<URL>>",
|
||||
"name": "test1",
|
||||
"params": [],
|
||||
@@ -16,7 +16,8 @@
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "",
|
||||
"params": [],
|
||||
@@ -13,20 +13,18 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
},
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -34,17 +32,15 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
{
|
||||
"v": 1,
|
||||
"folders": [],
|
||||
"requests":
|
||||
"requests":
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "fail",
|
||||
"params": [],
|
||||
@@ -12,20 +12,18 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": [],
|
||||
},
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -33,17 +31,15 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
{
|
||||
"v": 1,
|
||||
"folders": [],
|
||||
"requests":
|
||||
"requests":
|
||||
{
|
||||
"v": "1",
|
||||
"v": "2",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "fail",
|
||||
"params": [],
|
||||
@@ -22,7 +22,8 @@
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "",
|
||||
"params": [],
|
||||
@@ -13,20 +13,18 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
},
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -34,17 +32,15 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"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});",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||
}
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"v": 2,
|
||||
"name": "pre-req-script-env-var-persistence-coll",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": { "body": null, "contentType": null },
|
||||
"name": "sample-req",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
||||
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": { "authType": "inherit", "authActive": true },
|
||||
"headers": []
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"v": "3",
|
||||
"name": "test-request",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"method": "POST",
|
||||
@@ -19,7 +19,8 @@
|
||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||
},
|
||||
"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": {
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"v": 2,
|
||||
"name": "secret-envs-coll",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-headers",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [
|
||||
{
|
||||
"key": "Secret-Header-Key",
|
||||
"value": "<<secretHeaderValue>>",
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"requestVariables": [],
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||
"contentType": "application/json"
|
||||
},
|
||||
"name": "test-secret-body",
|
||||
"method": "POST",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"requestVariables": [],
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-query-params",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "secretQueryParamKey",
|
||||
"value": "<<secretQueryParamValue>>",
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"headers": [],
|
||||
"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})",
|
||||
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "basic",
|
||||
"password": "<<secretBasicAuthPassword>>",
|
||||
"username": "<<secretBasicAuthUsername>>",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-basic-auth",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"requestVariables": [],
|
||||
"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": ""
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"token": "<<secretBearerToken>>",
|
||||
"authType": "bearer",
|
||||
"password": "testpassword",
|
||||
"username": "testuser",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-bearer-auth",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"requestVariables": [],
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-fallback",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"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})",
|
||||
"preRequestScript": ""
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": false
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"v": 2,
|
||||
"name": "secret-envs-persistence-coll",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-headers",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"requestVariables": [],
|
||||
"headers": [
|
||||
{
|
||||
"key": "Secret-Header-Key",
|
||||
"value": "<<secretHeaderValue>>",
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"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})",
|
||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-headers-overrides",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"requestVariables": [],
|
||||
"headers": [
|
||||
{
|
||||
"key": "Secret-Header-Key",
|
||||
"value": "<<secretHeaderValue>>",
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"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})",
|
||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||
"contentType": "application/json"
|
||||
},
|
||||
"name": "test-secret-body",
|
||||
"method": "POST",
|
||||
"params": [],
|
||||
"requestVariables": [],
|
||||
"headers": [],
|
||||
"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 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": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-query-params",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "secretQueryParamKey",
|
||||
"value": "<<secretQueryParamValue>>",
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"requestVariables": [],
|
||||
"headers": [],
|
||||
"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})",
|
||||
"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": "3",
|
||||
"auth": {
|
||||
"authType": "basic",
|
||||
"password": "<<secretBasicAuthPassword>>",
|
||||
"username": "<<secretBasicAuthUsername>>",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-basic-auth",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"requestVariables": [],
|
||||
"headers": [],
|
||||
"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": "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": "3",
|
||||
"auth": {
|
||||
"token": "<<secretBearerToken>>",
|
||||
"authType": "bearer",
|
||||
"password": "testpassword",
|
||||
"username": "testuser",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "test-secret-bearer-auth",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"requestVariables": [],
|
||||
"headers": [],
|
||||
"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 // 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)"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": false
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"v": 2,
|
||||
"name": "secret-envs-persistence-scripting-req",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/post",
|
||||
"name": "req",
|
||||
"params": [],
|
||||
"headers": [
|
||||
{
|
||||
"active": true,
|
||||
"key": "Custom-Header",
|
||||
"value": "<<customHeaderValueFromSecretVar>>"
|
||||
}
|
||||
],
|
||||
"method": "POST",
|
||||
"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\")",
|
||||
"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": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": { "authType": "inherit", "authActive": false },
|
||||
"headers": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"v": 0,
|
||||
"name": "Env-I",
|
||||
"variables": [
|
||||
{
|
||||
"key": "firstName",
|
||||
"value": "John"
|
||||
},
|
||||
{
|
||||
"key": "lastName",
|
||||
"value": "Doe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"v": 1,
|
||||
"id": "2",
|
||||
"name": "Env-II",
|
||||
"variables": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://echo.hoppscotch.io",
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"key": "secretVar",
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": 123,
|
||||
"v": "1",
|
||||
"name": "secret-envs",
|
||||
"values": [
|
||||
{
|
||||
"key": "secretVar",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "regularVar",
|
||||
"secret": false,
|
||||
"value": "regular-variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"v": 0,
|
||||
"name": "Response body sample",
|
||||
"variables": [
|
||||
{
|
||||
@@ -34,4 +35,4 @@
|
||||
"value": "<<salutation>> <<fullName>>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"v": 1,
|
||||
"id": "2",
|
||||
"name": "secret-envs-persistence-scripting-envs",
|
||||
"variables": [
|
||||
{
|
||||
"key": "preReqVarOne",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "preReqVarTwo",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "postReqVarOne",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "preReqVarTwo",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "customHeaderValueFromSecretVar",
|
||||
"secret": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "2",
|
||||
"v": 1,
|
||||
"name": "secret-envs",
|
||||
"variables": [
|
||||
{
|
||||
"key": "secretBearerToken",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBasicAuthUsername",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBasicAuthPassword",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretQueryParamValue",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBodyValue",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretHeaderValue",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "nonExistentValueInSystemEnv",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "echoHoppBaseURL",
|
||||
"value": "https://echo.hoppscotch.io",
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"key": "httpbinBaseURL",
|
||||
"value": "https://httpbin.org",
|
||||
"secret": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"v": 1,
|
||||
"id": "2",
|
||||
"name": "secret-values-envs",
|
||||
"variables": [
|
||||
{
|
||||
"key": "secretBearerToken",
|
||||
"value": "test-token",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBasicAuthUsername",
|
||||
"value": "test-user",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBasicAuthPassword",
|
||||
"value": "test-pass",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretQueryParamValue",
|
||||
"value": "secret-query-param-value",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretBodyValue",
|
||||
"value": "secret-body-value",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "secretHeaderValue",
|
||||
"value": "secret-header-value",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "nonExistentValueInSystemEnv",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "echoHoppBaseURL",
|
||||
"value": "https://echo.hoppscotch.io",
|
||||
"secret": false
|
||||
},
|
||||
{
|
||||
"key": "httpbinBaseURL",
|
||||
"value": "https://httpbin.org",
|
||||
"secret": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,15 +3,16 @@ import { resolve } from "path";
|
||||
|
||||
import { ExecResponse } from "./types";
|
||||
|
||||
export const runCLI = (args: string): Promise<ExecResponse> =>
|
||||
{
|
||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||
const command = `node ${CLI_PATH} ${args}`
|
||||
export const runCLI = (args: string, options = {}): Promise<ExecResponse> => {
|
||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp.js");
|
||||
const command = `node ${CLI_PATH} ${args}`;
|
||||
|
||||
return new Promise((resolve) =>
|
||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
}
|
||||
return new Promise((resolve) =>
|
||||
exec(command, options, (error, stdout, stderr) =>
|
||||
resolve({ error, stdout, stderr })
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const trimAnsi = (target: string) => {
|
||||
const ansiRegex =
|
||||
@@ -25,7 +26,18 @@ export const getErrorCode = (out: string) => {
|
||||
return ansiTrimmedStr.split(" ")[0];
|
||||
};
|
||||
|
||||
export const getTestJsonFilePath = (file: string) => {
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`);
|
||||
export const getTestJsonFilePath = (
|
||||
file: string,
|
||||
kind: "collection" | "environment"
|
||||
) => {
|
||||
const kindDir = {
|
||||
collection: "collections",
|
||||
environment: "environments",
|
||||
}[kind];
|
||||
|
||||
const filePath = resolve(
|
||||
__dirname,
|
||||
`../../src/__tests__/samples/${kindDir}/${file}`
|
||||
);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import * as E from "fp-ts/Either";
|
||||
|
||||
import { version } from "../package.json";
|
||||
import { test } from "./commands/test";
|
||||
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"
|
||||
)}`;
|
||||
|
||||
const program = new Command()
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("hopp")
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface RequestStack {
|
||||
*/
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
supported: boolean;
|
||||
displayUrl?: string
|
||||
}
|
||||
|
||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
@@ -30,6 +31,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
* This contains path, params and environment variables all applied to it
|
||||
*/
|
||||
effectiveFinalURL: string;
|
||||
effectiveFinalDisplayURL?: string;
|
||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalBody: FormData | string | null;
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
import { Environment } from "@hoppscotch/data";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { error } from "../../types/errors";
|
||||
import {
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
HoppEnvPair,
|
||||
HoppEnvs,
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
* Parses env json file for given path and validates the parsed env json object
|
||||
* @param path Path of env.json file to be parsed
|
||||
* @returns For successful parsing we get HoppEnvs object
|
||||
*/
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.safeParse(contents);
|
||||
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
|
||||
|
||||
// CLI doesnt support bulk environments export.
|
||||
// Hence we check for this case and throw an error if it matches the format.
|
||||
// The legacy key-value pair format that is still supported
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
|
||||
// Shape of the single environment export object that is exported from the app
|
||||
const HoppEnvExportObjectResult = Environment.safeParse(contents);
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app
|
||||
const HoppBulkEnvExportObjectResult = z
|
||||
.array(entityReference(Environment))
|
||||
.safeParse(contents);
|
||||
|
||||
// CLI doesnt support bulk environments export
|
||||
// Hence we check for this case and throw an error if it matches the format
|
||||
if (HoppBulkEnvExportObjectResult.success) {
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
// Checks if the environment file is of the correct format.
|
||||
// If it doesnt match either of them, we throw an error.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
// Checks if the environment file is of the correct format
|
||||
// If it doesnt match either of them, we throw an error
|
||||
if (
|
||||
!HoppEnvKeyPairResult.success &&
|
||||
HoppEnvExportObjectResult.type === "err"
|
||||
) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
envPairs.push({ key, value, secret: false });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
envPairs.push(...HoppEnvExportObjectResult.data.variables);
|
||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||
}
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
value: string | Blob;
|
||||
};
|
||||
|
||||
export type HoppEnvPair = { key: string; value: string };
|
||||
export type HoppEnvPair = Environment["variables"][number];
|
||||
|
||||
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||
|
||||
// Shape of the single environment export object that is exported from the app.
|
||||
export const HoppEnvExportObject = z.object({
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app.
|
||||
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
||||
|
||||
export type HoppEnvs = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { CommanderError } from "commander";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
|
||||
@@ -14,48 +12,6 @@ export const hasProperty = <P extends PropertyKey>(
|
||||
prop: P
|
||||
): 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
|
||||
* of code property.
|
||||
|
||||
@@ -131,7 +131,7 @@ const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
|
||||
* path of each request within collection-json file, failed-tests-report, errors,
|
||||
* total execution duration for requests, pre-request-scripts, test-scripts.
|
||||
* @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 = (
|
||||
requestsReport: RequestReport[]
|
||||
|
||||
@@ -112,7 +112,7 @@ export const printTestsMetrics = (testsMetrics: TestMetrics) => {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const printErrorsReport = (
|
||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
||||
*/
|
||||
start: (requestConfig: RequestConfig) => {
|
||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||
const ENDPOINT = requestConfig.url;
|
||||
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||
|
||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import fs from "fs/promises";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { error } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection } from "@hoppscotch/data";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
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.
|
||||
@@ -67,7 +105,11 @@ export async function parseCollectionData(
|
||||
? contents
|
||||
: [contents];
|
||||
|
||||
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
const collectionSchemaParsedResult = z
|
||||
.array(entityReference(HoppCollection))
|
||||
.safeParse(maybeArrayOfCollections);
|
||||
|
||||
if (!collectionSchemaParsedResult.success) {
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
@@ -75,5 +117,5 @@ export async function parseCollectionData(
|
||||
});
|
||||
}
|
||||
|
||||
return maybeArrayOfCollections as HoppCollection[];
|
||||
return getValidRequests(collectionSchemaParsedResult.data, path);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
|
||||
export const preRequestScriptRunner = (
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> =>
|
||||
pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
|
||||
export function getEffectiveRESTRequest(
|
||||
request: HoppRESTRequest,
|
||||
environment: Environment
|
||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
||||
): E.Either<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> {
|
||||
const envVariables = environment.variables;
|
||||
|
||||
// Parsing final headers with applied ENVs.
|
||||
@@ -103,27 +109,40 @@ export function getEffectiveRESTRequest(
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
});
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
) {
|
||||
} else if (request.auth.authType === "bearer") {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(
|
||||
request.auth.token,
|
||||
envVariables
|
||||
)}`,
|
||||
value: `Bearer ${parseTemplateString(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") {
|
||||
const { key, value, addTo } = request.auth;
|
||||
if (addTo === "Headers") {
|
||||
if (addTo === "HEADERS") {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: parseTemplateString(key, envVariables),
|
||||
value: parseTemplateString(value, envVariables),
|
||||
});
|
||||
} else if (addTo === "Query params") {
|
||||
} else if (addTo === "QUERY_PARAMS") {
|
||||
effectiveFinalParams.push({
|
||||
active: true,
|
||||
key: parseTemplateString(key, envVariables),
|
||||
@@ -162,12 +181,30 @@ export function getEffectiveRESTRequest(
|
||||
}
|
||||
const effectiveFinalURL = _effectiveFinalURL.right;
|
||||
|
||||
// Secret environment variables referenced in the request endpoint should be masked
|
||||
let effectiveFinalDisplayURL;
|
||||
if (envVariables.some(({ secret }) => secret)) {
|
||||
const _effectiveFinalDisplayURL = parseTemplateStringE(
|
||||
request.endpoint,
|
||||
envVariables,
|
||||
true
|
||||
);
|
||||
|
||||
if (E.isRight(_effectiveFinalDisplayURL)) {
|
||||
effectiveFinalDisplayURL = _effectiveFinalDisplayURL.right;
|
||||
}
|
||||
}
|
||||
|
||||
return E.right({
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
effectiveRequest: {
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalDisplayURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
},
|
||||
updatedEnvs: { global: [], selected: envVariables },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import axios, { Method } from "axios";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as E from "fp-ts/Either";
|
||||
@@ -29,6 +29,38 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
|
||||
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
|
||||
|
||||
/**
|
||||
* Processes given variable, which includes checking for secret variables
|
||||
* and getting value from system environment
|
||||
* @param variable Variable to be processed
|
||||
* @returns Updated variable with value from system environment
|
||||
*/
|
||||
const processVariables = (variable: Environment["variables"][number]) => {
|
||||
if (variable.secret) {
|
||||
return {
|
||||
...variable,
|
||||
value:
|
||||
"value" in variable ? variable.value : process.env[variable.key] || "",
|
||||
};
|
||||
}
|
||||
return variable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes given envs, which includes processing each variable in global
|
||||
* and selected envs
|
||||
* @param envs Global + selected envs used by requests with in collection
|
||||
* @returns Processed envs with each variable processed
|
||||
*/
|
||||
const processEnvs = (envs: HoppEnvs) => {
|
||||
const processedEnvs = {
|
||||
global: envs.global.map(processVariables),
|
||||
selected: envs.selected.map(processVariables),
|
||||
};
|
||||
|
||||
return processedEnvs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms given request data to request-config used by request-runner to
|
||||
* perform HTTP request.
|
||||
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL,
|
||||
};
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
const reqParams = finalParams(req);
|
||||
@@ -98,6 +131,7 @@ export const requestRunner =
|
||||
let status: number;
|
||||
const baseResponse = await axios(requestConfig);
|
||||
const { config } = baseResponse;
|
||||
// PR-COMMENT: type error
|
||||
const runnerResponse: RequestRunnerResponse = {
|
||||
...baseResponse,
|
||||
endpoint: getRequest.endpoint(config.url),
|
||||
@@ -221,9 +255,16 @@ export const processRequest =
|
||||
effectiveFinalParams: [],
|
||||
effectiveFinalURL: "",
|
||||
};
|
||||
let updatedEnvs = <HoppEnvs>{};
|
||||
|
||||
// Fetch values for secret environment variables from system environment
|
||||
const processedEnvs = processEnvs(envs);
|
||||
|
||||
// Executing pre-request-script
|
||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
||||
const preRequestRes = await preRequestScriptRunner(
|
||||
request,
|
||||
processedEnvs
|
||||
)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
||||
@@ -231,8 +272,8 @@ export const processRequest =
|
||||
report.errors.push(preRequestRes.left);
|
||||
report.result = report.result && false;
|
||||
} else {
|
||||
// Updating effective-request
|
||||
effectiveRequest = preRequestRes.right;
|
||||
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||
}
|
||||
|
||||
// Creating request-config for request-runner.
|
||||
@@ -270,7 +311,7 @@ export const processRequest =
|
||||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
request,
|
||||
envs
|
||||
updatedEnvs
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
@@ -310,7 +351,7 @@ export const processRequest =
|
||||
*/
|
||||
export const preProcessRequest = (
|
||||
request: HoppRESTRequest,
|
||||
collection: HoppCollection,
|
||||
collection: HoppCollection
|
||||
): HoppRESTRequest => {
|
||||
const tempRequest = Object.assign({}, request);
|
||||
const { headers: parentHeaders, auth: parentAuth } = collection;
|
||||
@@ -335,8 +376,10 @@ export const preProcessRequest = (
|
||||
// Filter out header entries present in the parent (folder/collection) under the same name
|
||||
// This ensures the child headers take precedence over the parent headers
|
||||
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
|
||||
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
|
||||
})
|
||||
return !tempRequest.headers.some(
|
||||
(reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key
|
||||
);
|
||||
});
|
||||
tempRequest.headers.push(...filteredEntries);
|
||||
} else if (!tempRequest.headers) {
|
||||
tempRequest.headers = [];
|
||||
|
||||
@@ -10,6 +10,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
ecmaFeatures: {
|
||||
jsx: false,
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
"@vue/typescript/recommended",
|
||||
|
||||
@@ -429,6 +429,11 @@ pre.ace_editor {
|
||||
}
|
||||
}
|
||||
|
||||
.splitpanes__pane {
|
||||
@apply will-change-auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@@ -558,12 +563,22 @@ details[open] summary .indicator {
|
||||
.env-highlight {
|
||||
@apply text-accentContrast;
|
||||
|
||||
&.env-found {
|
||||
@apply bg-accentDark;
|
||||
@apply hover:bg-accent;
|
||||
&.request-variable-highlight {
|
||||
@apply bg-amber-500;
|
||||
@apply hover:bg-amber-600;
|
||||
}
|
||||
|
||||
&.env-not-found {
|
||||
&.environment-variable-highlight {
|
||||
@apply bg-green-500;
|
||||
@apply hover:bg-green-600;
|
||||
}
|
||||
|
||||
&.global-variable-highlight {
|
||||
@apply bg-blue-500;
|
||||
@apply hover:bg-blue-600;
|
||||
}
|
||||
|
||||
&.environment-not-found-highlight {
|
||||
@apply bg-red-500;
|
||||
@apply hover:bg-red-600;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
"go_back": "Go back",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"hide_secret": "Hide secret",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"download_here": "Download here",
|
||||
"less": "Less",
|
||||
"more": "More",
|
||||
"new": "New",
|
||||
@@ -43,6 +45,7 @@
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"show_secret": "Show secret",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -101,8 +104,10 @@
|
||||
"auth": {
|
||||
"account_exists": "Account exists with different credential - Login to link both accounts",
|
||||
"all_sign_in_options": "All sign in options",
|
||||
"continue_with_auth_provider": "Continue with {provider}",
|
||||
"continue_with_email": "Continue with Email",
|
||||
"continue_with_github": "Continue with GitHub",
|
||||
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
|
||||
"continue_with_google": "Continue with Google",
|
||||
"continue_with_microsoft": "Continue with Microsoft",
|
||||
"email": "Email",
|
||||
@@ -135,9 +140,30 @@
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
|
||||
"grant_type": "Grant Type",
|
||||
"grant_type_auth_code": "Authorization Code",
|
||||
"token_fetched_successfully": "Token fetched successfully",
|
||||
"token_fetch_failed": "Failed to fetch token",
|
||||
"validation_failed": "Validation Failed, please check the form fields",
|
||||
"label_authorization_endpoint": "Authorization Endpoint",
|
||||
"label_client_id": "Client ID",
|
||||
"label_client_secret": "Client Secret",
|
||||
"label_code_challenge": "Code Challenge",
|
||||
"label_code_challenge_method": "Code Challenge Method",
|
||||
"label_code_verifier": "Code Verifier",
|
||||
"label_scopes": "Scopes",
|
||||
"label_token_endpoint": "Token Endpoint",
|
||||
"label_use_pkce": "Use PKCE",
|
||||
"label_implicit": "Implicit",
|
||||
"label_password": "Password",
|
||||
"label_username": "Username",
|
||||
"label_auth_code": "Authorization Code",
|
||||
"label_client_credentials": "Client Credentials"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"pass_by_query_params_label": "Query Parameters",
|
||||
"pass_by_headers_label": "Headers",
|
||||
"password": "Password",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Token",
|
||||
@@ -149,10 +175,11 @@
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Edit Collection",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"import_collection":"Import Collection",
|
||||
"invalid_name": "Please provide a name for the collection",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"my_collections": "My Collections",
|
||||
"my_collections": "Personal Collections",
|
||||
"name": "My New Collection",
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "New Collection",
|
||||
@@ -164,14 +191,12 @@
|
||||
"save_as": "Save as",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"select": "Select a Collection",
|
||||
"select_location": "Select location",
|
||||
"select_team": "Select a team",
|
||||
"team_collections": "Team Collections"
|
||||
"select_location": "Select location"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"exit_team": "Are you sure you want to leave this team?",
|
||||
"exit_team": "Are you sure you want to leave this workspace?",
|
||||
"logout": "Are you sure you want to logout?",
|
||||
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
||||
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
||||
@@ -179,7 +204,7 @@
|
||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Are you sure you want to delete this team?",
|
||||
"remove_team": "Are you sure you want to delete this workspace?",
|
||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
@@ -232,17 +257,19 @@
|
||||
"headers": "This request does not have any headers",
|
||||
"history": "History is empty",
|
||||
"invites": "Invite list is empty",
|
||||
"members": "Team is empty",
|
||||
"members": "Workspace is empty",
|
||||
"parameters": "This request does not have any parameters",
|
||||
"pending_invites": "There are no pending invites for this team",
|
||||
"pending_invites": "There are no pending invites for this workspace",
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"request_variables": "This request does not have any request variables",
|
||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||
"secret_environments": "Secrets are not synced to Hoppscotch",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Team name empty",
|
||||
"teams": "You don't belong to any teams",
|
||||
"team_name": "Workspace name empty",
|
||||
"teams": "You don't belong to any workspaces",
|
||||
"tests": "There are no tests for this request"
|
||||
},
|
||||
"environment": {
|
||||
@@ -259,7 +286,7 @@
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Please provide a name for the environment",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
"my_environments": "Personal Environments",
|
||||
"name": "Name",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "New Environment",
|
||||
@@ -269,14 +296,17 @@
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"secrets": "Secrets",
|
||||
"secret_value": "Secret value",
|
||||
"select": "Select environment",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Workspace Environments",
|
||||
"title": "Environments",
|
||||
"updated": "Environment updated",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"variables": "Variables",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
@@ -286,8 +316,9 @@
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL is not formatted properly",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"delete_account": "Your account is currently an owner in these workspaces:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
|
||||
"empty_profile_name": "Profile name cannot be empty",
|
||||
"empty_req_name": "Empty Request Name",
|
||||
"f12_details": "(F12 for details)",
|
||||
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
||||
@@ -307,9 +338,11 @@
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"same_profile_name": "Updated profile name is same as the current profile name",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
"test_script_fail": "Could not execute post-request script",
|
||||
"reading_files": "Error while reading one or more files."
|
||||
},
|
||||
"export": {
|
||||
"as_json": "Export as JSON",
|
||||
@@ -341,7 +374,8 @@
|
||||
"mutations": "Mutations",
|
||||
"schema": "Schema",
|
||||
"subscriptions": "Subscriptions",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "Switch connection",
|
||||
"url_placeholder": "Enter a GraphQL endpoint URL"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
@@ -388,8 +422,8 @@
|
||||
"from_insomnia_description": "Import from Insomnia collection",
|
||||
"from_json": "Import from Hoppscotch",
|
||||
"from_json_description": "Import from Hoppscotch collection file",
|
||||
"from_my_collections": "Import from My Collections",
|
||||
"from_my_collections_description": "Import from My Collections file",
|
||||
"from_my_collections": "Import from Personal Collections",
|
||||
"from_my_collections_description": "Import from Personal Collections file",
|
||||
"from_openapi": "Import from OpenAPI",
|
||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||
"from_postman": "Import from Postman",
|
||||
@@ -407,16 +441,21 @@
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "Import"
|
||||
"title": "Import",
|
||||
"file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of 10MB. Only the first {files} selected will be imported",
|
||||
"file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of 10MB. Please select another file.",
|
||||
"success": "Successfully imported"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"add_environment_value": "Add value",
|
||||
"empty_value": "Environment value is empty for the variable '{variable}' ",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set Cookie Headers. Please use Authorization Headers instead. However, our Hoppscotch Desktop App is live now and supports Cookies."
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
@@ -501,7 +540,7 @@
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
"no_permission": "You do not have permission to perform this action.",
|
||||
"owner": "Owner",
|
||||
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
|
||||
"owner_description": "Owners can add, edit, and delete requests, collections and workspace members.",
|
||||
"roles": "Roles",
|
||||
"roles_description": "Roles are used to control access to the shared collections.",
|
||||
"updated": "Profile updated",
|
||||
@@ -548,6 +587,7 @@
|
||||
"raw_body": "Raw Request Body",
|
||||
"rename": "Rename Request",
|
||||
"renamed": "Request renamed",
|
||||
"request_variables": "Request variables",
|
||||
"run": "Run",
|
||||
"save": "Save",
|
||||
"save_as": "Save as",
|
||||
@@ -559,6 +599,7 @@
|
||||
"title": "Request",
|
||||
"type": "Request type",
|
||||
"url": "URL",
|
||||
"url_placeholder": "Enter a URL or paste a cURL command",
|
||||
"variables": "Variables",
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
@@ -805,12 +846,19 @@
|
||||
"title": "Tabs"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"delete": "Delete current workspace",
|
||||
"edit": "Edit current workspace",
|
||||
"invite": "Invite people to workspace",
|
||||
"new": "Create new workspace",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"phrases":{
|
||||
"try": "Try",
|
||||
"import_collections": "Import collections",
|
||||
"create_environment": "Create environment",
|
||||
"create_workspace": "Create workspace",
|
||||
"share_request": "Share request"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -867,7 +915,6 @@
|
||||
"forum": "Ask questions and get answers",
|
||||
"github": "Follow us on Github",
|
||||
"shortcuts": "Browse app faster",
|
||||
"team": "Get in touch with the team",
|
||||
"title": "Support",
|
||||
"twitter": "Follow us on Twitter"
|
||||
},
|
||||
@@ -889,6 +936,7 @@
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"share_tab_request": "Share tab request",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Tests",
|
||||
@@ -897,60 +945,60 @@
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"team": {
|
||||
"already_member": "You are already a member of this team. Contact your team owner.",
|
||||
"create_new": "Create new team",
|
||||
"deleted": "Team deleted",
|
||||
"edit": "Edit Team",
|
||||
"already_member": "You are already a member of this workspace. Contact your workspace owner.",
|
||||
"create_new": "Create new workspace",
|
||||
"deleted": "Workspace deleted",
|
||||
"edit": "Edit Workspace",
|
||||
"email": "E-mail",
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
||||
"exit": "Exit Team",
|
||||
"exit_disabled": "Only owner cannot exit the team",
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your workspace owner.",
|
||||
"exit": "Exit Workspace",
|
||||
"exit_disabled": "Only owner cannot exit the workspace",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "Email format is invalid",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
"invalid_id": "Invalid workspace ID. Contact your workspace owner.",
|
||||
"invalid_invite_link": "Invalid invite link",
|
||||
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
|
||||
"invalid_member_permission": "Please provide a valid permission to the team member",
|
||||
"invalid_invite_link_description": "The link you followed is invalid. Contact your workspace owner.",
|
||||
"invalid_member_permission": "Please provide a valid permission to the workspace member",
|
||||
"invite": "Invite",
|
||||
"invite_more": "Invite more",
|
||||
"invite_tooltip": "Invite people to this workspace",
|
||||
"invited_to_team": "{owner} invited you to join {team}",
|
||||
"invited_to_team": "{owner} invited you to join {workspace}",
|
||||
"join": "Invitation accepted",
|
||||
"join_beta": "Join the beta program to access teams.",
|
||||
"join_team": "Join {team}",
|
||||
"joined_team": "You have joined {team}",
|
||||
"joined_team_description": "You are now a member of this team",
|
||||
"left": "You left the team",
|
||||
"join_team": "Join {workspace}",
|
||||
"joined_team": "You have joined {workspace}",
|
||||
"joined_team_description": "You are now a member of this workspace",
|
||||
"left": "You left the workspace",
|
||||
"login_to_continue": "Login to continue",
|
||||
"login_to_continue_description": "You need to be logged in to join a team.",
|
||||
"login_to_continue_description": "You need to be logged in to join a workspace.",
|
||||
"logout_and_try_again": "Logout and sign in with another account",
|
||||
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
|
||||
"member_not_found": "Member not found. Contact your team owner.",
|
||||
"member_has_invite": "This email ID already has an invite. Contact your workspace owner.",
|
||||
"member_not_found": "Member not found. Contact your workspace owner.",
|
||||
"member_removed": "User removed",
|
||||
"member_role_updated": "User roles updated",
|
||||
"members": "Members",
|
||||
"more_members": "+{count} more",
|
||||
"name_length_insufficient": "Team name should be at least 6 characters long",
|
||||
"name_updated": "Team name updated",
|
||||
"new": "New Team",
|
||||
"new_created": "New team created",
|
||||
"new_name": "My New Team",
|
||||
"no_access": "You do not have edit access to this team",
|
||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
||||
"name_length_insufficient": "Workspace name should be at least 6 characters long",
|
||||
"name_updated": "Workspace name updated",
|
||||
"new": "New Workspace",
|
||||
"new_created": "New workspace created",
|
||||
"new_name": "My New Workspace",
|
||||
"no_access": "You do not have edit access to this workspace",
|
||||
"no_invite_found": "Invitation not found. Contact your workspace owner.",
|
||||
"no_request_found": "Request not found.",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
|
||||
"not_found": "Workspace not found. Contact your workspace owner.",
|
||||
"not_valid_viewer": "You are not a valid viewer. Contact your workspace owner.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"pending_invites": "Pending invites",
|
||||
"permissions": "Permissions",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "Team saved",
|
||||
"select_a_team": "Select a team",
|
||||
"saved": "Workspace saved",
|
||||
"select_a_team": "Select a workspace",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Teams",
|
||||
"title": "Workspaces",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace.",
|
||||
"search_title": "Team Requests"
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
@@ -976,8 +1024,14 @@
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"personal": "Personal Workspace",
|
||||
"other_workspaces": "My Workspaces",
|
||||
"team": "Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"site_protection": {
|
||||
"login_to_continue": "Login to continue",
|
||||
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance.",
|
||||
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"no": "No",
|
||||
"open_workspace": "Abrir espacio de trabajo",
|
||||
"paste": "Pegar",
|
||||
"prettify": "Embellecer",
|
||||
"properties": "Properties",
|
||||
"prettify": "Formatear",
|
||||
"properties": "Propiedades",
|
||||
"remove": "Eliminar",
|
||||
"rename": "Rename",
|
||||
"restore": "Restaurar",
|
||||
@@ -63,7 +63,7 @@
|
||||
"contact_us": "Contáctanos",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Copiar",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_interface_type": "Copiar tipo de interfaz",
|
||||
"copy_user_id": "Copiar token de autenticación de usuario",
|
||||
"developer_option": "Opciones para desarrolladores",
|
||||
"developer_option_description": "Herramientas para desarrolladores que ayudan en el desarrollo y mantenimiento de Hoppscotch.",
|
||||
@@ -80,14 +80,14 @@
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "Se ha encontrado una nueva versión. Recarga la página para usarla.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Options",
|
||||
"options": "Opciones",
|
||||
"proxy_privacy_policy": "Política de privacidad de proxy",
|
||||
"reload": "Recargar",
|
||||
"search": "Buscar",
|
||||
"share": "Compartir",
|
||||
"shortcuts": "Atajos",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "Síguenos en redes sociales para estar al día de las últimas noticias, actualizaciones y lanzamientos.",
|
||||
"social_links": "Redes sociales",
|
||||
"spotlight": "Destacar",
|
||||
"status": "Estado",
|
||||
"status_description": "Comprobar el estado del sitio web",
|
||||
@@ -119,27 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generar token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"graphql_headers": "Las cabeceras de autorización se envían como parte de la carga útil de connection_init",
|
||||
"include_in_url": "Incluir en la URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"inherited_from": "Heredado {auth} de colección padre {collection} ",
|
||||
"learn": "Aprender",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
"redirect_auth_server_returned_error": "El servidor de autenticación ha devuelto un estado de error",
|
||||
"redirect_auth_token_request_failed": "Fallo en la solicitud de token de autentificación",
|
||||
"redirect_auth_token_request_invalid_response": "Respuesta no válida del punto final de Token al solicitar un token de autentificación",
|
||||
"redirect_invalid_state": "Valor de estado no válido presente en la redirección",
|
||||
"redirect_no_auth_code": "No hay código de autorización en la redirección",
|
||||
"redirect_no_client_id": "No se ha definido el ID de cliente",
|
||||
"redirect_no_client_secret": "No se ha definido ningún ID secreto de cliente",
|
||||
"redirect_no_code_verifier": "No se ha definido ningún verificador de códigos",
|
||||
"redirect_no_token_endpoint": "No se ha definido ningún punto final de token",
|
||||
"something_went_wrong_on_oauth_redirect": "Algo ha ido mal durante la redirección OAuth",
|
||||
"something_went_wrong_on_token_generation": "Algo salió mal en la generación del token",
|
||||
"token_generation_oidc_discovery_failed": "Fallo en la generación del token: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pasar por",
|
||||
"password": "Contraseña",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"save_to_inherit": "Por favor, guarda esta solicitud en cualquier colección para heredar la autorización",
|
||||
"token": "Token",
|
||||
"type": "Tipo de autorización",
|
||||
"username": "Nombre de usuario"
|
||||
@@ -148,7 +148,7 @@
|
||||
"created": "Colección creada",
|
||||
"different_parent": "No se puede reordenar la colección con un padre diferente",
|
||||
"edit": "Editar colección",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"import_or_create": "Importar o crear una colección",
|
||||
"invalid_name": "Proporciona un nombre válido para la colección.",
|
||||
"invalid_root_move": "La colección ya está en la raíz",
|
||||
"moved": "Movido con éxito",
|
||||
@@ -157,20 +157,20 @@
|
||||
"name_length_insufficient": "El nombre de la colección debe tener al menos 3 caracteres",
|
||||
"new": "Nueva colección",
|
||||
"order_changed": "Orden de colección actualizada",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"properties": "Propiedades de la colección",
|
||||
"properties_updated": "Propiedades de la colección actualizadas",
|
||||
"renamed": "Colección renombrada",
|
||||
"request_in_use": "Solicitud en uso",
|
||||
"save_as": "Guardar como",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "Guardar en la colección",
|
||||
"select": "Seleccionar colección",
|
||||
"select_location": "Seleccionar ubicación",
|
||||
"select_team": "Seleccionar equipo",
|
||||
"team_collections": "Colecciones de equipos"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"close_unsaved_tab": "¿Seguro que quieres cerrar esta pestaña?",
|
||||
"close_unsaved_tabs": "¿Estás seguro de que quieres cerrar todas las pestañas? {count} pestañas no guardadas se perderán.",
|
||||
"exit_team": "¿Estás seguro de que quieres dejar este equipo?",
|
||||
"logout": "¿Estás seguro de que deseas cerrar la sesión?",
|
||||
"remove_collection": "¿Estás seguro de que deseas eliminar esta colección de forma permanente?",
|
||||
@@ -178,7 +178,7 @@
|
||||
"remove_folder": "¿Estás seguro de que deseas eliminar esta carpeta de forma permanente?",
|
||||
"remove_history": "¿Estás seguro de que deseas eliminar todo el historial de forma permanente?",
|
||||
"remove_request": "¿Estás seguro de que deseas eliminar esta solicitud de forma permanente?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_shared_request": "¿Estás seguro de que quieres eliminar definitivamente esta solicitud compartida?",
|
||||
"remove_team": "¿Estás seguro de que deseas eliminar este equipo?",
|
||||
"remove_telemetry": "¿Estás seguro de que deseas darse de baja de la telemetría?",
|
||||
"request_change": "¿Estás seguro de que deseas descartar la solicitud actual, los cambios no guardados se perderán.",
|
||||
@@ -186,35 +186,35 @@
|
||||
"sync": "¿Estás seguro de que deseas sincronizar este espacio de trabajo?"
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "Añadir a parámetros",
|
||||
"open_request_in_new_tab": "Abrir solicitud en una nueva pestaña",
|
||||
"set_environment_variable": "Establecer como variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
"cookie_expires": "Expira en",
|
||||
"cookie_name": "Nombre",
|
||||
"cookie_path": "Ruta",
|
||||
"cookie_string": "Cookies",
|
||||
"cookie_value": "Valor",
|
||||
"empty_domain": "Dominio vacio",
|
||||
"empty_domains": "No hay dominios",
|
||||
"enter_cookie_string": "Introducir cookies",
|
||||
"interceptor_no_support": "El interceptor seleccionado actualmente no admite cookies. Seleccione otro interceptor e inténtelo de nuevo.",
|
||||
"managed_tab": "Gestionado",
|
||||
"new_domain_name": "Nuevo nombre de dominio",
|
||||
"no_cookies_in_domain": "No hay cookies para este dominio",
|
||||
"raw_tab": "Sin procesar",
|
||||
"set": "Establecer una cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "Encabezado {count}",
|
||||
"message": "Mensaje {count}",
|
||||
"parameter": "Parámetro {count}",
|
||||
"protocol": "Protocolo {count}",
|
||||
"value": "Valor {cuenta}",
|
||||
"variable": "Variable {count}"
|
||||
"header": "{count} encabezado(s)",
|
||||
"message": "{count} mensaje(s)",
|
||||
"parameter": "{count} parámetro(s)",
|
||||
"protocol": "{count} protocolo(s)",
|
||||
"value": "{cuenta} valor(es)",
|
||||
"variable": "{count} variable(es)"
|
||||
},
|
||||
"documentation": {
|
||||
"generate": "Generar documentación",
|
||||
@@ -223,26 +223,26 @@
|
||||
"empty": {
|
||||
"authorization": "Esta solicitud no utiliza ninguna autorización.",
|
||||
"body": "Esta solicitud no tiene cuerpo",
|
||||
"collection": "La colección está vacía",
|
||||
"collections": "Las colecciones están vacías",
|
||||
"collection": "Colección vacía",
|
||||
"collections": "No hay colecciones",
|
||||
"documentation": "Conectarse a un punto final de GraphQL para ver la documentación",
|
||||
"endpoint": "El punto final no puede estar vacío",
|
||||
"environments": "Los entornos están vacíos",
|
||||
"folder": "La carpeta está vacía",
|
||||
"environments": "No hay entornos",
|
||||
"folder": "Carpeta vacía",
|
||||
"headers": "Esta solicitud no tiene encabezados",
|
||||
"history": "El historial está vacío",
|
||||
"invites": "La lista de invitados está vacía",
|
||||
"members": "El equipo está vacío",
|
||||
"history": "No hay historial",
|
||||
"invites": "Lista de invitados vacía",
|
||||
"members": "No hay miembros en el equipo",
|
||||
"parameters": "Esta solicitud no tiene ningún parámetro",
|
||||
"pending_invites": "No hay invitaciones pendientes para este equipo",
|
||||
"profile": "Iniciar sesión para ver tu perfil",
|
||||
"protocols": "Los protocolos están vacíos",
|
||||
"protocols": "No hay protocolos",
|
||||
"schema": "Conectarse a un punto final de GraphQL",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"shared_requests": "No hay solicitudes compartidas",
|
||||
"shared_requests_logout": "Iniciar sesión para ver sus solicitudes compartidas o crear una nueva",
|
||||
"subscription": "No hay suscripciones",
|
||||
"team_name": "Nombre del equipo vacío",
|
||||
"teams": "Los equipos están vacíos",
|
||||
"teams": "No hay equipos",
|
||||
"tests": "No hay pruebas para esta solicitud",
|
||||
"shortcodes": "Aún no se han creado Shortcodes"
|
||||
},
|
||||
@@ -250,41 +250,41 @@
|
||||
"add_to_global": "Añadir a Global",
|
||||
"added": "Adición al entorno",
|
||||
"create_new": "Crear un nuevo entorno",
|
||||
"created": "Environment created",
|
||||
"created": "Entorno creado",
|
||||
"deleted": "Eliminar el entorno",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "Entorno duplicado",
|
||||
"edit": "Editar entorno",
|
||||
"empty_variables": "No variables",
|
||||
"empty_variables": "No hay variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"global_variables": "Variables globales",
|
||||
"import_or_create": "Importar o crear un entorno",
|
||||
"invalid_name": "Proporciona un nombre válido para el entorno.",
|
||||
"list": "Environment variables",
|
||||
"list": "Variables de entorno",
|
||||
"my_environments": "Mis entornos",
|
||||
"name": "Name",
|
||||
"name": "Nombre",
|
||||
"nested_overflow": "las variables de entorno anidadas están limitadas a 10 niveles",
|
||||
"new": "Nuevo entorno",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_active_environment": "Ningún entorno activo",
|
||||
"no_environment": "Sin entorno",
|
||||
"no_environment_description": "No se ha seleccionado ningún entorno. Elije qué hacer con las siguientes variables.",
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"quick_peek": "Vistazo rápido al entorno",
|
||||
"replace_with_variable": "Sustituir por variable",
|
||||
"scope": "Ámbito",
|
||||
"select": "Seleccionar entorno",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"set": "Establecer entorno",
|
||||
"set_as_environment": "Establecer como entorno",
|
||||
"team_environments": "Entornos de trabajo en equipo",
|
||||
"title": "Entornos",
|
||||
"updated": "Entorno actualizado",
|
||||
"value": "Value",
|
||||
"value": "Valor",
|
||||
"variable": "Variable",
|
||||
"variable_list": "Lista de variables"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"authproviders_load_error": "No se han podido cargar los proveedores de autenticación",
|
||||
"browser_support_sse": "Este navegador no parece ser compatible con los eventos enviados por el servidor.",
|
||||
"check_console_details": "Consulta el registro de la consola para obtener más detalles.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"check_how_to_add_origin": "Comprueba cómo puede añadir un origen",
|
||||
"curl_invalid_format": "cURL no está formateado correctamente",
|
||||
"danger_zone": "Zona de peligro",
|
||||
"delete_account": "Tu cuenta es actualmente propietaria en estos equipos:",
|
||||
@@ -300,13 +300,13 @@
|
||||
"json_prettify_invalid_body": "No se puede aplicar embellecedor a un cuerpo inválido, resuelve errores de sintaxis json y vuelve a intentarlo",
|
||||
"network_error": "Parece que hay un error de red. Por favor, inténtalo de nuevo.",
|
||||
"network_fail": "No se pudo enviar la solicitud",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_collections_to_export": "No hay colecciones para exportar. Crea una colección para empezar.",
|
||||
"no_duration": "Sin duración",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_environments_to_export": "No hay entornos para exportar. Por favor, crea un entorno para empezar.",
|
||||
"no_results_found": "No se han encontrado coincidencias",
|
||||
"page_not_found": "No se ha podido encontrar esta página",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"proxy_error": "Error de proxy",
|
||||
"script_fail": "No se pudo ejecutar el script de solicitud previa",
|
||||
"something_went_wrong": "Algo salió mal",
|
||||
"test_script_fail": "No se ha podido ejecutar la secuencia de comandos posterior a la solicitud"
|
||||
@@ -314,15 +314,15 @@
|
||||
"export": {
|
||||
"as_json": "Exportar como JSON",
|
||||
"create_secret_gist": "Crear un Gist secreto",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"failed": "Algo ha ido mal al exportar",
|
||||
"gist_created": "Gist creado",
|
||||
"require_github": "Iniciar sesión con GitHub para crear un Gist secreto",
|
||||
"title": "Exportar"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
"all": "Todos",
|
||||
"none": "Ninguno",
|
||||
"starred": "Destacado"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Carpeta creada",
|
||||
@@ -333,16 +333,16 @@
|
||||
"renamed": "Carpeta renombrada"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "¿Deseas conectarte con el endpoint GraphQL más reciente?",
|
||||
"connection_switch_new_url": "Al cambiar a una pestaña se desconectará de la conexión GraphQL activa. La nueva URL de conexión es",
|
||||
"connection_switch_url": "Estás conectado a un endpoint GraphQL cuya URL de conexión es",
|
||||
"mutations": "Mutaciones",
|
||||
"schema": "Esquema",
|
||||
"subscriptions": "Suscripciones",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "Cambiar conexión"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
"title": "Colecciones de GraphQL"
|
||||
},
|
||||
"group": {
|
||||
"time": "Tiempo",
|
||||
@@ -355,8 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la solicitud.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"collection_properties_authorization": " Esta autorización se establecerá para cada solicitud de esta colección.",
|
||||
"collection_properties_header": "Este encabezado se establecerá para cada solicitud de esta colección.",
|
||||
"generate_documentation_first": "Generar la documentación primero",
|
||||
"network_fail": "No se puede acceder a la API. Comprueba tu conexión de red y vuelve a intentarlo.",
|
||||
"offline": "Parece estar desconectado. Es posible que los datos de este espacio de trabajo no estén actualizados.",
|
||||
@@ -376,10 +376,10 @@
|
||||
"import": {
|
||||
"collections": "Importar colecciones",
|
||||
"curl": "Importar cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"environments_from_gist": "Importar desde Gist",
|
||||
"environments_from_gist_description": "Importar entornos Hoppscotch desde Gist",
|
||||
"failed": "Importación fallida",
|
||||
"from_file": "Import from File",
|
||||
"from_file": "Importar desde archivo",
|
||||
"from_gist": "Importar desde Gist",
|
||||
"from_gist_description": "Importar desde URL de Gist",
|
||||
"from_insomnia": "Importar desde Insomnia",
|
||||
@@ -394,41 +394,41 @@
|
||||
"from_postman_description": "Importar desde una colección de Postman",
|
||||
"from_url": "Importar desde una URL",
|
||||
"gist_url": "Introduce la URL de Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"gql_collections_from_gist_description": "Importar colecciones GraphQL desde Gist",
|
||||
"hoppscotch_environment": "Entorno de Hoppscotch",
|
||||
"hoppscotch_environment_description": "Importar archivo JSON del entorno de Hoppscotch",
|
||||
"import_from_url_invalid_fetch": "No se han podido obtener datos de la url",
|
||||
"import_from_url_invalid_file_format": "Error al importar colecciones",
|
||||
"import_from_url_invalid_type": "Tipo no admitido. Los valores aceptados son \"hoppscotch\", \"openapi\", \"postman\", \"insomnia\".",
|
||||
"import_from_url_success": "Colecciones Importadas",
|
||||
"insomnia_environment_description": "Importar el entorno de Insomnia desde un archivo JSON/YAML",
|
||||
"json_description": "Importar colecciones desde un archivo JSON de colecciones de Hoppscotch",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"postman_environment": "Entorno de Postman",
|
||||
"postman_environment_description": "Importar entorno de Postman desde un archivo JSON",
|
||||
"title": "Importar"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "Inspeccionar posibles errores",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "Añadir al Entorno",
|
||||
"not_found": "No se ha encontrado la variable de entorno \"{environment}\"."
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "El navegador no permite que Hoppscotch establezca el encabezado Cookie. Mientras trabajamos en la aplicación de escritorio de Hoppscotch (próximamente), por favor utilice el encabezado de autorización en su lugar."
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "Compruebe tus credenciales de autenticación.",
|
||||
"404_error": "Compruebe la URL de su solicitud y el tipo de método.",
|
||||
"cors_error": "Por favor, comprueba tu configuración de Compartición de Recursos \"Cross-Origin\".",
|
||||
"default_error": "Por favor, comprueba tu solicitud.",
|
||||
"network_error": "Comprueba tu conexión de red."
|
||||
},
|
||||
"title": "Inspector",
|
||||
"title": "Inspeccionador",
|
||||
"url": {
|
||||
"extension_not_installed": "Extension not installed.",
|
||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "Extensión no instalada.",
|
||||
"extension_unknown_origin": "Asegúrate de haber agregado el origen del punto final de la API a la lista de Extensiones del Navegador Hoppscotch.",
|
||||
"extention_enable_action": "Activar la extensión del navegador",
|
||||
"extention_not_enabled": "Extensión no habilitada."
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -442,10 +442,10 @@
|
||||
"close_unsaved_tab": "Tienes cambios sin guardar",
|
||||
"collections": "Colecciones",
|
||||
"confirm": "Confirmar",
|
||||
"customize_request": "Customize Request",
|
||||
"customize_request": "Personalizar solicitud",
|
||||
"edit_request": "Editar solicitud",
|
||||
"import_export": "Importación y exportación",
|
||||
"share_request": "Share Request"
|
||||
"share_request": "Compartir solicitud"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "Ya estás suscrito a este tema.",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Ajustes de la aplicación",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"default_hopp_displayname": "Usuario anónimo",
|
||||
"editor": "Editor",
|
||||
"editor_description": "Los editores pueden añadir, editar y eliminar solicitudes.",
|
||||
"email_verification_mail": "Se ha enviado un correo electrónico de verificación a tu dirección de correo electrónico. Haz clic en el enlace para verificar tu dirección de correo electrónico.",
|
||||
@@ -526,12 +526,12 @@
|
||||
"enter_curl": "Ingrese cURL",
|
||||
"generate_code": "Generar código",
|
||||
"generated_code": "Código generado",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"go_to_authorization_tab": "Ir a la pestaña Autorización",
|
||||
"go_to_body_tab": "Ir a la pestaña de cuerpo",
|
||||
"header_list": "Lista de encabezados",
|
||||
"invalid_name": "Proporciona un nombre para la solicitud.",
|
||||
"method": "Método",
|
||||
"moved": "Request moved",
|
||||
"moved": "Solicitud movida",
|
||||
"name": "Nombre de solicitud",
|
||||
"new": "Nueva solicitud",
|
||||
"order_changed": "Orden de solicitudes actualizadas",
|
||||
@@ -543,8 +543,8 @@
|
||||
"path": "Ruta",
|
||||
"payload": "Carga útil",
|
||||
"query": "Consulta",
|
||||
"raw_body": "Cuerpo de solicitud sin procesar",
|
||||
"rename": "Rename Request",
|
||||
"raw_body": "cuerpo sin procesar",
|
||||
"rename": "Renombrar solicitud",
|
||||
"renamed": "Solicitud renombrada",
|
||||
"run": "Ejecutar",
|
||||
"save": "Guardar",
|
||||
@@ -552,8 +552,8 @@
|
||||
"saved": "Solicitud guardada",
|
||||
"share": "Compartir",
|
||||
"share_description": "Comparte Hoppscotch con tus amigos",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"share_request": "Compartir solicitud",
|
||||
"stop": "Detener",
|
||||
"title": "Solicitud",
|
||||
"type": "Tipo de solicitud",
|
||||
"url": "URL",
|
||||
@@ -571,7 +571,7 @@
|
||||
"json": "JSON",
|
||||
"pdf": "PDF",
|
||||
"preview_html": "Vista previa de HTML",
|
||||
"raw": "Crudo",
|
||||
"raw": "Sin procesar",
|
||||
"size": "Tamaño",
|
||||
"status": "Estado",
|
||||
"time": "Tiempo",
|
||||
@@ -635,29 +635,29 @@
|
||||
"verify_email": "Verificar correo electrónico"
|
||||
},
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"button": "Botón",
|
||||
"button_info": "Crea un botón \"Ejecutar en Hoppscotch\" para tu página web, blog o un README.",
|
||||
"copy_html": "Copiar HTML",
|
||||
"copy_link": "Copiar enlace",
|
||||
"copy_markdown": "Copiar Markdown",
|
||||
"creating_widget": "Crear widget",
|
||||
"customize": "Personalizar",
|
||||
"deleted": "Solicitud compartida eliminada",
|
||||
"description": "Selecciona un widget, puedes cambiarlo y personalizarlo más tarde",
|
||||
"embed": "Incrustar",
|
||||
"embed_info": "Añada un mini \"Hoppscotch API Playground\" a tu sitio web, blog o documentación.",
|
||||
"link": "Enlace",
|
||||
"link_info": "Crea un enlace compartible para compartirlo con cualquier persona en Internet con acceso de visualización.",
|
||||
"modified": "Solicitud compartida modificada",
|
||||
"not_found": "Solicitud compartida no encontrada",
|
||||
"open_new_tab": "Abrir en una nueva pestaña",
|
||||
"preview": "Vista previa",
|
||||
"run_in_hoppscotch": "Correr en Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"system": "Sistema",
|
||||
"title": "Tema"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -684,8 +684,8 @@
|
||||
"title": "Navegación"
|
||||
},
|
||||
"others": {
|
||||
"prettify": "Prettify Editor's Content",
|
||||
"title": "Others"
|
||||
"prettify": "Formatear el contenido del editor",
|
||||
"title": "Otros"
|
||||
},
|
||||
"request": {
|
||||
"delete_method": "Seleccionar método DELETE",
|
||||
@@ -697,13 +697,13 @@
|
||||
"post_method": "Seleccionar método POST",
|
||||
"previous_method": "Seleccionar método anterior",
|
||||
"put_method": "Seleccionar método PUT",
|
||||
"rename": "Rename Request",
|
||||
"rename": "Renombrar solicitud",
|
||||
"reset_request": "Solicitud de reinicio",
|
||||
"save_request": "Save Request",
|
||||
"save_request": "Guardar solicitud",
|
||||
"save_to_collections": "Guardar en colecciones",
|
||||
"send_request": "Enviar solicitud",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"share_request": "Compartir solicitud",
|
||||
"show_code": "Generar fragmento de código",
|
||||
"title": "Solicitud",
|
||||
"copy_request_link": "Copiar enlace de solicitud"
|
||||
},
|
||||
@@ -722,95 +722,95 @@
|
||||
},
|
||||
"show": {
|
||||
"code": "Mostrar código",
|
||||
"collection": "Expand Collection Panel",
|
||||
"collection": "Ampliar el panel de colecciones",
|
||||
"more": "Mostrar más",
|
||||
"sidebar": "Mostrar barra lateral"
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Comunicación",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"connection_not_authorized": "Esta conexión SocketIO no utiliza ningún tipo de autenticación.",
|
||||
"event_name": "Nombre del evento",
|
||||
"events": "Eventos",
|
||||
"log": "Registro",
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "Cambiar idioma",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"title": "Environments"
|
||||
"delete": "Borrar el entorno actual",
|
||||
"duplicate": "Duplicar el entorno actual",
|
||||
"duplicate_global": "Entorno global duplicado",
|
||||
"edit": "Editar el entorno actual",
|
||||
"edit_global": "Editar el entorno global",
|
||||
"new": "Crear un nuevo entorno",
|
||||
"new_variable": "Crear una nueva variable de entorno",
|
||||
"title": "Entornos"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"chat": "Chatear con el servicio de asistencia",
|
||||
"help_menu": "Ayuda y asistencia",
|
||||
"open_docs": "Leer la documentación",
|
||||
"open_github": "Abrir repositorio de GitHub",
|
||||
"open_keybindings": "Atajos de teclado",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "Conectarse al servidor",
|
||||
"disconnect": "Desconectarse del servidor"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "Invita a tus amigos a Hoppscotch",
|
||||
"title": "Varios"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "Guardar como nueva solicitud",
|
||||
"select_method": "Seleccionar método",
|
||||
"switch_to": "Cambiar a",
|
||||
"tab_authorization": "Pestaña de autorización",
|
||||
"tab_body": "Pestaña de cuerpo",
|
||||
"tab_headers": "Pestaña de encabezados",
|
||||
"tab_parameters": "Pestaña de parámetros",
|
||||
"tab_pre_request_script": "Pestaña del script de pre-solicitud",
|
||||
"tab_query": "Pestaña de consulta",
|
||||
"tab_tests": "Pestaña de pruebas",
|
||||
"tab_variables": "Pestaña de variables"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Copiar respuesta",
|
||||
"download": "Descargar la respuesta como archivo",
|
||||
"title": "Respuesta"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"interface": "Interfaz",
|
||||
"theme": "Tema",
|
||||
"user": "Usuario"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "Cambiar Interceptor",
|
||||
"change_language": "Cambiar idioma",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "Negro",
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"system": "Preferencia del sistema"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "Cerrar la pestaña actual",
|
||||
"close_others": "Cerrar todas las demás pestañas",
|
||||
"duplicate": "Duplicar pestaña actual",
|
||||
"new_tab": "Abrir una nueva pestaña",
|
||||
"title": "Pestañas"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "Borrar el equipo actual",
|
||||
"edit": "Editar el equipo actual",
|
||||
"invite": "Invitar al equipo",
|
||||
"new": "Crear un nuevo equipo",
|
||||
"switch_to_personal": "Cambia a tu espacio de trabajo personal",
|
||||
"title": "Equipos"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -825,10 +825,10 @@
|
||||
"connected": "Conectado",
|
||||
"connected_to": "Conectado a {name}",
|
||||
"connecting_to": "Conectando con {name}...",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Error de conexión",
|
||||
"connection_error": "Error de conexión",
|
||||
"connection_failed": "Conexión fallida",
|
||||
"connection_lost": "Conexión perdida",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_interface_to_clipboard": "Copiado tipo de interfaz {language} al portapapeles",
|
||||
"copied_to_clipboard": "Copiado al portapapeles",
|
||||
"deleted": "Eliminado",
|
||||
"deprecated": "OBSOLETO",
|
||||
@@ -836,21 +836,21 @@
|
||||
"disconnected": "Desconectado",
|
||||
"disconnected_from": "Desconectado de {name}",
|
||||
"docs_generated": "Documentación generada",
|
||||
"download_failed": "Download failed",
|
||||
"download_failed": "Descarga fallida",
|
||||
"download_started": "Descarga iniciada",
|
||||
"enabled": "Activado",
|
||||
"file_imported": "Archivo importado",
|
||||
"finished_in": "Terminado en {duration} ms",
|
||||
"hide": "Hide",
|
||||
"finished_in": "Terminado en {duration}ms",
|
||||
"hide": "Ocultar",
|
||||
"history_deleted": "Historial eliminado",
|
||||
"linewrap": "Envolver líneas",
|
||||
"loading": "Cargando...",
|
||||
"message_received": "Mensaje: {mensaje} llegó sobre el tema: {topic}",
|
||||
"message_received": "Mensaje: llegó {message} al: {topic}",
|
||||
"mqtt_subscription_failed": "Algo ha ido mal al suscribirse al tema: {topic}",
|
||||
"none": "Ninguno",
|
||||
"nothing_found": "Nada encontrado para",
|
||||
"published_error": "Algo ha ido mal al publicar el mensaje: {topic} al tema: {message}",
|
||||
"published_message": "Mensaje publicado: {mensaje} al tema: {topic}",
|
||||
"published_error": "Algo ha ido mal al publicar el mensaje: {message} al tema: {topic}",
|
||||
"published_message": "Mensaje publicado: {message} al tema: {topic}",
|
||||
"reconnection_error": "Fallo en la reconexión",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Error al suscribirse al tema: {topic}",
|
||||
@@ -874,12 +874,12 @@
|
||||
"tab": {
|
||||
"authorization": "Autorización",
|
||||
"body": "Cuerpo",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"close": "Cerrar pestaña",
|
||||
"close_others": "Cerrar otras pestañas",
|
||||
"collections": "Colecciones",
|
||||
"documentation": "Documentación",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"environments": "Environments",
|
||||
"duplicate": "Duplicar pestaña",
|
||||
"environments": "Entornos",
|
||||
"headers": "Encabezados",
|
||||
"history": "Historial",
|
||||
"mqtt": "MQTT",
|
||||
@@ -888,7 +888,7 @@
|
||||
"queries": "Consultas",
|
||||
"query": "Consulta",
|
||||
"schema": "Esquema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"shared_requests": "Solicitudes compartidas",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Pruebas",
|
||||
@@ -905,7 +905,7 @@
|
||||
"email_do_not_match": "El correo electrónico no coincide con los datos de tu cuenta. Ponte en contacto con el propietario de tu equipo.",
|
||||
"exit": "Salir del equipo",
|
||||
"exit_disabled": "Solo el propietario puede salir del equipo",
|
||||
"failed_invites": "Failed invites",
|
||||
"failed_invites": "Invitaciones fallidas",
|
||||
"invalid_coll_id": "Identificador de colección no válido",
|
||||
"invalid_email_format": "El formato de correo electrónico no es válido",
|
||||
"invalid_id": "Identificador de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
|
||||
@@ -915,7 +915,7 @@
|
||||
"invite": "Invitar",
|
||||
"invite_more": "Invitar a más",
|
||||
"invite_tooltip": "Invite a personas a este espacio de trabajo",
|
||||
"invited_to_team": "{owner} te ha invitado a unirte al {tema}",
|
||||
"invited_to_team": "{owner} te ha invitado al equipo {team}",
|
||||
"join": "Invitación aceptada",
|
||||
"join_beta": "Únete al programa beta para acceder a los equipos.",
|
||||
"join_team": "Entrar a {team}",
|
||||
@@ -930,7 +930,7 @@
|
||||
"member_removed": "Usuario eliminado",
|
||||
"member_role_updated": "Funciones de usuario actualizadas",
|
||||
"members": "Miembros",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "+{count} más",
|
||||
"name_length_insufficient": "El nombre del equipo debe tener al menos 6 caracteres",
|
||||
"name_updated": "Nombre de equipo actualizado",
|
||||
"new": "Nuevo equipo",
|
||||
@@ -944,13 +944,13 @@
|
||||
"parent_coll_move": "No se puede mover la colección a una colección hija",
|
||||
"pending_invites": "Invitaciones pendientes",
|
||||
"permissions": "Permisos",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "Mismo objetivo y destino",
|
||||
"saved": "Equipo guardado",
|
||||
"select_a_team": "Seleccionar un equipo",
|
||||
"success_invites": "Success invites",
|
||||
"success_invites": "Invitaciones con éxito",
|
||||
"title": "Equipos",
|
||||
"we_sent_invite_link": "¡Hemos enviado un enlace de invitación a todos los invitados!",
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen tu bandeja de entrada. Haz clic en el enlace para unirse al equipo."
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen su bandeja de entrada. Tienen que hacer clic en el enlace para unirse al equipo."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Entorno eliminado",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"add": "Ajouter",
|
||||
"autoscroll": "Auto-scroll",
|
||||
"cancel": "Annuler",
|
||||
"choose_file": "Choisir un fichier",
|
||||
"clear": "Effacer",
|
||||
"clear_all": "Tout effacer",
|
||||
"clear_history": "Clear all History",
|
||||
"close": "Close",
|
||||
"clear_history": "Effacer tout l'historique",
|
||||
"close": "Fermer",
|
||||
"connect": "Connecter",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Connexion",
|
||||
"copy": "Copier",
|
||||
"create": "Create",
|
||||
"delete": "Supprimer",
|
||||
@@ -22,8 +22,8 @@
|
||||
"edit": "Éditer",
|
||||
"filter": "Filter",
|
||||
"go_back": "Retour",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Avancer",
|
||||
"group_by": "Grouper par",
|
||||
"label": "Étiqueter",
|
||||
"learn_more": "En savoir plus",
|
||||
"less": "Moins",
|
||||
@@ -35,16 +35,16 @@
|
||||
"prettify": "Formater",
|
||||
"properties": "Properties",
|
||||
"remove": "Supprimer",
|
||||
"rename": "Rename",
|
||||
"rename": "Renommer",
|
||||
"restore": "Restaurer",
|
||||
"save": "Sauvegarder",
|
||||
"scroll_to_bottom": "Scroll to bottom",
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"scroll_to_bottom": "Défiler vers le bas",
|
||||
"scroll_to_top": "Défiler vers le haut",
|
||||
"search": "Chercher",
|
||||
"send": "Envoyer",
|
||||
"share": "Share",
|
||||
"start": "Démarrer",
|
||||
"starting": "Starting",
|
||||
"starting": "Démarrage",
|
||||
"stop": "Arrêter",
|
||||
"to_close": "pour fermer",
|
||||
"to_navigate": "pour naviguer",
|
||||
@@ -86,8 +86,8 @@
|
||||
"search": "Chercher",
|
||||
"share": "Partager",
|
||||
"shortcuts": "Raccourcis",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "Suivez-nous sur les médias sociaux pour rester informé des dernières nouvelles, mises à jour et communiqués.",
|
||||
"social_links": "Liens sociaux",
|
||||
"spotlight": "Projecteur",
|
||||
"status": "Statut",
|
||||
"status_description": "Vérifier l'état du site web",
|
||||
@@ -95,7 +95,7 @@
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "Tapez une commande ou recherchez…",
|
||||
"we_use_cookies": "Nous utilisons des cookies",
|
||||
"whats_new": "Quoi de neuf?",
|
||||
"whats_new": "Quoi de neuf ?",
|
||||
"wiki": "Wiki"
|
||||
},
|
||||
"auth": {
|
||||
@@ -119,39 +119,38 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Générer un jeton",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"graphql_headers": "Les en-têtes d'autorisation sont envoyés en tant que partie de la charge utile de connection_init.",
|
||||
"include_in_url": "Inclure dans l'URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Apprendre comment",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Mot de passe",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Jeton",
|
||||
"type": "Type d'autorisation",
|
||||
"username": "Nom d'utilisateur"
|
||||
"username": "Nom d'utilisateur",
|
||||
"oauth": {
|
||||
"something_went_wrong_on_token_generation": "Un problème s'est produit lors de la génération des jetons",
|
||||
"redirect_auth_server_returned_error": "Le serveur d'authentification a renvoyé un état d'erreur",
|
||||
"redirect_no_auth_code": "Pas de code d'autorisation dans la redirection",
|
||||
"redirect_invalid_state": "Valeur d'état non valide présente dans la redirection",
|
||||
"redirect_no_token_endpoint": "Aucun point de terminaison de jeton n'est défini",
|
||||
"redirect_no_client_id": "Pas d'ID client défini",
|
||||
"redirect_no_client_secret": "Pas de secret client défini",
|
||||
"redirect_no_code_verifier": "Pas de vérificateur de code défini",
|
||||
"redirect_auth_token_request_failed": "La demande d'obtention du jeton d'authentification a échoué",
|
||||
"redirect_auth_token_request_invalid_response": "Réponse invalide du point de terminaison Token lors de la demande d'un jeton d'authentification",
|
||||
"something_went_wrong_on_oauth_redirect": "Quelque chose s'est mal passé lors de la redirection OAuth"
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"created": "Collection créée",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "Impossible de réorganiser une collection dont le parent est différent",
|
||||
"edit": "Modifier la collection",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"import_or_create": "Importer ou créer une collection",
|
||||
"invalid_name": "Veuillez fournir un nom valide pour la collection",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "Collection déjà présente dans la racine",
|
||||
"moved": "Déplacement réussi",
|
||||
"my_collections": "Mes collections",
|
||||
"name": "Ma nouvelle collection",
|
||||
"name_length_insufficient": "Le nom de la collection doit comporter au moins 3 caractères",
|
||||
@@ -162,16 +161,16 @@
|
||||
"renamed": "Collection renommée",
|
||||
"request_in_use": "Demande en cours d'utilisation",
|
||||
"save_as": "Enregistrer sous",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "Enregistrer dans la collection",
|
||||
"select": "Sélectionnez une collection",
|
||||
"select_location": "Sélectionnez l'emplacement",
|
||||
"select_team": "Sélectionnez une équipe",
|
||||
"team_collections": "Collections de l'équipe"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"exit_team": "Are you sure you want to leave this team?",
|
||||
"close_unsaved_tab": "Êtes-vous sûr de vouloir fermer cet onglet ?",
|
||||
"close_unsaved_tabs": "Êtes-vous sûr de vouloir fermer tous les onglets ? {count} onglets non enregistrés seront perdus",
|
||||
"exit_team": "Êtes-vous sûr de vouloir quitter cette équipe ?",
|
||||
"logout": "Êtes-vous sûr de vouloir vous déconnecter?",
|
||||
"remove_collection": "Voulez-vous vraiment supprimer définitivement cette collection ?",
|
||||
"remove_environment": "Voulez-vous vraiment supprimer définitivement cet environnement ?",
|
||||
@@ -181,31 +180,31 @@
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Voulez-vous vraiment supprimer cette équipe ?",
|
||||
"remove_telemetry": "Êtes-vous sûr de vouloir désactiver la télémétrie ?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"request_change": "Êtes-vous sûr de vouloir rejeter la demande en cours ? Les modifications non enregistrées seront perdues.",
|
||||
"save_unsaved_tab": "Souhaitez-vous enregistrer les modifications apportées dans cet onglet ?",
|
||||
"sync": "Voulez-vous vraiment synchroniser cet espace de travail ?"
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "Ajouter aux paramètres",
|
||||
"open_request_in_new_tab": "Ouvrir la demande dans un nouvel onglet",
|
||||
"set_environment_variable": "Définir comme variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
"new_domain_name": "Nouveau nom de domaine",
|
||||
"set": "Définir un cookie",
|
||||
"cookie_string": "Chaîne de caractères de cookie",
|
||||
"enter_cookie_string": "Saisir la chaîne de caractères du cookie",
|
||||
"cookie_name": "Nom",
|
||||
"cookie_value": "Valeur",
|
||||
"cookie_path": "Chemin d'accès",
|
||||
"cookie_expires": "Expiration",
|
||||
"managed_tab": "Gestion",
|
||||
"raw_tab": "Brut",
|
||||
"interceptor_no_support": "L'intercepteur que vous avez sélectionné ne prend pas en charge les cookies. Sélectionnez un autre intercepteur et réessayez.",
|
||||
"empty_domains": "La liste des domaines est vide",
|
||||
"empty_domain": "Le domaine est vide",
|
||||
"no_cookies_in_domain": "Aucun cookie n'est défini pour ce domaine"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
@@ -238,7 +237,7 @@
|
||||
"profile": "Connectez-vous pour voir votre profil",
|
||||
"protocols": "Les protocoles sont vides",
|
||||
"schema": "Se connecter à un point de terminaison GraphQL",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests": "Il n'y a pas de requêtes partagées",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Nom de l'équipe vide",
|
||||
@@ -252,15 +251,15 @@
|
||||
"create_new": "Créer un nouvel environnement",
|
||||
"created": "Environnement créé",
|
||||
"deleted": "Environnement supprimé",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "Environnement dupliqué",
|
||||
"edit": "Modifier l'environnement",
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"global_variables": "Variables globales",
|
||||
"import_or_create": "Importer ou créer un environnement",
|
||||
"invalid_name": "Veuillez fournir un nom valide pour l'environnement",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
"list": "Variables d'environnement",
|
||||
"my_environments": "Mes environnements",
|
||||
"name": "Name",
|
||||
"nested_overflow": "les variables d'environnement imbriquées sont limitées à 10 niveaux",
|
||||
"new": "Nouvel environnement",
|
||||
@@ -284,11 +283,11 @@
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "Ce navigateur ne semble pas prendre en charge les événements envoyés par le serveur.",
|
||||
"check_console_details": "Consultez le journal de la console pour plus de détails.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"check_how_to_add_origin": "Vérifiez comment vous pouvez ajouter une origine",
|
||||
"curl_invalid_format": "cURL n'est pas formaté correctement",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"danger_zone": "Zone de danger",
|
||||
"delete_account": "Votre compte est actuellement propriétaire de ces équipes :",
|
||||
"delete_account_description": "Vous devez vous retirer, transférer la propriété ou supprimer ces équipes avant de pouvoir supprimer votre compte.",
|
||||
"empty_req_name": "Nom de la requête vide",
|
||||
"f12_details": "(F12 pour les détails)",
|
||||
"gql_prettify_invalid_query": "Impossible de formater une requête non valide, résolvez les erreurs de syntaxe de la requête et réessayer",
|
||||
@@ -300,13 +299,13 @@
|
||||
"json_prettify_invalid_body": "Impossible de formater un corps non valide, résolvez les erreurs de syntaxe json et réessayez",
|
||||
"network_error": "Il semble y avoir une erreur de réseau. Veuillez réessayer.",
|
||||
"network_fail": "Impossible d'envoyer la requête",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_collections_to_export": "Aucune collection à exporter. Veuillez créer une collection pour commencer.",
|
||||
"no_duration": "Pas de durée",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_environments_to_export": "Aucun environnement à exporter. Veuillez créer un environnement pour commencer.",
|
||||
"no_results_found": "Aucune correspondance trouvée",
|
||||
"page_not_found": "Cette page n'a pas pu être trouvée",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"please_install_extension": "Veuillez installer l'extension et ajouter l'origine à l'extension.",
|
||||
"proxy_error": "Erreur de proxy",
|
||||
"script_fail": "Impossible d'exécuter le script de pré-requête",
|
||||
"something_went_wrong": "Quelque chose s'est mal passé",
|
||||
"test_script_fail": "Impossible d'exécuter le script post-requête"
|
||||
@@ -320,9 +319,9 @@
|
||||
"title": "Exportation"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
"all": "Tout",
|
||||
"none": "Aucun",
|
||||
"starred": "Étoilé"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Dossier créé",
|
||||
@@ -333,19 +332,19 @@
|
||||
"renamed": "Dossier renommé"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "Voulez-vous vous connecter avec le dernier point de terminaison GraphQL ?",
|
||||
"connection_switch_new_url": "Le passage à un autre onglet vous déconnectera de la connexion GraphQL active. La nouvelle URL de connexion est",
|
||||
"connection_switch_url": "Vous êtes connecté à un point de terminaison GraphQL dont l'URL de connexion est",
|
||||
"mutations": "Mutations",
|
||||
"schema": "Schéma",
|
||||
"subscriptions": "Abonnements",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "Changer de connexion"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Temps",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -376,7 +375,7 @@
|
||||
"import": {
|
||||
"collections": "Importer des collections",
|
||||
"curl": "Importer en cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist": "Importer depuis Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "Échec de l'importation",
|
||||
"from_file": "Import from File",
|
||||
@@ -408,27 +407,27 @@
|
||||
"title": "Importer"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "Inspecter les erreurs possibles",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "Ajouter à l'environnement",
|
||||
"not_found": "La variable d'environnement “{environment}“ n'a pas été trouvée."
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "Le navigateur ne permet pas à Hoppscotch de définir l'en-tête Cookie. Pendant que nous travaillons sur l'application de bureau Hoppscotch (bientôt disponible), veuillez utiliser l'en-tête d'autorisation à la place."
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "Veuillez vérifier vos informations d'authentification.",
|
||||
"404_error": "Veuillez vérifier l'URL de votre demande et le type de méthode.",
|
||||
"cors_error": "Veuillez vérifier la configuration du partage des ressources entre les origines.",
|
||||
"default_error": "Veuillez vérifier votre demande.",
|
||||
"network_error": "Veuillez vérifier votre connexion réseau."
|
||||
},
|
||||
"title": "Inspector",
|
||||
"title": "Inspecteur",
|
||||
"url": {
|
||||
"extension_not_installed": "Extension not installed.",
|
||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "L'extension n'est pas installée.",
|
||||
"extension_unknown_origin": "Assurez-vous d'avoir ajouté l'origine du point de terminaison de l'API à la liste des extensions du navigateur Hoppscotch.",
|
||||
"extention_enable_action": "Activer l'extension du navigateur",
|
||||
"extention_not_enabled": "L'extension n'est pas activée."
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -439,25 +438,25 @@
|
||||
"row": "Disposition horizontale"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Vous avez des modifications non enregistrées",
|
||||
"collections": "Collections",
|
||||
"confirm": "Confirmer",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "Modifier la requête",
|
||||
"import_export": "Importer / Exporter",
|
||||
"share_request": "Share Request"
|
||||
"share_request": "Partager une requête"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"already_subscribed": "Vous êtes déjà abonné à ce sujet.",
|
||||
"clean_session": "Effacer la Session",
|
||||
"clear_input": "Effacer la saisie",
|
||||
"clear_input_on_send": "Effacer la saisie lors de l'envoi",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"color": "Choisir la couleur",
|
||||
"communication": "Communication",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"connection_not_authorized": "Cette connexion MQTT n'utilise pas d'authentification.",
|
||||
"invalid_topic": "Veuillez fournir un sujet pour l'abonnement",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "Infos",
|
||||
"lw_message": "Last-Will Message",
|
||||
@@ -466,7 +465,7 @@
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"message": "Message",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"not_connected": "Veuillez d'abord établir une connexion MQTT.",
|
||||
"publish": "Publier",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -480,7 +479,7 @@
|
||||
"navigation": {
|
||||
"doc": "Documents",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "Profile",
|
||||
"profile": "Profil",
|
||||
"realtime": "Temps réel",
|
||||
"rest": "REST",
|
||||
"settings": "Paramètres"
|
||||
@@ -493,7 +492,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Réglages de l'application",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"default_hopp_displayname": "Utilisateur anonyme",
|
||||
"editor": "Éditeur",
|
||||
"editor_description": "Les éditeurs peuvent ajouter, modifier et supprimer des demandes.",
|
||||
"email_verification_mail": "Un e-mail de vérification a été envoyé à votre adresse e-mail. Veuillez cliquer sur le lien pour vérifier votre adresse électronique.",
|
||||
@@ -526,17 +525,17 @@
|
||||
"enter_curl": "Entrer cURL",
|
||||
"generate_code": "Générer le code",
|
||||
"generated_code": "Code généré",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"go_to_authorization_tab": "Aller à l'autorisation",
|
||||
"header_list": "Liste des en-têtes",
|
||||
"invalid_name": "Veuillez fournir un nom pour la requête",
|
||||
"method": "Méthode",
|
||||
"moved": "Request moved",
|
||||
"name": "Nom de la requête",
|
||||
"new": "Nouvelle requête",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "Demande de commande Mise à jour",
|
||||
"override": "Remplacer",
|
||||
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
|
||||
"override_help": "Définir <xmp>Content-Type</xmp> dans les en-têtes",
|
||||
"overriden": "Remplacé",
|
||||
"parameter_list": "Paramètres de requête",
|
||||
"parameters": "Paramètres",
|
||||
@@ -544,7 +543,7 @@
|
||||
"payload": "Charge utile",
|
||||
"query": "Requête",
|
||||
"raw_body": "Corps de requête brut",
|
||||
"rename": "Rename Request",
|
||||
"rename": "Demande de renommage",
|
||||
"renamed": "Requête renommée",
|
||||
"run": "Lancer",
|
||||
"save": "Sauvegarder",
|
||||
@@ -564,7 +563,7 @@
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corps de réponse",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "Filtrer le corps de la réponse JSON (utilise la syntaxe JSONPath)",
|
||||
"headers": "En-têtes",
|
||||
"html": "HTML",
|
||||
"image": "Image",
|
||||
@@ -576,14 +575,14 @@
|
||||
"status": "Statut",
|
||||
"time": "Temps",
|
||||
"title": "Réponse",
|
||||
"video": "Video",
|
||||
"video": "Vidéo",
|
||||
"waiting_for_connection": "En attente de connexion",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Couleur d'accent",
|
||||
"account": "Compte",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_deleted": "Votre compte a été supprimé",
|
||||
"account_description": "Personnalisez les paramètres de votre compte.",
|
||||
"account_email_description": "Votre adresse e-mail principale.",
|
||||
"account_name_description": "Ceci est votre nom d'affichage.",
|
||||
@@ -592,8 +591,8 @@
|
||||
"black_mode": "Noir",
|
||||
"choose_language": "Choisissez la langue",
|
||||
"dark_mode": "Sombre",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Supprimer le compte",
|
||||
"delete_account_description": "Lorsque vous supprimez votre compte, toutes vos données sont définitivement effacées. Cette action ne peut être annulée.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Expériences",
|
||||
"experiments_notice": "Il s'agit d'une collection d'expériences sur lesquelles nous travaillons et qui pourraient s'avérer utiles, amusantes, les deux ou aucune. Ils ne sont pas définitifs et peuvent ne pas être stables, donc si quelque chose de trop étrange se produit, ne paniquez pas. Il suffit d'éteindre le truc. Blague à part,",
|
||||
@@ -601,7 +600,7 @@
|
||||
"extension_version": "Version d'extension",
|
||||
"extensions": "Extensions",
|
||||
"extensions_use_toggle": "Utilisez l'extension de navigateur pour envoyer des requêtes (le cas échéant)",
|
||||
"follow": "Follow Us",
|
||||
"follow": "Suivez-nous",
|
||||
"interceptor": "Intercepteur",
|
||||
"interceptor_description": "Middleware entre l'application et les API.",
|
||||
"language": "Langue",
|
||||
@@ -631,7 +630,7 @@
|
||||
"theme_description": "Personnalisez le thème de votre application.",
|
||||
"use_experimental_url_bar": "Utiliser la barre d'URL expérimentale avec mise en évidence de l'environnement",
|
||||
"user": "Utilisateur",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "E-mail vérifié",
|
||||
"verify_email": "Vérifier l'email"
|
||||
},
|
||||
"shared_requests": {
|
||||
@@ -684,20 +683,20 @@
|
||||
"title": "Navigation"
|
||||
},
|
||||
"others": {
|
||||
"prettify": "Prettify Editor's Content",
|
||||
"title": "Others"
|
||||
"prettify": "Améliorer le contenu de l'éditeur",
|
||||
"title": "Autres"
|
||||
},
|
||||
"request": {
|
||||
"delete_method": "Sélectionnez la méthode DELETE",
|
||||
"get_method": "Sélectionnez la méthode GET",
|
||||
"head_method": "Sélectionnez la méthode HEAD",
|
||||
"import_curl": "Import cURL",
|
||||
"import_curl": "Importer cURL",
|
||||
"method": "Méthode",
|
||||
"next_method": "Sélectionnez la méthode suivante",
|
||||
"post_method": "Sélectionnez la méthode POST",
|
||||
"previous_method": "Sélectionnez la méthode précédente",
|
||||
"put_method": "Sélectionnez la méthode PUT",
|
||||
"rename": "Rename Request",
|
||||
"rename": "Demande de renommage",
|
||||
"reset_request": "Réinitialiser la requête",
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "Enregistrer dans les collections",
|
||||
@@ -728,89 +727,89 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Communication",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"connection_not_authorized": "Cette connexion SocketIO n'utilise pas d'authentification.",
|
||||
"event_name": "Nom de l'événement",
|
||||
"events": "Événements",
|
||||
"log": "Infos",
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "Changer de langue",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"delete": "Supprimer l'environnement actuel",
|
||||
"duplicate": "Dupliquer l'environnement actuel",
|
||||
"duplicate_global": "Duplication de l'environnement global",
|
||||
"edit": "Modifier l'environnement actuel",
|
||||
"edit_global": "Modifier l'environnement mondial",
|
||||
"new": "Créer un nouvel environnement",
|
||||
"new_variable": "Créer une nouvelle variable d'environnement",
|
||||
"title": "Environments"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"chat": "Chat avec le support",
|
||||
"help_menu": "Aide et assistance",
|
||||
"open_docs": "Lire la documentation",
|
||||
"open_github": "Ouvrir le dépôt GitHub",
|
||||
"open_keybindings": "Raccourcis clavier",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
"title": "Général"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "Connexion au serveur",
|
||||
"disconnect": "Déconnexion du serveur"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "Invitez vos amis à Hoppscotch",
|
||||
"title": "Divers"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "Sauvegarder comme nouvelle demande",
|
||||
"select_method": "Sélectionner la méthode",
|
||||
"switch_to": "Basculer vers",
|
||||
"tab_authorization": "Onglet Autorisation",
|
||||
"tab_body": "Onglet du corps",
|
||||
"tab_headers": "Onglet En-têtes",
|
||||
"tab_parameters": "Onglet Paramètres",
|
||||
"tab_pre_request_script": "Onglet script de pré-demande",
|
||||
"tab_query": "Onglet Requête",
|
||||
"tab_tests": "Onglet Tests",
|
||||
"tab_variables": "Onglet Variables"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Copier la réponse",
|
||||
"download": "Télécharger la réponse sous forme de fichier",
|
||||
"title": "Réponse"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interceptor": "Intercepteur",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"theme": "Thème",
|
||||
"user": "Utilisateur"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "Changer d'intercepteur",
|
||||
"change_language": "Changer de langue",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "Noir",
|
||||
"dark": "Sombre",
|
||||
"light": "Clair",
|
||||
"system": "Préférence du système"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "Fermer l'onglet actuel",
|
||||
"close_others": "Fermer tous les autres onglets",
|
||||
"duplicate": "Dupliquer l'onglet actuel",
|
||||
"new_tab": "Ouvrir un nouvel onglet",
|
||||
"title": "Onglets"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "Supprimer l'équipe actuelle",
|
||||
"edit": "Modifier l'équipe actuelle",
|
||||
"invite": "Inviter les gens à rejoindre l'équipe",
|
||||
"new": "Créer une nouvelle équipe",
|
||||
"switch_to_personal": "Passez à votre espace de travail personnel",
|
||||
"title": "Les équipes"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -836,12 +835,12 @@
|
||||
"disconnected": "Déconnecté",
|
||||
"disconnected_from": "Déconnecté de {name}",
|
||||
"docs_generated": "Documentation générée",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "Téléchargement commencé",
|
||||
"download_failed": "Téléchargement échoué",
|
||||
"enabled": "Active",
|
||||
"file_imported": "Fichier importé",
|
||||
"finished_in": "Terminé en {duration} ms",
|
||||
"hide": "Hide",
|
||||
"hide": "Cacher",
|
||||
"history_deleted": "Historique supprimé",
|
||||
"linewrap": "Retour à la ligne",
|
||||
"loading": "Chargement...",
|
||||
@@ -852,7 +851,7 @@
|
||||
"published_error": "Quelque chose s'est mal passé lors de la publication du message : {topic} dans le sujet : {message}",
|
||||
"published_message": "Message publié : {message} au sujet : {topic}",
|
||||
"reconnection_error": "Échec de la reconnexion",
|
||||
"show": "Show",
|
||||
"show": "Afficher",
|
||||
"subscribed_failed": "Échec de l'inscription au sujet : {topic}",
|
||||
"subscribed_success": "Inscription réussie au sujet : {topic}",
|
||||
"unsubscribed_failed": "Échec de la désinscription du sujet : {topic}",
|
||||
@@ -861,7 +860,7 @@
|
||||
},
|
||||
"support": {
|
||||
"changelog": "En savoir plus sur les dernières versions",
|
||||
"chat": "Des questions? Discutez avec nous!",
|
||||
"chat": "Des questions ? Discutez avec nous!",
|
||||
"community": "Posez des questions et aidez les autres",
|
||||
"documentation": "En savoir plus sur Hoppscotch",
|
||||
"forum": "Posez des questions et obtenez des réponses",
|
||||
@@ -874,21 +873,21 @@
|
||||
"tab": {
|
||||
"authorization": "Autorisation",
|
||||
"body": "Corps",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"close": "Fermer l'onglet",
|
||||
"close_others": "Fermer les autres onglets",
|
||||
"collections": "Collections",
|
||||
"documentation": "Documentation",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"duplicate": "Dupliquer l'onglet",
|
||||
"environments": "Environments",
|
||||
"headers": "En-têtes",
|
||||
"history": "Histoire",
|
||||
"history": "Historique",
|
||||
"mqtt": "MQTT",
|
||||
"parameters": "Paramètres",
|
||||
"pre_request_script": "Script de pré-requête",
|
||||
"queries": "Requêtes",
|
||||
"query": "Requête",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"shared_requests": "Requêtes partagées",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "ESS",
|
||||
"tests": "Tests",
|
||||
@@ -905,7 +904,6 @@
|
||||
"email_do_not_match": "L'email ne correspond pas aux détails de votre compte. Contactez le propriétaire de votre équipe.",
|
||||
"exit": "Quitter l'équipe",
|
||||
"exit_disabled": "Seul le propriétaire ne peut pas quitter l'équipe",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "Le format de l'e-mail n'est pas valide",
|
||||
"invalid_id": "L'email ne correspond pas aux détails de votre compte. Contactez le propriétaire de votre équipe.",
|
||||
@@ -941,21 +939,22 @@
|
||||
"no_request_found": "Request not found.",
|
||||
"not_found": "Équipe non trouvée. Contactez le propriétaire de votre équipe.",
|
||||
"not_valid_viewer": "Vous n'êtes pas un visionneur valide. Contactez le propriétaire de votre équipe.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "Impossible de déplacer une collection vers une collection enfant",
|
||||
"success_invites": "Les invitations réussites",
|
||||
"pending_invites": "Invitations en attente",
|
||||
"failed_invites": "Échec des invitations",
|
||||
"permissions": "Autorisations",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "Même destinataire et même cible",
|
||||
"saved": "Équipe enregistrée",
|
||||
"select_a_team": "Choisir une équipe",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Équipes",
|
||||
"we_sent_invite_link": "Nous avons envoyé un lien d'invitation à tous les invités !",
|
||||
"we_sent_invite_link_description": "Demandez à tous les invités de vérifier leur boîte de réception. Cliquez sur le lien pour rejoindre l'équipe."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Environment supprimé",
|
||||
"duplicate": "Environment dupliqué",
|
||||
"not_found": "Environment non trouvé"
|
||||
},
|
||||
"test": {
|
||||
"failed": "Test échoué",
|
||||
@@ -975,10 +974,10 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Changer d'espace de travail",
|
||||
"personal": "Mon espace de travail",
|
||||
"team": "Espace de travail de l'équipe",
|
||||
"title": "Espaces de travail"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"add": "Hozzáadás",
|
||||
"autoscroll": "Automatikus görgetés",
|
||||
"cancel": "Mégse",
|
||||
"choose_file": "Válasszon egy fájlt",
|
||||
"clear": "Törlés",
|
||||
"clear_all": "Összes törlése",
|
||||
"clear_history": "Clear all History",
|
||||
"clear_history": "Összes előzmény törlése",
|
||||
"close": "Bezárás",
|
||||
"connect": "Kapcsolódás",
|
||||
"connecting": "Kapcsolódás",
|
||||
"copy": "Másolás",
|
||||
"create": "Create",
|
||||
"create": "Létrehozás",
|
||||
"delete": "Törlés",
|
||||
"disconnect": "Leválasztás",
|
||||
"dismiss": "Eltüntetés",
|
||||
@@ -33,16 +33,16 @@
|
||||
"open_workspace": "Munkaterület megnyitása",
|
||||
"paste": "Beillesztés",
|
||||
"prettify": "Csinosítás",
|
||||
"properties": "Properties",
|
||||
"properties": "Tulajdonságok",
|
||||
"remove": "Eltávolítás",
|
||||
"rename": "Rename",
|
||||
"rename": "Átnevezés",
|
||||
"restore": "Visszaállítás",
|
||||
"save": "Mentés",
|
||||
"scroll_to_bottom": "Görgetés az aljára",
|
||||
"scroll_to_top": "Görgetés a tetejére",
|
||||
"search": "Keresés",
|
||||
"send": "Küldés",
|
||||
"share": "Share",
|
||||
"share": "Megosztás",
|
||||
"start": "Indítás",
|
||||
"starting": "Indítás",
|
||||
"stop": "Leállítás",
|
||||
@@ -61,9 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "Csevegjen velünk",
|
||||
"contact_us": "Lépjen kapcsolatba velünk",
|
||||
"cookies": "Cookies",
|
||||
"cookies": "Sütik",
|
||||
"copy": "Másolás",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_interface_type": "Interface típusának másolása",
|
||||
"copy_user_id": "Felhasználó-hitelesítési token másolása",
|
||||
"developer_option": "Fejlesztői beállítások",
|
||||
"developer_option_description": "Fejlesztői eszközök, amelyek segítenek a Hoppscotch fejlesztésében és karbantartásában.",
|
||||
@@ -79,15 +79,15 @@
|
||||
"keyboard_shortcuts": "Gyorsbillentyűk",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "Új verzió található. Töltse újra az oldalt a frissítéshez.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"open_in_hoppscotch": "Megnyitás Hoppscotch-ban.",
|
||||
"options": "Beállítások",
|
||||
"proxy_privacy_policy": "Proxy adatvédelmi irányelvei",
|
||||
"reload": "Újratöltés",
|
||||
"search": "Keresés",
|
||||
"share": "Megosztás",
|
||||
"shortcuts": "Gyorsbillentyűk",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "Kövess minket a közösségi médiában, hogy ne maradj le a hírekről, frissítésekről és új kiadásokról.",
|
||||
"social_links": "Közösségi média linkek",
|
||||
"spotlight": "Reflektorfény",
|
||||
"status": "Állapot",
|
||||
"status_description": "A weboldal állapotának ellenőrzése",
|
||||
@@ -119,27 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Token előállítása",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"graphql_headers": "Azonosító fejléc connection_init tartalmaként elküldve",
|
||||
"include_in_url": "Felvétel az URL-be",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"inherited_from": "Örökölt a(z) {auth}-tól, a(z) {collection} gyűjteményből ",
|
||||
"learn": "Tudja meg, hogyan",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
"redirect_auth_server_returned_error": "Az Auth szerver hibás állapottal tért vissza",
|
||||
"redirect_auth_token_request_failed": "Kérés az auth token lekéréséhez sikertelen",
|
||||
"redirect_auth_token_request_invalid_response": "Érvénytelen válasz a Token Endpoint-tól, az auth token lekérésekpr",
|
||||
"redirect_invalid_state": "Érvénytelen állapotérték az átirányításban",
|
||||
"redirect_no_auth_code": "Nincs azonosítás az átirányításban",
|
||||
"redirect_no_client_id": "Nincs felhasználó azonosító",
|
||||
"redirect_no_client_secret": "Nincs felhasználó jelszó",
|
||||
"redirect_no_code_verifier": "Nincs kódellenőrző",
|
||||
"redirect_no_token_endpoint": "Nincs \"Token Endpoint\"",
|
||||
"something_went_wrong_on_oauth_redirect": "Valami rosszul sikerült az OAuth átirányításakor",
|
||||
"something_went_wrong_on_token_generation": "Valami rosszul sikerült a token generálásakor",
|
||||
"token_generation_oidc_discovery_failed": "Hiba a token generálásakor: OpenID Connect Discovery hiba"
|
||||
},
|
||||
"pass_key_by": "Átadta",
|
||||
"password": "Jelszó",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"save_to_inherit": "Kérjük, mentse el ezt kérést bármelyik gyűjteménybe, hogy az azonosítás örökölhető lehessen",
|
||||
"token": "Token",
|
||||
"type": "Felhatalmazás típusa",
|
||||
"username": "Felhasználónév"
|
||||
@@ -148,7 +148,7 @@
|
||||
"created": "Gyűjtemény létrehozva",
|
||||
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
|
||||
"edit": "Gyűjtemény szerkesztése",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"import_or_create": "Gyűjtemény importálása vagy létrehozása",
|
||||
"invalid_name": "Adjon nevet a gyűjteménynek",
|
||||
"invalid_root_move": "A gyűjtemény már a gyökérben van",
|
||||
"moved": "Sikeresen áthelyezve",
|
||||
@@ -157,20 +157,20 @@
|
||||
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
|
||||
"new": "Új gyűjtemény",
|
||||
"order_changed": "Gyűjtemény sorrendje frissítve",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"properties": "Gyűjtemény tulajdonságok",
|
||||
"properties_updated": "Gyűjtemény tulajdonságai frissítve",
|
||||
"renamed": "Gyűjtemény átnevezve",
|
||||
"request_in_use": "A kérés használatban",
|
||||
"save_as": "Mentés másként",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "Mentés egy gyűjteménybe",
|
||||
"select": "Gyűjtemény kiválasztása",
|
||||
"select_location": "Hely kiválasztása",
|
||||
"select_team": "Csapat kiválasztása",
|
||||
"team_collections": "Csapat gyűjteményei"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"close_unsaved_tab": "Biztos, hogy bezárja ezt a lapot?",
|
||||
"close_unsaved_tabs": "Biztos, hogy bezárja az összes lapot? {count} elmentetlen lap el fog veszni.",
|
||||
"exit_team": "Biztosan el szeretné hagyni ezt a csapatot?",
|
||||
"logout": "Biztosan ki szeretne jelentkezni?",
|
||||
"remove_collection": "Biztosan véglegesen törölni szeretné ezt a gyűjteményt?",
|
||||
@@ -178,7 +178,7 @@
|
||||
"remove_folder": "Biztosan véglegesen törölni szeretné ezt a mappát?",
|
||||
"remove_history": "Biztosan véglegesen törölni szeretné az összes előzményt?",
|
||||
"remove_request": "Biztosan véglegesen törölni szeretné ezt a kérést?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_shared_request": "Biztos, hogy véglegesen törölni szeretné ezt a megosztott kérést?",
|
||||
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
||||
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
|
||||
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
|
||||
@@ -186,26 +186,26 @@
|
||||
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "Paraméterek hozzáadása",
|
||||
"open_request_in_new_tab": "Kérés megnyitása új lapot",
|
||||
"set_environment_variable": "Változóként való beállítás"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
"cookie_expires": "Lejárat",
|
||||
"cookie_name": "Név",
|
||||
"cookie_path": "Útvonal",
|
||||
"cookie_string": "Süti szöveg",
|
||||
"cookie_value": "Érték",
|
||||
"empty_domain": "Üres domain",
|
||||
"empty_domains": "Domain lista üres",
|
||||
"enter_cookie_string": "Süti szövegének megadása",
|
||||
"interceptor_no_support": "A kiválasztott interceptor nem támogatja a sütiket. Válasszon ki egy másik interceptor-t és próbálja újra.",
|
||||
"managed_tab": "Menedzselt",
|
||||
"new_domain_name": "Új domain neve",
|
||||
"no_cookies_in_domain": "Nincs süti beállítva ehhez a domainhez.",
|
||||
"raw_tab": "Nyers",
|
||||
"set": "Süti beállítása"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
@@ -221,7 +221,7 @@
|
||||
"generate_message": "Importáljon bármilyen Hoppscotch-gyűjteményt, hogy API-dokumentációt készítsen a folyamat során."
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "Ez a kérés nem használ felhatalmazást",
|
||||
"authorization": "Ez a kérés nem használ azonosítást",
|
||||
"body": "Ennek a kérésnek nincs törzse",
|
||||
"collection": "A gyűjtemény üres",
|
||||
"collections": "A gyűjtemények üresek",
|
||||
@@ -252,39 +252,39 @@
|
||||
"create_new": "Új környezet létrehozása",
|
||||
"created": "Környezet létrehozva",
|
||||
"deleted": "Környezet törlése",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "Környezet duplikálása",
|
||||
"edit": "Környezet szerkesztése",
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"empty_variables": "Nincs változó",
|
||||
"global": "Globális",
|
||||
"global_variables": "Globális változók",
|
||||
"import_or_create": "Környezet importálása vagy létrehozása",
|
||||
"invalid_name": "Adjon nevet a környezetnek",
|
||||
"list": "Environment variables",
|
||||
"list": "Környezeti változók",
|
||||
"my_environments": "Saját környezetek",
|
||||
"name": "Name",
|
||||
"name": "Név",
|
||||
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
|
||||
"new": "Új környezet",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_active_environment": "Nincs aktív környezet",
|
||||
"no_environment": "Nincs környezet",
|
||||
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"quick_peek": "Környezet gyors megnézése",
|
||||
"replace_with_variable": "Cserélje le egy változóra",
|
||||
"scope": "Hatókör",
|
||||
"select": "Környezet kiválasztása",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"set": "Környezet beállítása",
|
||||
"set_as_environment": "Környezetként való beállítás",
|
||||
"team_environments": "Csapatkörnyezetek",
|
||||
"title": "Környezetek",
|
||||
"updated": "Környezet frissítve",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"value": "Érték",
|
||||
"variable": "Változó",
|
||||
"variable_list": "Változólista"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"authproviders_load_error": "Nem sikerült betölteni az azonosító szolgáltatókat",
|
||||
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
|
||||
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"check_how_to_add_origin": "Ellenőrizze, hogy hogyan adhat hozzá forrást",
|
||||
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
||||
"danger_zone": "Veszélyes zóna",
|
||||
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
|
||||
@@ -300,13 +300,13 @@
|
||||
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
|
||||
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
|
||||
"network_fail": "Nem sikerült elküldeni a kérést",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_collections_to_export": "Nincs exportálható gyűjtemény. Kérjük, hozzon létre egyet, hogy elkezdhesse.",
|
||||
"no_duration": "Nincs időtartam",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_environments_to_export": "Nincs exportálható környezet. Kérjük, hozzon létre egyet, hogy elkezdhesse.",
|
||||
"no_results_found": "Nincs találat",
|
||||
"page_not_found": "Ez az oldal nem található",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"please_install_extension": "Kérjük telepítse a bővítményt és adja hozzá a forráshoz.",
|
||||
"proxy_error": "Proxy hiba",
|
||||
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
||||
"something_went_wrong": "Valami elromlott",
|
||||
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
||||
@@ -314,7 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "Exportálás JSON formátumban",
|
||||
"create_secret_gist": "Titkos Gist létrehozása",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"failed": "Valami hiba történt az exportálás közben",
|
||||
"gist_created": "Gist létrehozva",
|
||||
"require_github": "Jelentkezzen be GitHub használatával a titkos Gist létrehozásához",
|
||||
"title": "Exportálás"
|
||||
@@ -333,16 +333,16 @@
|
||||
"renamed": "Mappa átnevezve"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "Szeretne csatlakozni a legújabb GraphQL végponttal?",
|
||||
"connection_switch_new_url": "A tab váltása lecsatlakoztatta az aktív GraphQL kapcsolatról. Az új kapcsolat",
|
||||
"connection_switch_url": "Kapcsolódott a GraphQL végponthoz. A kapcsolat",
|
||||
"mutations": "Mutációk",
|
||||
"schema": "Séma",
|
||||
"subscriptions": "Feliratkozások",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "Kapcsolat váltása"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
"title": "GraphQL gyűjtemény"
|
||||
},
|
||||
"group": {
|
||||
"time": "Idő",
|
||||
@@ -354,9 +354,9 @@
|
||||
"save_workspace": "Saját munkaterület mentése"
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "A felhatalmazási fejléc automatikusan elő lesz állítva a kérés elküldésekor.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"authorization": "Az Azonosítás fejléc automatikusan elő lesz állítva a kérés elküldésekor.",
|
||||
"collection_properties_authorization": " Ez az azonosítás be lesz állítva minden kéréshez ebben a gyűjteményben.",
|
||||
"collection_properties_header": "Ez a fejléc be lesz állítva mint minden kéréshez ebben a gyűjteményben.",
|
||||
"generate_documentation_first": "Először állítsa elő a dokumentációt",
|
||||
"network_fail": "Nem lehet elérni az API-végpontot. Ellenőrizze a hálózati kapcsolatot vagy válasszon egy másik elfogót, és próbálja újra.",
|
||||
"offline": "Úgy tűnik, hogy kapcsolat nélküli módban van. Előfordulhat, hogy a munkaterületen lévő adatok nem naprakészek.",
|
||||
@@ -376,11 +376,11 @@
|
||||
"import": {
|
||||
"collections": "Gyűjtemények importálása",
|
||||
"curl": "cURL importálása",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"environments_from_gist": "Importálás Gist-ből",
|
||||
"environments_from_gist_description": "Hoppscotch környezetek importálása Gist-ből",
|
||||
"failed": "Hiba az importálás során: a formátum nem azonosítható",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "Importálás Gistből",
|
||||
"from_file": "Importálás fájlból",
|
||||
"from_gist": "Importálás Gist-ből",
|
||||
"from_gist_description": "Importálás Gist URL-ből",
|
||||
"from_insomnia": "Importálás Insomniából",
|
||||
"from_insomnia_description": "Importálás Insomnia-gyűjteményből",
|
||||
@@ -390,45 +390,45 @@
|
||||
"from_my_collections_description": "Importálás saját gyűjtemények fájlból",
|
||||
"from_openapi": "Importálás OpenAPI-ból",
|
||||
"from_openapi_description": "Importálás OpenAPI specifikációs fájlból (YML/JSON)",
|
||||
"from_postman": "Importálás Postmanből",
|
||||
"from_postman": "Importálás Postman-ből",
|
||||
"from_postman_description": "Importálás Postman-gyűjteményből",
|
||||
"from_url": "Importálás URL-ből",
|
||||
"gist_url": "Gist URL megadása",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"gql_collections_from_gist_description": "GraphQL gyűjtemények importálása Gist-ből",
|
||||
"hoppscotch_environment": "Hoppscotch környezet",
|
||||
"hoppscotch_environment_description": "Hoppscotch környezet importálása JSON fájlból",
|
||||
"import_from_url_invalid_fetch": "Nem sikerült lekérni az adatokat az URL-ről",
|
||||
"import_from_url_invalid_file_format": "Hiba a gyűjtemények importálása során",
|
||||
"import_from_url_invalid_type": "Nem támogatott típus. Az elfogadott értékek: „hoppscotch”, „openapi”, „postman” vagy „insomnia”.",
|
||||
"import_from_url_success": "Gyűjtemények importálva",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"insomnia_environment_description": "Insomnia környezet importálása JSON/YAML fájlból",
|
||||
"json_description": "Gyűjtemények importálása Hoppscotch-gyűjtemények JSON-fájlból",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"postman_environment": "Postman környezet",
|
||||
"postman_environment_description": "Postman környezet importálása JSON fájlból",
|
||||
"title": "Importálás"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "Lehetséges hibák ellenőrzése",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "Hozzáadás a környezethez",
|
||||
"not_found": "\"{environment}\" környezet nem található."
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "A böngésző nem engedélyezi a Hoppscotch-nak, hogy süti fejlécet állítson be. Amíg dolgozunk a Hoppscotch asztali alkalmazáson (hamarosan), kérjük használjon Authorization Header fejlécet."
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "Kérjük ellenőrizze az autentikációs adatokat.",
|
||||
"404_error": "Kérjük ellenőrizze a kérés URL-jét és típusát.",
|
||||
"cors_error": "Kérjük ellenőrizze a Cross-Origin Resource Sharing beállítást.",
|
||||
"default_error": "Kérjük ellenőrizze a kérését.",
|
||||
"network_error": "Kérjük ellenőrizze a internet elérhetőségét."
|
||||
},
|
||||
"title": "Inspector",
|
||||
"url": {
|
||||
"extension_not_installed": "Extension not installed.",
|
||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "Bővítmény nincs telepítve.",
|
||||
"extension_unknown_origin": "Ellenőrizze, hogy hozzáadta az API végpont forrását Hoppscotch Browser bővítmény listájához.",
|
||||
"extention_enable_action": "Bővítmény engedélyezése",
|
||||
"extention_not_enabled": "Bővítmény nincs engedélyezve."
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -442,10 +442,10 @@
|
||||
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
|
||||
"collections": "Gyűjtemények",
|
||||
"confirm": "Megerősítés",
|
||||
"customize_request": "Customize Request",
|
||||
"customize_request": "Kérés testreszabása",
|
||||
"edit_request": "Kérés szerkesztése",
|
||||
"import_export": "Importálás és exportálás",
|
||||
"share_request": "Share Request"
|
||||
"share_request": "Kérés megosztása"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "Ön már feliratkozott erre a témára.",
|
||||
@@ -511,7 +511,7 @@
|
||||
},
|
||||
"request": {
|
||||
"added": "Kérés hozzáadva",
|
||||
"authorization": "Felhatalmazás",
|
||||
"authorization": "Azonosítás",
|
||||
"body": "Kérés törzse",
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"content_type": "Tartalom típusa",
|
||||
@@ -526,8 +526,8 @@
|
||||
"enter_curl": "cURL-parancs megadása",
|
||||
"generate_code": "Kód előállítása",
|
||||
"generated_code": "Előállított kód",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"go_to_authorization_tab": "Navigálás az Azonosítás lapra",
|
||||
"go_to_body_tab": "Navigálás a Törzs lapra.",
|
||||
"header_list": "Fejléclista",
|
||||
"invalid_name": "Adjon nevet a kérésnek",
|
||||
"method": "Módszer",
|
||||
@@ -552,8 +552,8 @@
|
||||
"saved": "Kérés elmentve",
|
||||
"share": "Megosztás",
|
||||
"share_description": "A Hoppscotch megosztása az ismerőseivel",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"share_request": "Kérés megosztása",
|
||||
"stop": "Leállítás",
|
||||
"title": "Kérés",
|
||||
"type": "Kérés típusa",
|
||||
"url": "URL",
|
||||
@@ -587,7 +587,7 @@
|
||||
"account_description": "A fiókbeállítások személyre szabása.",
|
||||
"account_email_description": "Az Ön elsődleges e-mail-címe.",
|
||||
"account_name_description": "Ez a megjelenített neve.",
|
||||
"additional": "Additional Settings",
|
||||
"additional": "További beállítások",
|
||||
"background": "Háttér",
|
||||
"black_mode": "Fekete",
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
@@ -635,29 +635,29 @@
|
||||
"verify_email": "E-mail-cím ellenőrzése"
|
||||
},
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"button": "Gomb",
|
||||
"button_info": "Hozza létre a 'Futtatás Hoppscotch-ban' gombot a weboldalán vagy blogján.",
|
||||
"copy_html": "HTML másolása",
|
||||
"copy_link": "Link másolása",
|
||||
"copy_markdown": "Jelölő másolása",
|
||||
"creating_widget": "Widget létrehozása",
|
||||
"customize": "Testreszabás",
|
||||
"deleted": "Megosztott kérés törölve",
|
||||
"description": "Válasszon ki egy widgetet, ezt később módosíthatja és testreszabhatja",
|
||||
"embed": "Beágyazás",
|
||||
"embed_info": "Adja hozzá a \"Hoppscotch API Playground\"-ot weboldalához vagy blogjához.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"link_info": "Link létrehozása olvasási joggal való megosztáshoz.",
|
||||
"modified": "Megosztott kérés módosítva",
|
||||
"not_found": "Megosztott kérés nem található.",
|
||||
"open_new_tab": "Megnyitás új lapon.",
|
||||
"preview": "Előnézet",
|
||||
"run_in_hoppscotch": "Futtatás Hoppscotch-ban.",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
"dark": "Sötét",
|
||||
"light": "Világos",
|
||||
"system": "Rendszer",
|
||||
"title": "Téma"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -669,7 +669,7 @@
|
||||
"title": "Általános"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Emberek meghívása a Hoppscotchba",
|
||||
"invite": "Emberek meghívása a Hoppscotch-ba",
|
||||
"title": "Egyebek"
|
||||
},
|
||||
"navigation": {
|
||||
@@ -691,19 +691,19 @@
|
||||
"delete_method": "DELETE módszer kiválasztása",
|
||||
"get_method": "GET módszer kiválasztása",
|
||||
"head_method": "HEAD módszer kiválasztása",
|
||||
"import_curl": "Import cURL",
|
||||
"import_curl": "cURL importálása",
|
||||
"method": "Módszer",
|
||||
"next_method": "Következő módszer kiválasztása",
|
||||
"post_method": "POST módszer kiválasztása",
|
||||
"previous_method": "Előző módszer kiválasztása",
|
||||
"put_method": "PUT módszer kiválasztása",
|
||||
"rename": "Rename Request",
|
||||
"rename": "Kérés átnevezése",
|
||||
"reset_request": "Kérés visszaállítása",
|
||||
"save_request": "Save Request",
|
||||
"save_request": "Kérés mentése",
|
||||
"save_to_collections": "Mentés a gyűjteményekbe",
|
||||
"send_request": "Kérés elküldése",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"share_request": "Kérés megosztása",
|
||||
"show_code": "Kódrészlet generálása",
|
||||
"title": "Kérés",
|
||||
"copy_request_link": "Kérés hivatkozásának másolása"
|
||||
},
|
||||
@@ -735,82 +735,82 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "Nyelv váltása",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"title": "Environments"
|
||||
"delete": "Jelenlegi környezet törlése",
|
||||
"duplicate": "Jelenlegi környezet duplikálása",
|
||||
"duplicate_global": "Globális környezet duplikálása",
|
||||
"edit": "Jelenlegi környezet szerkesztése",
|
||||
"edit_global": "Globális környezet szerkesztése",
|
||||
"new": "Új környezet létrehozása",
|
||||
"new_variable": "Új környezeti változó létrehozása",
|
||||
"title": "Környezetek"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
"chat": "Üzenet a supportnak",
|
||||
"help_menu": "Segítség és support",
|
||||
"open_docs": "Dokumentáció olvasása",
|
||||
"open_github": "GitHub repository megnyitása",
|
||||
"open_keybindings": "Billentyűkombinációk megnyitása",
|
||||
"social": "Közösség",
|
||||
"title": "Általános"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "Csatlakozás a szerverhez",
|
||||
"disconnect": "Lecsatlakozás a szerverről"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "Hívja meg barátait a Hoppscotch-ba",
|
||||
"title": "Egyéb"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "Mentés új kérésként",
|
||||
"select_method": "Módszer kiválasztása",
|
||||
"switch_to": "Váltás",
|
||||
"tab_authorization": "Azonosítás lap",
|
||||
"tab_body": "Törzs lap",
|
||||
"tab_headers": "Fejlécek lap",
|
||||
"tab_parameters": "Paraméterek lap",
|
||||
"tab_pre_request_script": "Előzetes script lap",
|
||||
"tab_query": "Kérés lap",
|
||||
"tab_tests": "Tesztek lap",
|
||||
"tab_variables": "Változók lap"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Válasz másolása",
|
||||
"download": "Válasz letöltése fájlként",
|
||||
"title": "Válasz"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"theme": "Téma",
|
||||
"user": "Felhasználó"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "Interceptor váltása",
|
||||
"change_language": "Nyelv váltása",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "Fekete",
|
||||
"dark": "Sötét",
|
||||
"light": "Világos",
|
||||
"system": "Rendszer"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "Jelenlegi lap bezására",
|
||||
"close_others": "Összes többi lap bezására",
|
||||
"duplicate": "Jelenlegi lap diplikálása",
|
||||
"new_tab": "Új lap megnyitása",
|
||||
"title": "Lapok"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "Jelenlegi csapat törlése",
|
||||
"edit": "Jelenlegi csapat szerkesztése",
|
||||
"invite": "Emberek meghívása a jelenlegi csapatba",
|
||||
"new": "Új csapat létrehozása",
|
||||
"switch_to_personal": "Váltás a személyes munkaterületére",
|
||||
"title": "Csapatok"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -828,20 +828,20 @@
|
||||
"connection_error": "Nem sikerült kapcsolódni",
|
||||
"connection_failed": "A kapcsolódás sikertelen",
|
||||
"connection_lost": "A kapcsolat elveszett",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_interface_to_clipboard": "{language} interface típusa vágólapra másolva",
|
||||
"copied_to_clipboard": "Vágólapra másolva",
|
||||
"deleted": "Törölve",
|
||||
"deprecated": "ELAVULT",
|
||||
"disabled": "Letiltva",
|
||||
"disconnected": "Leválasztva",
|
||||
"disconnected_from": "Leválasztva innen: {name}",
|
||||
"disconnected": "Lecsatlakoztatva",
|
||||
"disconnected_from": "Lecsatlakoztatva innen: {name}",
|
||||
"docs_generated": "Dokumentáció előállítva",
|
||||
"download_failed": "Download failed",
|
||||
"download_failed": "Letöltés sikertelen",
|
||||
"download_started": "A letöltés elkezdődött",
|
||||
"enabled": "Engedélyezve",
|
||||
"file_imported": "Fájl importálva",
|
||||
"finished_in": "Befejeződött {duration} ms alatt",
|
||||
"hide": "Hide",
|
||||
"hide": "Elrejtés",
|
||||
"history_deleted": "Előzmények törölve",
|
||||
"linewrap": "Sorok tördelése",
|
||||
"loading": "Betöltés…",
|
||||
@@ -872,13 +872,13 @@
|
||||
"twitter": "Kövessen minket Twitteren"
|
||||
},
|
||||
"tab": {
|
||||
"authorization": "Felhatalmazás",
|
||||
"authorization": "Azonosítás",
|
||||
"body": "Törzs",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"close": "Lap bezárása",
|
||||
"close_others": "Többi lap bezárása",
|
||||
"collections": "Gyűjtemények",
|
||||
"documentation": "Dokumentáció",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"duplicate": "Lap duplikálása",
|
||||
"environments": "Környezetek",
|
||||
"headers": "Fejlécek",
|
||||
"history": "Előzmények",
|
||||
@@ -905,7 +905,7 @@
|
||||
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"exit": "Kilépés a csapatból",
|
||||
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
|
||||
"failed_invites": "Failed invites",
|
||||
"failed_invites": "Hiba a meghívás közben",
|
||||
"invalid_coll_id": "Érvénytelen gyűjteményazonosító",
|
||||
"invalid_email_format": "Az e-mail formátuma érvénytelen",
|
||||
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user