Compare commits
47 Commits
feat/admin
...
hotfix/ful
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e3cf0a6a | ||
|
|
21b1f385e6 | ||
|
|
c0aca81004 | ||
|
|
88bbd1a861 | ||
|
|
befbead59f | ||
|
|
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 |
@@ -118,7 +118,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
environment:
|
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)
|
# 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
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -23,13 +23,13 @@
|
|||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^16.2.3",
|
"@commitlint/cli": "16.3.0",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "16.2.4",
|
||||||
"@hoppscotch/ui": "^0.1.0",
|
"@hoppscotch/ui": "0.1.0",
|
||||||
"@types/node": "17.0.27",
|
"@types/node": "17.0.27",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "^7.0.4",
|
"husky": "7.0.4",
|
||||||
"lint-staged": "12.4.0"
|
"lint-staged": "12.4.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"vue": "3.3.9"
|
"vue": "3.3.9"
|
||||||
},
|
},
|
||||||
"packageExtensions": {
|
"packageExtensions": {
|
||||||
"httpsnippet@^3.0.1": {
|
"httpsnippet@3.0.1": {
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ajv": "6.12.3"
|
"ajv": "6.12.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,16 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "6.9.3",
|
"@codemirror/language": "6.10.1",
|
||||||
"@lezer/highlight": "1.2.0",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@lezer/lr": "^1.3.14"
|
"@lezer/lr": "1.3.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.5.1",
|
"@lezer/generator": "1.5.1",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "9.2.2",
|
||||||
"rollup": "^3.29.3",
|
"rollup": "3.29.4",
|
||||||
"rollup-plugin-dts": "^6.0.2",
|
"rollup-plugin-dts": "6.0.2",
|
||||||
"rollup-plugin-ts": "^3.4.5",
|
"rollup-plugin-ts": "3.4.5",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.12.3",
|
"version": "2024.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -24,80 +24,83 @@
|
|||||||
"do-test": "pnpm run test"
|
"do-test": "pnpm run test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.9.4",
|
"@apollo/server": "4.9.5",
|
||||||
"@nestjs-modules/mailer": "^1.9.1",
|
"@nestjs-modules/mailer": "1.9.1",
|
||||||
"@nestjs/apollo": "^12.0.9",
|
"@nestjs/apollo": "12.0.9",
|
||||||
"@nestjs/common": "^10.2.6",
|
"@nestjs/common": "10.2.7",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "3.1.1",
|
||||||
"@nestjs/core": "^10.2.6",
|
"@nestjs/core": "10.2.7",
|
||||||
"@nestjs/graphql": "^12.0.9",
|
"@nestjs/graphql": "12.0.9",
|
||||||
"@nestjs/jwt": "^10.1.1",
|
"@nestjs/jwt": "10.1.1",
|
||||||
"@nestjs/passport": "^10.0.2",
|
"@nestjs/passport": "10.0.2",
|
||||||
"@nestjs/platform-express": "^10.2.6",
|
"@nestjs/platform-express": "10.2.7",
|
||||||
"@nestjs/throttler": "^5.0.0",
|
"@nestjs/schedule": "4.0.1",
|
||||||
"@prisma/client": "^5.8.0",
|
"@nestjs/throttler": "5.0.1",
|
||||||
"argon2": "^0.30.3",
|
"@prisma/client": "5.8.1",
|
||||||
"bcrypt": "^5.1.0",
|
"argon2": "0.30.3",
|
||||||
"cookie": "^0.5.0",
|
"bcrypt": "5.1.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie": "0.5.0",
|
||||||
"express": "^4.17.1",
|
"cookie-parser": "1.4.6",
|
||||||
"express-session": "^1.17.3",
|
"cron": "3.1.6",
|
||||||
"fp-ts": "^2.13.1",
|
"express": "4.18.2",
|
||||||
"graphql": "^16.8.1",
|
"express-session": "1.17.3",
|
||||||
"graphql-query-complexity": "^0.12.0",
|
"fp-ts": "2.13.1",
|
||||||
"graphql-redis-subscriptions": "^2.6.0",
|
"graphql": "16.8.1",
|
||||||
"graphql-subscriptions": "^2.0.0",
|
"graphql-query-complexity": "0.12.0",
|
||||||
"handlebars": "^4.7.7",
|
"graphql-redis-subscriptions": "2.6.0",
|
||||||
"io-ts": "^2.2.16",
|
"graphql-subscriptions": "2.0.0",
|
||||||
"luxon": "^3.2.1",
|
"handlebars": "4.7.7",
|
||||||
"nodemailer": "^6.9.1",
|
"io-ts": "2.2.16",
|
||||||
"passport": "^0.6.0",
|
"luxon": "3.2.1",
|
||||||
"passport-github2": "^0.1.12",
|
"nodemailer": "6.9.1",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport": "0.6.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-github2": "0.1.12",
|
||||||
"passport-local": "^1.0.0",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-jwt": "4.0.1",
|
||||||
"prisma": "^5.8.0",
|
"passport-local": "1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"passport-microsoft": "1.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"posthog-node": "3.6.3",
|
||||||
"rxjs": "^7.6.0"
|
"prisma": "5.8.1",
|
||||||
|
"reflect-metadata": "0.1.13",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"rxjs": "7.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.1.18",
|
"@nestjs/cli": "10.2.1",
|
||||||
"@nestjs/schematics": "^10.0.2",
|
"@nestjs/schematics": "10.0.3",
|
||||||
"@nestjs/testing": "^10.2.6",
|
"@nestjs/testing": "10.2.7",
|
||||||
"@relmify/jest-fp-ts": "^2.0.2",
|
"@relmify/jest-fp-ts": "2.0.2",
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/argon2": "0.15.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "5.0.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "0.5.1",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "1.4.3",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "4.17.14",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "29.4.0",
|
||||||
"@types/luxon": "^3.2.0",
|
"@types/luxon": "3.2.0",
|
||||||
"@types/node": "^18.11.10",
|
"@types/node": "18.11.10",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/passport-github2": "^1.2.5",
|
"@types/passport-github2": "1.2.5",
|
||||||
"@types/passport-google-oauth20": "^2.0.11",
|
"@types/passport-google-oauth20": "2.0.11",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "3.0.8",
|
||||||
"@types/passport-microsoft": "^0.0.0",
|
"@types/passport-microsoft": "0.0.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "^8.29.0",
|
"eslint": "8.29.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"jest": "^29.4.1",
|
"jest": "29.4.1",
|
||||||
"jest-mock-extended": "^3.0.1",
|
"jest-mock-extended": "3.0.1",
|
||||||
"jwt": "link:@types/nestjs/jwt",
|
"jwt": "link:@types/nestjs/jwt",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "2.8.4",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"supertest": "^6.3.2",
|
"supertest": "6.3.2",
|
||||||
"ts-jest": "29.0.5",
|
"ts-jest": "29.0.5",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "9.4.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsconfig-paths": "4.1.1",
|
"tsconfig-paths": "4.1.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "4.9.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE
|
||||||
|
"TeamCollection"
|
||||||
|
ADD
|
||||||
|
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE
|
||||||
|
"TeamRequest"
|
||||||
|
ADD
|
||||||
|
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamCollection_textSearch_idx" ON "TeamCollection" USING GIN (titleSearch);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamRequest_textSearch_idx" ON "TeamRequest" USING GIN (titleSearch);
|
||||||
@@ -41,31 +41,31 @@ model TeamInvitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model TeamCollection {
|
model TeamCollection {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
parentID String?
|
parentID String?
|
||||||
data Json?
|
data Json?
|
||||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||||
requests TeamRequest[]
|
requests TeamRequest[]
|
||||||
teamID String
|
teamID String
|
||||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamRequest {
|
model TeamRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
collectionID String
|
collectionID String
|
||||||
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
||||||
teamID String
|
teamID String
|
||||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
request Json
|
request Json
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Shortcode {
|
model Shortcode {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import {
|
|||||||
EnableAndDisableSSOArgs,
|
EnableAndDisableSSOArgs,
|
||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
|
import { ServiceStatus } from 'src/infra-config/helper';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Infra)
|
@Resolver(() => Infra)
|
||||||
@@ -273,10 +274,10 @@ export class InfraResolver {
|
|||||||
async infraConfigs(
|
async infraConfigs(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'configNames',
|
name: 'configNames',
|
||||||
type: () => [InfraConfigEnumForClient],
|
type: () => [InfraConfigEnum],
|
||||||
description: 'Configs to fetch',
|
description: 'Configs to fetch',
|
||||||
})
|
})
|
||||||
names: InfraConfigEnumForClient[],
|
names: InfraConfigEnum[],
|
||||||
) {
|
) {
|
||||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||||
@@ -310,6 +311,25 @@ export class InfraResolver {
|
|||||||
return updatedRes.right;
|
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, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Reset Infra Configs with default values (.env)',
|
description: 'Reset Infra Configs with default values (.env)',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
|||||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||||
import { loadInfraConfiguration } from './infra-config/helper';
|
import { loadInfraConfiguration } from './infra-config/helper';
|
||||||
import { MailerModule } from './mailer/mailer.module';
|
import { MailerModule } from './mailer/mailer.module';
|
||||||
|
import { PosthogModule } from './posthog/posthog.module';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
|
PosthogModule,
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import {
|
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
|
||||||
AuthProvider,
|
|
||||||
authCookieHandler,
|
|
||||||
authProviderCheck,
|
|
||||||
throwHTTPErr,
|
|
||||||
} from './helper';
|
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
@@ -31,6 +26,7 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
|
|||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
|
|||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
import {
|
||||||
|
isInfraConfigTablePopulated,
|
||||||
|
loadInfraConfiguration,
|
||||||
|
} from 'src/infra-config/helper';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
|
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
|
||||||
|
if (!isInfraConfigPopulated) {
|
||||||
|
return { module: AuthModule };
|
||||||
|
}
|
||||||
|
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from 'src/types/AuthTokens';
|
} from 'src/types/AuthTokens';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
@@ -117,7 +117,7 @@ export class AuthService {
|
|||||||
userUid,
|
userUid,
|
||||||
);
|
);
|
||||||
if (E.isLeft(updatedUser))
|
if (E.isLeft(updatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: updatedUser.left,
|
message: updatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -255,7 +255,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async verifyMagicLinkTokens(
|
async verifyMagicLinkTokens(
|
||||||
magicLinkIDTokens: VerifyMagicDto,
|
magicLinkIDTokens: VerifyMagicDto,
|
||||||
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
|
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
|
||||||
const passwordlessTokens = await this.validatePasswordlessTokens(
|
const passwordlessTokens = await this.validatePasswordlessTokens(
|
||||||
magicLinkIDTokens,
|
magicLinkIDTokens,
|
||||||
);
|
);
|
||||||
@@ -373,7 +373,7 @@ export class AuthService {
|
|||||||
if (usersCount === 1) {
|
if (usersCount === 1) {
|
||||||
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
||||||
if (E.isLeft(elevatedUser))
|
if (E.isLeft(elevatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: elevatedUser.left,
|
message: elevatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
@@ -25,15 +24,6 @@ export enum AuthProvider {
|
|||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function allows throw to be used as an expression
|
|
||||||
* @param errMessage Message present in the error message
|
|
||||||
*/
|
|
||||||
export function throwHTTPErr(errorData: AuthError): never {
|
|
||||||
const { message, statusCode } = errorData;
|
|
||||||
throw new HttpException(message, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets and returns the cookies in the response object on successful authentication
|
* Sets and returns the cookies in the response object on successful authentication
|
||||||
* @param res Express Response Object
|
* @param res Express Response Object
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
|
||||||
scope: [configService.get('GITHUB_SCOPE')],
|
scope: [configService.get('INFRA.GITHUB_SCOPE')],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
|
||||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
|
||||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
|
||||||
tenant: configService.get('MICROSOFT_TENANT'),
|
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,12 @@ export const TEAM_COL_NOT_SAME_PARENT =
|
|||||||
export const TEAM_COL_SAME_NEXT_COLL =
|
export const TEAM_COL_SAME_NEXT_COLL =
|
||||||
'team_coll/collection_and_next_collection_are_same';
|
'team_coll/collection_and_next_collection_are_same';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection search failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COL_SEARCH_FAILED = 'team_coll/team_collection_search_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Team Collection Re-Ordering Failed
|
* Team Collection Re-Ordering Failed
|
||||||
* (TeamCollectionService)
|
* (TeamCollectionService)
|
||||||
@@ -283,6 +289,13 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
|||||||
export const TEAM_COLL_DATA_INVALID =
|
export const TEAM_COLL_DATA_INVALID =
|
||||||
'team_coll/team_coll_data_invalid' as const;
|
'team_coll/team_coll_data_invalid' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection parent tree generation failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COLL_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_coll/team_coll_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -308,6 +321,19 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
|
|||||||
*/
|
*/
|
||||||
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request search failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_SEARCH_FAILED = 'team_req/team_request_search_failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request parent tree generation failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_req/team_req_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No Postmark Sender Email defined
|
* No Postmark Sender Email defined
|
||||||
* (AuthService)
|
* (AuthService)
|
||||||
@@ -705,9 +731,22 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
|||||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||||
'infra_config/service_not_configured' as const;
|
'infra_config/service_not_configured' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infra Config update/fetch operation not allowed
|
||||||
|
* (InfraConfigService)
|
||||||
|
*/
|
||||||
|
export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
|
||||||
|
'infra_config/operation_not_allowed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message for when the database table does not exist
|
* Error message for when the database table does not exist
|
||||||
* (InfraConfigService)
|
* (InfraConfigService)
|
||||||
*/
|
*/
|
||||||
export const DATABASE_TABLE_NOT_EXIST =
|
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';
|
'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';
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
import {
|
||||||
|
AUTH_PROVIDER_NOT_CONFIGURED,
|
||||||
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
export enum ServiceStatus {
|
export enum ServiceStatus {
|
||||||
ENABLE = 'ENABLE',
|
ENABLE = 'ENABLE',
|
||||||
@@ -13,14 +17,21 @@ const AuthProviderConfigurations = {
|
|||||||
[AuthProvider.GOOGLE]: [
|
[AuthProvider.GOOGLE]: [
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.GITHUB]: [
|
[AuthProvider.GITHUB]: [
|
||||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GITHUB_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.MICROSOFT]: [
|
[AuthProvider.MICROSOFT]: [
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
|
InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
],
|
],
|
||||||
[AuthProvider.EMAIL]: [
|
[AuthProvider.EMAIL]: [
|
||||||
InfraConfigEnum.MAILER_SMTP_URL,
|
InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
@@ -53,6 +64,125 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if 'infra_config' table is loaded with all entries
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export async function isInfraConfigTablePopulated(): Promise<boolean> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
try {
|
||||||
|
const dbInfraConfigs = await prisma.infraConfig.findMany();
|
||||||
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
|
||||||
|
const propsRemainingToInsert = infraConfigDefaultObjs.filter(
|
||||||
|
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (propsRemainingToInsert.length > 0) {
|
||||||
|
console.log(
|
||||||
|
'Infra Config table is not populated with all entries. Populating now...',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the app after 5 seconds
|
* Stop the app after 5 seconds
|
||||||
* (Docker will re-start the app)
|
* (Docker will re-start the app)
|
||||||
@@ -104,3 +234,12 @@ export function getConfiguredSSOProviders() {
|
|||||||
|
|
||||||
return configuredAuthProviders.join(',');
|
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 * as E from 'fp-ts/Either';
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
||||||
import { throwHTTPErr } from 'src/auth/helper';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'site', version: '1' })
|
@Controller({ path: 'site', version: '1' })
|
||||||
@@ -17,11 +17,11 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async fetchSetupInfo() {
|
async fetchSetupInfo() {
|
||||||
const status = await this.infraConfigService.get(
|
const status = await this.infraConfigService.get(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(status))
|
if (E.isLeft(status))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: status.left,
|
message: status.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -32,13 +32,13 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async setSetupAsComplete() {
|
async setSetupAsComplete() {
|
||||||
const res = await this.infraConfigService.update(
|
const res = await this.infraConfigService.update(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
false.toString(),
|
false.toString(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(res))
|
if (E.isLeft(res))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: res.left,
|
message: res.left,
|
||||||
statusCode: HttpStatus.FORBIDDEN,
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -8,7 +8,7 @@ export class InfraConfig {
|
|||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
@@ -16,7 +16,7 @@ export class InfraConfig {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(InfraConfigEnumForClient, {
|
registerEnumType(InfraConfigEnum, {
|
||||||
name: 'InfraConfigEnum',
|
name: 'InfraConfigEnum',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import {
|
import {
|
||||||
InfraConfigEnum,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
InfraConfigEnumForClient,
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/types/InfraConfig';
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as helper from './helper';
|
import * as helper from './helper';
|
||||||
|
import { InfraConfig as dbInfraConfig } from '@prisma/client';
|
||||||
|
import { InfraConfig } from './infra-config.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
|
|||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const INITIALIZED_DATE_CONST = new Date();
|
||||||
|
const dbInfraConfigs: dbInfraConfig[] = [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: 'abcdefghijkl',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: 'google',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const infraConfigs: InfraConfig[] = [
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[0].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[0].value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[1].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[1].value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('InfraConfigService', () => {
|
describe('InfraConfigService', () => {
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
|
it('should update the infra config without backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
const result = await infraConfigService.update(name, value);
|
||||||
|
|
||||||
|
expect(helper.stopApp).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the infra config with backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
|
||||||
|
const result = await infraConfigService.update(name, value, true);
|
||||||
|
|
||||||
|
expect(helper.stopApp).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the infra config', async () => {
|
it('should update the infra config', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should get the infra config', async () => {
|
it('should get the infra config', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass correct params to prisma findUnique', async () => {
|
it('should pass correct params to prisma findUnique', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
await infraConfigService.get(name);
|
await infraConfigService.get(name);
|
||||||
|
|
||||||
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the infra config does not exist', async () => {
|
it('should throw an error if the infra config does not exist', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
||||||
|
|
||||||
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
|
|||||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMany', () => {
|
||||||
|
it('should throw error if any disallowed names are provided', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
});
|
||||||
|
it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
disallowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames, false);
|
||||||
|
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => disallowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return right with infraConfigs if Prisma query succeeds', async () => {
|
||||||
|
const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
allowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(allowedNames);
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => allowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,23 +3,25 @@ import { InfraConfig } from './infra-config.model';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import {
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
InfraConfigEnum,
|
|
||||||
InfraConfigEnumForClient,
|
|
||||||
} from 'src/types/InfraConfig';
|
|
||||||
import {
|
import {
|
||||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
INFRA_CONFIG_INVALID_INPUT,
|
INFRA_CONFIG_INVALID_INPUT,
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
INFRA_CONFIG_NOT_LISTED,
|
|
||||||
INFRA_CONFIG_RESET_FAILED,
|
INFRA_CONFIG_RESET_FAILED,
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||||
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
import {
|
||||||
|
throwErr,
|
||||||
|
validateSMTPEmail,
|
||||||
|
validateSMTPUrl,
|
||||||
|
validateUrl,
|
||||||
|
} from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@@ -30,76 +32,32 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
||||||
|
EXCLUDE_FROM_UPDATE_CONFIGS = [
|
||||||
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
];
|
||||||
|
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
||||||
|
EXCLUDE_FROM_FETCH_CONFIGS = [
|
||||||
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
];
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.initializeInfraConfigTable();
|
await this.initializeInfraConfigTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultInfraConfigs(): Promise<
|
|
||||||
{ name: InfraConfigEnum; value: string }[]
|
|
||||||
> {
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
|
||||||
value: process.env.MAILER_SMTP_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
|
||||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
|
||||||
value: process.env.GITHUB_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
|
||||||
value: process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: getConfiguredSSOProviders(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the 'infra_config' table with values from .env
|
* Initialize the 'infra_config' table with values from .env
|
||||||
* @description This function create rows 'infra_config' in very first time (only once)
|
* @description This function create rows 'infra_config' in very first time (only once)
|
||||||
*/
|
*/
|
||||||
async initializeInfraConfigTable() {
|
async initializeInfraConfigTable() {
|
||||||
try {
|
try {
|
||||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
|
||||||
const enumValues = Object.values(InfraConfigEnum);
|
|
||||||
|
|
||||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await 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
|
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
||||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
||||||
@@ -156,11 +114,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
|
||||||
value: string,
|
|
||||||
restartEnabled = false,
|
|
||||||
) {
|
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -184,6 +138,11 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
||||||
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
const isValidate = this.validateEnvValues(infraConfigs);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -217,12 +176,26 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case AuthProvider.GOOGLE:
|
case AuthProvider.GOOGLE:
|
||||||
return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GOOGLE_CLIENT_ID &&
|
||||||
|
configMap.GOOGLE_CLIENT_SECRET &&
|
||||||
|
configMap.GOOGLE_CALLBACK_URL &&
|
||||||
|
configMap.GOOGLE_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.GITHUB:
|
case AuthProvider.GITHUB:
|
||||||
return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GITHUB_CLIENT_ID &&
|
||||||
|
configMap.GITHUB_CLIENT_SECRET &&
|
||||||
|
configMap.GITHUB_CALLBACK_URL &&
|
||||||
|
configMap.GITHUB_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.MICROSOFT:
|
case AuthProvider.MICROSOFT:
|
||||||
return (
|
return (
|
||||||
configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET
|
configMap.MICROSOFT_CLIENT_ID &&
|
||||||
|
configMap.MICROSOFT_CLIENT_SECRET &&
|
||||||
|
configMap.MICROSOFT_CALLBACK_URL &&
|
||||||
|
configMap.MICROSOFT_SCOPE &&
|
||||||
|
configMap.MICROSOFT_TENANT
|
||||||
);
|
);
|
||||||
case AuthProvider.EMAIL:
|
case AuthProvider.EMAIL:
|
||||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||||
@@ -231,6 +204,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
|
* Enable or Disable SSO for login/signup
|
||||||
* @param provider Auth Provider to enable or disable
|
* @param provider Auth Provider to enable or disable
|
||||||
@@ -281,7 +270,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param name Name of the InfraConfig
|
* @param name Name of the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async get(name: InfraConfigEnumForClient) {
|
async get(name: InfraConfigEnum) {
|
||||||
try {
|
try {
|
||||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||||
where: { name },
|
where: { name },
|
||||||
@@ -298,7 +287,15 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param names Names of the InfraConfigs
|
* @param names Names of the InfraConfigs
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async getMany(names: InfraConfigEnumForClient[]) {
|
async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) {
|
||||||
|
if (checkDisallowedKeys) {
|
||||||
|
// Check if the names are allowed to fetch by client
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i]))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||||
where: { name: { in: names } },
|
where: { name: { in: names } },
|
||||||
@@ -325,7 +322,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
|
||||||
await this.prisma.infraConfig.deleteMany({
|
await this.prisma.infraConfig.deleteMany({
|
||||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
||||||
@@ -358,36 +355,60 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
validateEnvValues(
|
validateEnvValues(
|
||||||
infraConfigs: {
|
infraConfigs: {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
name: InfraConfigEnum;
|
||||||
value: string;
|
value: string;
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
switch (infraConfigs[i].name) {
|
switch (infraConfigs[i].name) {
|
||||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GOOGLE_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GITHUB_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_TENANT:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class InfraConfigArgs {
|
export class InfraConfigArgs {
|
||||||
@Field(() => InfraConfigEnumForClient, {
|
@Field(() => InfraConfigEnum, {
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class MailerService {
|
|||||||
): string {
|
): string {
|
||||||
switch (mailDesc.template) {
|
switch (mailDesc.template) {
|
||||||
case 'team-invitation':
|
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':
|
case 'user-invitation':
|
||||||
return 'Sign in to Hoppscotch';
|
return 'Sign in to Hoppscotch';
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.nohighlight {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -458,7 +464,7 @@
|
|||||||
<td class="content-cell">
|
<td class="content-cell">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
<h1>Hi there,</h1>
|
<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 -->
|
<!-- Action -->
|
||||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -484,7 +490,7 @@
|
|||||||
Welcome aboard, <br />
|
Welcome aboard, <br />
|
||||||
Your friends at Hoppscotch
|
Your friends at Hoppscotch
|
||||||
</p>
|
</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 -->
|
<!-- Sub copy -->
|
||||||
<table class="body-sub">
|
<table class="body-sub">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.nohighlight {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,38 @@
|
|||||||
|
import { Controller, Get, 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';
|
||||||
|
|
||||||
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
|
@Controller({ path: 'team-collection', version: '1' })
|
||||||
|
export class TeamCollectionController {
|
||||||
|
constructor(private readonly teamCollectionService: TeamCollectionService) {}
|
||||||
|
|
||||||
|
@Get('search/:teamID/:searchQuery')
|
||||||
|
@RequiresTeamRole(
|
||||||
|
TeamMemberRole.VIEWER,
|
||||||
|
TeamMemberRole.EDITOR,
|
||||||
|
TeamMemberRole.OWNER,
|
||||||
|
)
|
||||||
|
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
|
||||||
|
async searchByTitle(
|
||||||
|
@Param('searchQuery') searchQuery: string,
|
||||||
|
@Param('teamID') teamID: string,
|
||||||
|
@Query('take') take: string,
|
||||||
|
@Query('skip') skip: string,
|
||||||
|
) {
|
||||||
|
const res = await this.teamCollectionService.searchByTitle(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
parseInt(take),
|
||||||
|
parseInt(skip),
|
||||||
|
);
|
||||||
|
if (E.isLeft(res)) throwHTTPErr(res.left);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-membe
|
|||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
|
import { TeamCollectionController } from './team-collection.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
||||||
@@ -15,5 +16,6 @@ import { PubSubModule } from '../pubsub/pubsub.module';
|
|||||||
GqlCollectionTeamMemberGuard,
|
GqlCollectionTeamMemberGuard,
|
||||||
],
|
],
|
||||||
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
||||||
|
controllers: [TeamCollectionController],
|
||||||
})
|
})
|
||||||
export class TeamCollectionModule {}
|
export class TeamCollectionModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
import { TeamCollection } from './team-collection.model';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COLL_DATA_INVALID,
|
TEAM_COLL_DATA_INVALID,
|
||||||
|
TEAM_REQ_SEARCH_FAILED,
|
||||||
|
TEAM_COL_SEARCH_FAILED,
|
||||||
|
TEAM_REQ_PARENT_TREE_GEN_FAILED,
|
||||||
|
TEAM_COLL_PARENT_TREE_GEN_FAILED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
@@ -22,6 +26,9 @@ import * as O from 'fp-ts/Option';
|
|||||||
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
import { stringToJson } from 'src/utils';
|
import { stringToJson } from 'src/utils';
|
||||||
|
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
|
||||||
|
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamCollectionService {
|
export class TeamCollectionService {
|
||||||
@@ -1056,4 +1063,274 @@ export class TeamCollectionService {
|
|||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections and TeamRequests by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
async searchByTitle(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take = 10,
|
||||||
|
skip = 0,
|
||||||
|
) {
|
||||||
|
// Fetch all collections and requests that match the search query
|
||||||
|
const searchResults: SearchQueryReturnType[] = [];
|
||||||
|
|
||||||
|
const matchedCollections = await this.searchCollections(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedCollections))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedCollections.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedCollections.right);
|
||||||
|
|
||||||
|
const matchedRequests = await this.searchRequests(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedRequests))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedRequests.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedRequests.right);
|
||||||
|
|
||||||
|
// Generate the parent tree for searchResults
|
||||||
|
const searchResultsWithTree: CollectionSearchNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < searchResults.length; i++) {
|
||||||
|
const fetchedParentTree = await this.fetchParentTree(searchResults[i]);
|
||||||
|
if (E.isLeft(fetchedParentTree))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: fetchedParentTree.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResultsWithTree.push({
|
||||||
|
type: searchResults[i].type,
|
||||||
|
title: searchResults[i].title,
|
||||||
|
method: searchResults[i].method,
|
||||||
|
id: searchResults[i].id,
|
||||||
|
path: !fetchedParentTree
|
||||||
|
? []
|
||||||
|
: (fetchedParentTree.right as CollectionSearchNode[]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right({ data: searchResultsWithTree });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
private async searchCollections(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take: number,
|
||||||
|
skip: number,
|
||||||
|
) {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
select id,title,'collection' AS type
|
||||||
|
from "TeamCollection"
|
||||||
|
where "TeamCollection"."teamID"=${teamID}
|
||||||
|
and titlesearch @@ plainto_tsquery(${searchQuery})
|
||||||
|
order by ts_rank(titlesearch,plainto_tsquery(${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 titlesearch @@ plainto_tsquery(${searchQuery})
|
||||||
|
order by ts_rank(titlesearch,plainto_tsquery(${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_ID = 'GOOGLE_CLIENT_ID',
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
|
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||||
|
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||||
|
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
|
||||||
|
GITHUB_SCOPE = 'GITHUB_SCOPE',
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL',
|
||||||
|
MICROSOFT_SCOPE = 'MICROSOFT_SCOPE',
|
||||||
|
MICROSOFT_TENANT = 'MICROSOFT_TENANT',
|
||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||||
}
|
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||||
|
|
||||||
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',
|
|
||||||
|
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { HttpStatus } from '@nestjs/common';
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Custom interface to handle errors specific to Auth module
|
** Custom interface to handle errors for REST modules such as Auth, Admin modules
|
||||||
** Since its REST we need to return the HTTP status code along with the error message
|
** Since its REST we need to return the HTTP status code along with the error message
|
||||||
*/
|
*/
|
||||||
export type AuthError = {
|
export type RESTError = {
|
||||||
message: string;
|
message: string;
|
||||||
statusCode: HttpStatus;
|
statusCode: HttpStatus;
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext } from '@nestjs/common';
|
import { ExecutionContext, HttpException } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { AuthProvider } from './auth/helper';
|
||||||
|
import { RESTError } from './types/RESTError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -27,6 +28,15 @@ export function throwErr(errMessage: string): never {
|
|||||||
throw new Error(errMessage);
|
throw new Error(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows throw to be used as an expression
|
||||||
|
* @param errMessage Message present in the error message
|
||||||
|
*/
|
||||||
|
export function throwHTTPErr(errorData: RESTError): never {
|
||||||
|
const { message, statusCode } = errorData;
|
||||||
|
throw new HttpException(message, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints the given value to log and returns the same value.
|
* Prints the given value to log and returns the same value.
|
||||||
* Used for debugging functional pipelines.
|
* Used for debugging functional pipelines.
|
||||||
@@ -173,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the URL is valid or not
|
||||||
|
* @param url The URL to validate
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export const validateUrl = (url: string) => {
|
||||||
|
const urlRegex = /^(http|https):\/\/[^ "]+$/;
|
||||||
|
return urlRegex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String to JSON parser
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @param {str} str The string to parse
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||||
"homepage": "https://hoppscotch.io",
|
"homepage": "https://hoppscotch.io",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -41,26 +41,28 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.6",
|
"axios": "1.6.7",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "5.3.0",
|
||||||
"commander": "^11.1.0",
|
"commander": "11.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"qs": "^6.11.2",
|
"qs": "6.11.2",
|
||||||
"zod": "^3.22.4"
|
"verzod": "0.2.2",
|
||||||
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "2.1.1",
|
||||||
"@swc/core": "^1.3.105",
|
"@swc/core": "1.4.2",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "29.5.12",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/qs": "^6.9.11",
|
"@types/qs": "6.9.12",
|
||||||
"fp-ts": "^2.16.2",
|
"fp-ts": "2.16.2",
|
||||||
"jest": "^29.7.0",
|
"jest": "29.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "3.2.5",
|
||||||
"ts-jest": "^29.1.2",
|
"qs": "6.11.2",
|
||||||
"tsup": "^8.0.1",
|
"ts-jest": "29.1.2",
|
||||||
"typescript": "^5.3.3"
|
"tsup": "8.0.2",
|
||||||
|
"typescript": "5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,138 +3,247 @@ import { ExecException } from "child_process";
|
|||||||
import { HoppErrorCode } from "../../types/errors";
|
import { HoppErrorCode } from "../../types/errors";
|
||||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||||
|
|
||||||
describe("Test 'hopp test <file>' command:", () => {
|
describe("Test `hopp test <file>` command:", () => {
|
||||||
test("No collection file path provided.", async () => {
|
describe("Argument parsing", () => {
|
||||||
const args = "test";
|
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||||
const { stderr } = await runCLI(args);
|
const args = "test";
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not found.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||||
const args = "test notfound.json";
|
const args = "invalid-arg";
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
|
||||||
test("Collection file is invalid JSON.", async () => {
|
describe("Supplied collection export file validations", () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||||
"malformed-collection.json"
|
const args = "test notfound.json";
|
||||||
)}`;
|
const { stderr } = await runCLI(args);
|
||||||
const { stderr } = await runCLI(args);
|
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Malformed collection file.", async () => {
|
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
|
||||||
"malformed-collection2.json"
|
const { stderr } = await runCLI(args);
|
||||||
)}`;
|
|
||||||
const { stderr } = await runCLI(args);
|
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid arguement.", async () => {
|
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
|
||||||
const args = "invalid-arg";
|
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not JSON type.", async () => {
|
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.txt")}`;
|
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Some errors occured (exit code 1).", async () => {
|
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("fails.json")}`;
|
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).not.toBeNull();
|
expect(error).not.toBeNull();
|
||||||
expect(error).toMatchObject(<ExecException>{
|
expect(error).toMatchObject(<ExecException>{
|
||||||
code: 1,
|
code: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("No errors occured (exit code 0).", async () => {
|
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
const args = `test ${getTestJsonFilePath(
|
||||||
|
"collection-level-headers-auth-coll.json", "collection"
|
||||||
|
)}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
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:", () => {
|
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
describe("Supplied environment export file validations", () => {
|
||||||
"passes.json"
|
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
)}`;
|
|
||||||
|
|
||||||
test("No env file path provided.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --env`;
|
const args = `${VALID_TEST_ARGS} --env`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
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 () => {
|
test("Successfully resolves values from the supplied environment export file", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
|
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
||||||
const { stderr } = await runCLI(args);
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||||
|
|
||||||
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");
|
|
||||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
|
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Correctly resolves environment variables referenced in the request body", async () => {
|
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json");
|
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
||||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json");
|
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
expect(error).toBeNull();
|
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(10000);
|
||||||
|
|
||||||
|
// 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:", () => {
|
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
"passes.json"
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
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 args = `${VALID_TEST_ARGS} --delay`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -142,7 +251,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
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 args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -150,10 +259,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
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 args = `${VALID_TEST_ARGS} --delay 1`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestD",
|
"name": "RequestD",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestC",
|
"name": "RequestC",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -67,7 +68,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -88,7 +90,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -104,6 +106,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -132,6 +135,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -174,6 +178,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -186,7 +191,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -202,6 +207,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "<<URL>>",
|
"endpoint": "<<URL>>",
|
||||||
"name": "test1",
|
"name": "test1",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -44,7 +45,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -43,7 +44,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -44,7 +45,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "pre-req-script-env-var-persistence-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"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": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"name": "test-request",
|
"name": "test-request",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||||
},
|
},
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
|
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "secret-envs-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"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": "<<baseURL>>/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": "2",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": {
|
||||||
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
|
"contentType": "application/json"
|
||||||
|
},
|
||||||
|
"name": "test-secret-body",
|
||||||
|
"method": "POST",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
|
"endpoint": "<<baseURL>>/post",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
|
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"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": "<<baseURL>>/get",
|
||||||
|
"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": "2",
|
||||||
|
"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": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
|
"preRequestScript": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"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": "<<baseURL>>/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 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": "2",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-fallback",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
|
"endpoint": "<<baseURL>>",
|
||||||
|
"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-setters-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "2",
|
||||||
|
"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": "<<baseURL>>/headers",
|
||||||
|
"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": "2",
|
||||||
|
"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": "<<baseURL>>/headers",
|
||||||
|
"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": "2",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
|
"contentType": "application/json"
|
||||||
|
},
|
||||||
|
"name": "test-secret-body",
|
||||||
|
"method": "POST",
|
||||||
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/post",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
|
"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": "2",
|
||||||
|
"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": "<<baseURL>>/get",
|
||||||
|
"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": "2",
|
||||||
|
"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": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
|
"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": "2",
|
||||||
|
"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": "<<baseURL>>/bearer",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
|
"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": "2",
|
||||||
|
"endpoint": "https://httpbin.org/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 sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
|
||||||
|
"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,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",
|
"name": "Response body sample",
|
||||||
"variables": [
|
"variables": [
|
||||||
{
|
{
|
||||||
@@ -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,40 @@
|
|||||||
|
{
|
||||||
|
"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": "baseURL",
|
||||||
|
"value": "https://httpbin.org",
|
||||||
|
"secret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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": "baseURL",
|
||||||
|
"value": "https://httpbin.org",
|
||||||
|
"secret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@ import { resolve } from "path";
|
|||||||
|
|
||||||
import { ExecResponse } from "./types";
|
import { ExecResponse } from "./types";
|
||||||
|
|
||||||
export const runCLI = (args: string): Promise<ExecResponse> =>
|
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
|
||||||
{
|
{
|
||||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||||
const command = `node ${CLI_PATH} ${args}`
|
const command = `node ${CLI_PATH} ${args}`
|
||||||
|
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,12 @@ export const getErrorCode = (out: string) => {
|
|||||||
return ansiTrimmedStr.split(" ")[0];
|
return ansiTrimmedStr.split(" ")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTestJsonFilePath = (file: string) => {
|
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
||||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`);
|
const kindDir = {
|
||||||
|
collection: "collections",
|
||||||
|
environment: "environments",
|
||||||
|
}[kind];
|
||||||
|
|
||||||
|
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
||||||
return filePath;
|
return filePath;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface RequestStack {
|
|||||||
*/
|
*/
|
||||||
export interface RequestConfig extends AxiosRequestConfig {
|
export interface RequestConfig extends AxiosRequestConfig {
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
|
displayUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
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
|
* This contains path, params and environment variables all applied to it
|
||||||
*/
|
*/
|
||||||
effectiveFinalURL: string;
|
effectiveFinalURL: string;
|
||||||
|
effectiveFinalDisplayURL?: string;
|
||||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||||
effectiveFinalBody: FormData | string | null;
|
effectiveFinalBody: FormData | string | null;
|
||||||
|
|||||||
@@ -1,34 +1,42 @@
|
|||||||
|
import { Environment } from "@hoppscotch/data";
|
||||||
|
import { entityReference } from "verzod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { error } from "../../types/errors";
|
import { error } from "../../types/errors";
|
||||||
import {
|
import {
|
||||||
HoppEnvs,
|
|
||||||
HoppEnvPair,
|
|
||||||
HoppEnvKeyPairObject,
|
HoppEnvKeyPairObject,
|
||||||
HoppEnvExportObject,
|
HoppEnvPair,
|
||||||
HoppBulkEnvExportObject,
|
HoppEnvs
|
||||||
} from "../../types/request";
|
} from "../../types/request";
|
||||||
import { readJsonFile } from "../../utils/mutators";
|
import { readJsonFile } from "../../utils/mutators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses env json file for given path and validates the parsed env json 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.
|
* @param path Path of env.json file to be parsed
|
||||||
* @returns For successful parsing we get HoppEnvs object.
|
* @returns For successful parsing we get HoppEnvs object
|
||||||
*/
|
*/
|
||||||
export async function parseEnvsData(path: string) {
|
export async function parseEnvsData(path: string) {
|
||||||
const contents = await readJsonFile(path);
|
const contents = await readJsonFile(path);
|
||||||
const envPairs: Array<HoppEnvPair> = [];
|
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
||||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
|
||||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
|
||||||
const HoppBulkEnvExportObjectResult =
|
|
||||||
HoppBulkEnvExportObject.safeParse(contents);
|
|
||||||
|
|
||||||
// CLI doesnt support bulk environments export.
|
// The legacy key-value pair format that is still supported
|
||||||
// Hence we check for this case and throw an error if it matches the format.
|
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) {
|
if (HoppBulkEnvExportObjectResult.success) {
|
||||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the environment file is of the correct format.
|
// Checks if the environment file is of the correct format
|
||||||
// If it doesnt match either of them, we throw an error.
|
// If it doesnt match either of them, we throw an error
|
||||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
||||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +44,8 @@ export async function parseEnvsData(path: string) {
|
|||||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||||
envPairs.push({ key, value });
|
envPairs.push({ key, value });
|
||||||
}
|
}
|
||||||
} else if (HoppEnvExportObjectResult.success) {
|
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||||
envPairs.push(...HoppEnvExportObjectResult.data.variables);
|
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
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 { TestReport } from "../interfaces/response";
|
||||||
import { HoppCLIError } from "./errors";
|
import { HoppCLIError } from "./errors";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export type FormDataEntry = {
|
export type FormDataEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
value: string | Blob;
|
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());
|
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 = {
|
export type HoppEnvs = {
|
||||||
global: HoppEnvPair[];
|
global: HoppEnvPair[];
|
||||||
selected: HoppEnvPair[];
|
selected: HoppEnvPair[];
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
|||||||
*/
|
*/
|
||||||
start: (requestConfig: RequestConfig) => {
|
start: (requestConfig: RequestConfig) => {
|
||||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||||
const ENDPOINT = requestConfig.url;
|
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||||
|
|
||||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
|
|||||||
export const preRequestScriptRunner = (
|
export const preRequestScriptRunner = (
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
envs: HoppEnvs
|
envs: HoppEnvs
|
||||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
): TE.TaskEither<
|
||||||
|
HoppCLIError,
|
||||||
|
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||||
|
> =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.of(request),
|
TE.of(request),
|
||||||
TE.chain(({ preRequestScript }) =>
|
TE.chain(({ preRequestScript }) =>
|
||||||
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
|
|||||||
export function getEffectiveRESTRequest(
|
export function getEffectiveRESTRequest(
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
environment: Environment
|
environment: Environment
|
||||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
): E.Either<
|
||||||
|
HoppCLIError,
|
||||||
|
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||||
|
> {
|
||||||
const envVariables = environment.variables;
|
const envVariables = environment.variables;
|
||||||
|
|
||||||
// Parsing final headers with applied ENVs.
|
// Parsing final headers with applied ENVs.
|
||||||
@@ -162,12 +168,30 @@ export function getEffectiveRESTRequest(
|
|||||||
}
|
}
|
||||||
const effectiveFinalURL = _effectiveFinalURL.right;
|
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({
|
return E.right({
|
||||||
...request,
|
effectiveRequest: {
|
||||||
effectiveFinalURL,
|
...request,
|
||||||
effectiveFinalHeaders,
|
effectiveFinalURL,
|
||||||
effectiveFinalParams,
|
effectiveFinalDisplayURL,
|
||||||
effectiveFinalBody,
|
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 axios, { Method } from "axios";
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import * as E from "fp-ts/Either";
|
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
|
// !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
|
* Transforms given request data to request-config used by request-runner to
|
||||||
* perform HTTP request.
|
* perform HTTP request.
|
||||||
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
|||||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||||
const config: RequestConfig = {
|
const config: RequestConfig = {
|
||||||
supported: true,
|
supported: true,
|
||||||
|
displayUrl: req.effectiveFinalDisplayURL
|
||||||
};
|
};
|
||||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||||
const reqParams = finalParams(req);
|
const reqParams = finalParams(req);
|
||||||
@@ -221,9 +254,13 @@ export const processRequest =
|
|||||||
effectiveFinalParams: [],
|
effectiveFinalParams: [],
|
||||||
effectiveFinalURL: "",
|
effectiveFinalURL: "",
|
||||||
};
|
};
|
||||||
|
let updatedEnvs = <HoppEnvs>{};
|
||||||
|
|
||||||
|
// Fetch values for secret environment variables from system environment
|
||||||
|
const processedEnvs = processEnvs(envs)
|
||||||
|
|
||||||
// Executing pre-request-script
|
// Executing pre-request-script
|
||||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
|
||||||
if (E.isLeft(preRequestRes)) {
|
if (E.isLeft(preRequestRes)) {
|
||||||
printPreRequestRunner.fail();
|
printPreRequestRunner.fail();
|
||||||
|
|
||||||
@@ -231,8 +268,8 @@ export const processRequest =
|
|||||||
report.errors.push(preRequestRes.left);
|
report.errors.push(preRequestRes.left);
|
||||||
report.result = report.result && false;
|
report.result = report.result && false;
|
||||||
} else {
|
} else {
|
||||||
// Updating effective-request
|
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||||
effectiveRequest = preRequestRes.right;
|
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creating request-config for request-runner.
|
// Creating request-config for request-runner.
|
||||||
@@ -270,7 +307,7 @@ export const processRequest =
|
|||||||
const testScriptParams = getTestScriptParams(
|
const testScriptParams = getTestScriptParams(
|
||||||
_requestRunnerRes,
|
_requestRunnerRes,
|
||||||
request,
|
request,
|
||||||
envs
|
updatedEnvs
|
||||||
);
|
);
|
||||||
|
|
||||||
// Executing test-runner.
|
// Executing test-runner.
|
||||||
|
|||||||
@@ -429,6 +429,11 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splitpanes__pane {
|
||||||
|
@apply will-change-auto;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
.smart-splitter .splitpanes__splitter {
|
.smart-splitter .splitpanes__splitter {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply before:absolute;
|
@apply before:absolute;
|
||||||
@@ -558,12 +563,22 @@ details[open] summary .indicator {
|
|||||||
.env-highlight {
|
.env-highlight {
|
||||||
@apply text-accentContrast;
|
@apply text-accentContrast;
|
||||||
|
|
||||||
&.env-found {
|
&.request-variable-highlight {
|
||||||
@apply bg-accentDark;
|
@apply bg-amber-500;
|
||||||
@apply hover:bg-accent;
|
@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 bg-red-500;
|
||||||
@apply hover:bg-red-600;
|
@apply hover:bg-red-600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "Go forward",
|
||||||
"group_by": "Group by",
|
"group_by": "Group by",
|
||||||
|
"hide_secret": "Hide secret",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
"less": "Less",
|
"less": "Less",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
|
"show_secret": "Show secret",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -152,7 +154,7 @@
|
|||||||
"invalid_name": "Please provide a name for the collection",
|
"invalid_name": "Please provide a name for the collection",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "Collection already in the root",
|
||||||
"moved": "Moved Successfully",
|
"moved": "Moved Successfully",
|
||||||
"my_collections": "My Collections",
|
"my_collections": "Personal Collections",
|
||||||
"name": "My New Collection",
|
"name": "My New Collection",
|
||||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||||
"new": "New Collection",
|
"new": "New Collection",
|
||||||
@@ -164,14 +166,12 @@
|
|||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"save_to_collection": "Save to Collection",
|
"save_to_collection": "Save to Collection",
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location"
|
||||||
"select_team": "Select a team",
|
|
||||||
"team_collections": "Team Collections"
|
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
"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_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?",
|
"logout": "Are you sure you want to logout?",
|
||||||
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
||||||
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
"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_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_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?",
|
"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.",
|
"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?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
@@ -232,17 +232,19 @@
|
|||||||
"headers": "This request does not have any headers",
|
"headers": "This request does not have any headers",
|
||||||
"history": "History is empty",
|
"history": "History is empty",
|
||||||
"invites": "Invite list is empty",
|
"invites": "Invite list is empty",
|
||||||
"members": "Team is empty",
|
"members": "Workspace is empty",
|
||||||
"parameters": "This request does not have any parameters",
|
"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",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
|
"request_variables": "This request does not have any request variables",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"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": "Shared requests are empty",
|
||||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Workspace name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any workspaces",
|
||||||
"tests": "There are no tests for this request"
|
"tests": "There are no tests for this request"
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
@@ -259,7 +261,7 @@
|
|||||||
"import_or_create": "Import or create a environment",
|
"import_or_create": "Import or create a environment",
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
"list": "Environment variables",
|
"list": "Environment variables",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "Personal Environments",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||||
"new": "New Environment",
|
"new": "New Environment",
|
||||||
@@ -269,14 +271,17 @@
|
|||||||
"quick_peek": "Environment Quick Peek",
|
"quick_peek": "Environment Quick Peek",
|
||||||
"replace_with_variable": "Replace with variable",
|
"replace_with_variable": "Replace with variable",
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secret_value": "Secret value",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
"set": "Set environment",
|
"set": "Set environment",
|
||||||
"set_as_environment": "Set as environment",
|
"set_as_environment": "Set as environment",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Workspace Environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
|
"variables":"Variables",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -286,8 +291,8 @@
|
|||||||
"check_how_to_add_origin": "Check how you can add an origin",
|
"check_how_to_add_origin": "Check how you can add an origin",
|
||||||
"curl_invalid_format": "cURL is not formatted properly",
|
"curl_invalid_format": "cURL is not formatted properly",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Danger zone",
|
||||||
"delete_account": "Your account is currently an owner in these teams:",
|
"delete_account": "Your account is currently an owner in these workspaces:",
|
||||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
|
||||||
"empty_req_name": "Empty Request Name",
|
"empty_req_name": "Empty Request Name",
|
||||||
"f12_details": "(F12 for details)",
|
"f12_details": "(F12 for details)",
|
||||||
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
||||||
@@ -309,7 +314,8 @@
|
|||||||
"proxy_error": "Proxy error",
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"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": {
|
"export": {
|
||||||
"as_json": "Export as JSON",
|
"as_json": "Export as JSON",
|
||||||
@@ -388,8 +394,8 @@
|
|||||||
"from_insomnia_description": "Import from Insomnia collection",
|
"from_insomnia_description": "Import from Insomnia collection",
|
||||||
"from_json": "Import from Hoppscotch",
|
"from_json": "Import from Hoppscotch",
|
||||||
"from_json_description": "Import from Hoppscotch collection file",
|
"from_json_description": "Import from Hoppscotch collection file",
|
||||||
"from_my_collections": "Import from My Collections",
|
"from_my_collections": "Import from Personal Collections",
|
||||||
"from_my_collections_description": "Import from My Collections file",
|
"from_my_collections_description": "Import from Personal Collections file",
|
||||||
"from_openapi": "Import from OpenAPI",
|
"from_openapi": "Import from OpenAPI",
|
||||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||||
"from_postman": "Import from Postman",
|
"from_postman": "Import from Postman",
|
||||||
@@ -407,12 +413,17 @@
|
|||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"postman_environment": "Postman Environment",
|
"postman_environment": "Postman Environment",
|
||||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
"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": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
"environment": {
|
"environment": {
|
||||||
"add_environment": "Add to 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."
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
@@ -501,7 +512,7 @@
|
|||||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
"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.",
|
"no_permission": "You do not have permission to perform this action.",
|
||||||
"owner": "Owner",
|
"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": "Roles",
|
||||||
"roles_description": "Roles are used to control access to the shared collections.",
|
"roles_description": "Roles are used to control access to the shared collections.",
|
||||||
"updated": "Profile updated",
|
"updated": "Profile updated",
|
||||||
@@ -548,6 +559,7 @@
|
|||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
"rename": "Rename Request",
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
|
"request_variables": "Request variables",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
@@ -805,12 +817,12 @@
|
|||||||
"title": "Tabs"
|
"title": "Tabs"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"delete": "Delete current team",
|
"delete": "Delete current workspace",
|
||||||
"edit": "Edit current team",
|
"edit": "Edit current workspace",
|
||||||
"invite": "Invite people to team",
|
"invite": "Invite people to workspace",
|
||||||
"new": "Create new team",
|
"new": "Create new workspace",
|
||||||
"switch_to_personal": "Switch to your personal workspace",
|
"switch_to_personal": "Switch to your personal workspace",
|
||||||
"title": "Teams"
|
"title": "Workspaces"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
@@ -867,7 +879,6 @@
|
|||||||
"forum": "Ask questions and get answers",
|
"forum": "Ask questions and get answers",
|
||||||
"github": "Follow us on Github",
|
"github": "Follow us on Github",
|
||||||
"shortcuts": "Browse app faster",
|
"shortcuts": "Browse app faster",
|
||||||
"team": "Get in touch with the team",
|
|
||||||
"title": "Support",
|
"title": "Support",
|
||||||
"twitter": "Follow us on Twitter"
|
"twitter": "Follow us on Twitter"
|
||||||
},
|
},
|
||||||
@@ -889,6 +900,7 @@
|
|||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"shared_requests": "Shared Requests",
|
"shared_requests": "Shared Requests",
|
||||||
|
"share_tab_request": "Share tab request",
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
@@ -897,60 +909,59 @@
|
|||||||
"websocket": "WebSocket"
|
"websocket": "WebSocket"
|
||||||
},
|
},
|
||||||
"team": {
|
"team": {
|
||||||
"already_member": "You are already a member of this team. Contact your team owner.",
|
"already_member": "You are already a member of this workspace. Contact your workspace owner.",
|
||||||
"create_new": "Create new team",
|
"create_new": "Create new workspace",
|
||||||
"deleted": "Team deleted",
|
"deleted": "Workspace deleted",
|
||||||
"edit": "Edit Team",
|
"edit": "Edit Workspace",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
"email_do_not_match": "Email doesn't match with your account details. Contact your workspace owner.",
|
||||||
"exit": "Exit Team",
|
"exit": "Exit Workspace",
|
||||||
"exit_disabled": "Only owner cannot exit the team",
|
"exit_disabled": "Only owner cannot exit the workspace",
|
||||||
"failed_invites": "Failed invites",
|
"failed_invites": "Failed invites",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "Invalid collection ID",
|
||||||
"invalid_email_format": "Email format is invalid",
|
"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": "Invalid invite link",
|
||||||
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
|
"invalid_invite_link_description": "The link you followed is invalid. Contact your workspace owner.",
|
||||||
"invalid_member_permission": "Please provide a valid permission to the team member",
|
"invalid_member_permission": "Please provide a valid permission to the workspace member",
|
||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"invite_more": "Invite more",
|
"invite_more": "Invite more",
|
||||||
"invite_tooltip": "Invite people to this workspace",
|
"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": "Invitation accepted",
|
||||||
"join_beta": "Join the beta program to access teams.",
|
"join_team": "Join {workspace}",
|
||||||
"join_team": "Join {team}",
|
"joined_team": "You have joined {workspace}",
|
||||||
"joined_team": "You have joined {team}",
|
"joined_team_description": "You are now a member of this workspace",
|
||||||
"joined_team_description": "You are now a member of this team",
|
"left": "You left the workspace",
|
||||||
"left": "You left the team",
|
|
||||||
"login_to_continue": "Login to continue",
|
"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",
|
"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_has_invite": "This email ID already has an invite. Contact your workspace owner.",
|
||||||
"member_not_found": "Member not found. Contact your team owner.",
|
"member_not_found": "Member not found. Contact your workspace owner.",
|
||||||
"member_removed": "User removed",
|
"member_removed": "User removed",
|
||||||
"member_role_updated": "User roles updated",
|
"member_role_updated": "User roles updated",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"more_members": "+{count} more",
|
"more_members": "+{count} more",
|
||||||
"name_length_insufficient": "Team name should be at least 6 characters long",
|
"name_length_insufficient": "Workspace name should be at least 6 characters long",
|
||||||
"name_updated": "Team name updated",
|
"name_updated": "Workspace name updated",
|
||||||
"new": "New Team",
|
"new": "New Workspace",
|
||||||
"new_created": "New team created",
|
"new_created": "New workspace created",
|
||||||
"new_name": "My New Team",
|
"new_name": "My New Workspace",
|
||||||
"no_access": "You do not have edit access to this team",
|
"no_access": "You do not have edit access to this workspace",
|
||||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
"no_invite_found": "Invitation not found. Contact your workspace owner.",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "Request not found.",
|
||||||
"not_found": "Team not found. Contact your team owner.",
|
"not_found": "Workspace not found. Contact your workspace owner.",
|
||||||
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
|
"not_valid_viewer": "You are not a valid viewer. Contact your workspace owner.",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "Cannot move collection to a child collection",
|
||||||
"pending_invites": "Pending invites",
|
"pending_invites": "Pending invites",
|
||||||
"permissions": "Permissions",
|
"permissions": "Permissions",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "Same target and destination",
|
||||||
"saved": "Team saved",
|
"saved": "Workspace saved",
|
||||||
"select_a_team": "Select a team",
|
"select_a_team": "Select a workspace",
|
||||||
"success_invites": "Success invites",
|
"success_invites": "Success invites",
|
||||||
"title": "Teams",
|
"title": "Workspaces",
|
||||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
"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."
|
||||||
},
|
},
|
||||||
"team_environment": {
|
"team_environment": {
|
||||||
"deleted": "Environment Deleted",
|
"deleted": "Environment Deleted",
|
||||||
@@ -976,8 +987,15 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "Change workspace",
|
||||||
"personal": "My Workspace",
|
"personal": "Personal Workspace",
|
||||||
"team": "Team Workspace",
|
"personal_workspace": "{name}'s Workspace",
|
||||||
|
"other_workspaces": "My Workspaces",
|
||||||
|
"team": "Workspace",
|
||||||
"title": "Workspaces"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.12.3",
|
"version": "2024.3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
@@ -21,147 +21,147 @@
|
|||||||
"do-lintfix": "pnpm run lintfix"
|
"do-lintfix": "pnpm run lintfix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.11.1",
|
"@codemirror/autocomplete": "6.13.0",
|
||||||
"@codemirror/commands": "^6.3.2",
|
"@codemirror/commands": "6.3.3",
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "6.2.2",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "6.0.1",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
"@codemirror/language": "6.9.3",
|
"@codemirror/language": "6.10.1",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/lint": "^6.4.2",
|
"@codemirror/lint": "6.5.0",
|
||||||
"@codemirror/search": "^6.5.5",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "^6.3.3",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "^6.22.3",
|
"@codemirror/view": "6.25.1",
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "^0.1.0",
|
"@hoppscotch/ui": "0.1.0",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "0.1.0",
|
||||||
"@lezer/highlight": "1.2.0",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@unhead/vue": "^1.8.8",
|
"@unhead/vue": "1.8.8",
|
||||||
"@urql/core": "^4.2.0",
|
"@urql/core": "4.2.0",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "2.1.6",
|
||||||
"@urql/exchange-graphcache": "^6.3.3",
|
"@urql/exchange-graphcache": "6.4.0",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "4.1.1",
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "10.7.0",
|
||||||
"acorn-walk": "^8.3.0",
|
"acorn-walk": "8.3.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "1.6.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "6.0.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "1.0.0",
|
||||||
"dioc": "^1.0.1",
|
"dioc": "1.0.1",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "2.16.1",
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "1.0.3",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "16.8.1",
|
||||||
"graphql-language-service-interface": "^2.10.2",
|
"graphql-language-service-interface": "2.10.2",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
"httpsnippet": "^3.0.1",
|
"httpsnippet": "3.0.1",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "3.6.0",
|
||||||
"io-ts": "^2.2.20",
|
"io-ts": "2.2.20",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonpath-plus": "^7.2.0",
|
"jsonpath-plus": "7.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"lossless-json": "^3.0.2",
|
"lossless-json": "3.0.2",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "6.3.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "0.12.7",
|
||||||
"postman-collection": "^4.3.0",
|
"postman-collection": "4.3.0",
|
||||||
"process": "^0.11.10",
|
"process": "0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "6.11.2",
|
||||||
"quicktype-core": "^23.0.79",
|
"quicktype-core": "23.0.79",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "2.6.0",
|
||||||
"set-cookie-parser-es": "^1.0.5",
|
"set-cookie-parser-es": "1.0.5",
|
||||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
"socket.io-client-v2": "npm:socket.io-client@2.4.0",
|
||||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
"socket.io-client-v3": "npm:socket.io-client@3.1.3",
|
||||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
"socket.io-client-v4": "npm:socket.io-client@4.4.1",
|
||||||
"socketio-wildcard": "^2.0.0",
|
"socketio-wildcard": "2.0.0",
|
||||||
"splitpanes": "^3.1.5",
|
"splitpanes": "3.1.5",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "0.11.0",
|
||||||
"tern": "^0.24.3",
|
"tern": "0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"url": "^0.11.3",
|
"url": "0.11.3",
|
||||||
"util": "^0.12.5",
|
"util": "0.12.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "9.0.1",
|
||||||
"verzod": "^0.2.0",
|
"verzod": "0.2.2",
|
||||||
"vue": "^3.3.8",
|
"vue": "3.3.9",
|
||||||
"vue-i18n": "^9.7.1",
|
"vue-i18n": "9.8.0",
|
||||||
"vue-pdf-embed": "^1.2.1",
|
"vue-pdf-embed": "1.2.1",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "4.2.5",
|
||||||
"vue-tippy": "6.3.1",
|
"vue-tippy": "6.3.1",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "4.1.1",
|
||||||
"wonka": "^6.3.4",
|
"wonka": "6.3.4",
|
||||||
"workbox-window": "^7.0.0",
|
"workbox-window": "7.0.0",
|
||||||
"xml-formatter": "^3.6.0",
|
"xml-formatter": "3.6.0",
|
||||||
"yargs-parser": "^21.1.1",
|
"yargs-parser": "21.1.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
"@esbuild-plugins/node-modules-polyfill": "0.2.2",
|
||||||
"@graphql-codegen/add": "^5.0.0",
|
"@graphql-codegen/add": "5.0.0",
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "5.0.0",
|
||||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "4.0.1",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
"@graphql-codegen/typescript-urql-graphcache": "3.0.0",
|
||||||
"@graphql-codegen/urql-introspection": "^3.0.0",
|
"@graphql-codegen/urql-introspection": "3.0.0",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@iconify-json/lucide": "^1.1.141",
|
"@iconify-json/lucide": "1.1.144",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.6.0",
|
"@rushstack/eslint-patch": "1.6.0",
|
||||||
"@types/har-format": "^1.2.15",
|
"@types/har-format": "1.2.15",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/lossless-json": "^1.0.4",
|
"@types/lossless-json": "1.0.4",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/paho-mqtt": "^1.0.10",
|
"@types/paho-mqtt": "1.0.10",
|
||||||
"@types/postman-collection": "^3.5.10",
|
"@types/postman-collection": "3.5.10",
|
||||||
"@types/splitpanes": "^2.2.6",
|
"@types/splitpanes": "2.2.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "9.0.7",
|
||||||
"@types/yargs-parser": "^21.0.3",
|
"@types/yargs-parser": "21.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "6.13.2",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "6.13.2",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "4.5.1",
|
||||||
"@vue/compiler-sfc": "^3.3.8",
|
"@vue/compiler-sfc": "3.3.10",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/runtime-core": "^3.3.8",
|
"@vue/runtime-core": "3.3.10",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "16.3.1",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "8.55.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "5.0.1",
|
||||||
"eslint-plugin-vue": "^9.18.1",
|
"eslint-plugin-vue": "9.19.2",
|
||||||
"glob": "^10.3.10",
|
"glob": "10.3.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "12.1.3",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "8.4.31",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "3.1.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
"prettier-plugin-tailwindcss": "0.5.7",
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"rollup-plugin-polyfill-node": "0.13.0",
|
||||||
"sass": "^1.69.5",
|
"sass": "1.69.5",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "3.3.5",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "5.3.2",
|
||||||
"unplugin-fonts": "^1.1.1",
|
"unplugin-fonts": "1.1.1",
|
||||||
"unplugin-icons": "^0.17.4",
|
"unplugin-icons": "0.17.4",
|
||||||
"unplugin-vue-components": "^0.25.2",
|
"unplugin-vue-components": "0.25.2",
|
||||||
"vite": "^4.5.0",
|
"vite": "4.5.0",
|
||||||
"vite-plugin-checker": "^0.6.2",
|
"vite-plugin-checker": "0.6.2",
|
||||||
"vite-plugin-fonts": "^0.7.0",
|
"vite-plugin-fonts": "0.7.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.42",
|
"vite-plugin-inspect": "0.7.42",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "0.31.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
"vite-plugin-pages-sitemap": "1.6.1",
|
||||||
"vite-plugin-pwa": "^0.17.0",
|
"vite-plugin-pwa": "0.17.3",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "0.8.0",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "0.34.6",
|
||||||
"vue-tsc": "^1.8.22"
|
"vue-tsc": "1.8.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
packages/hoppscotch-common/src/components.d.ts
vendored
13
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,11 +1,11 @@
|
|||||||
// generated by unplugin-vue-components
|
/* eslint-disable */
|
||||||
// We suggest you to commit this file into source control
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
import '@vue/runtime-core'
|
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppBanner: typeof import('./components/app/Banner.vue')['default']
|
AppBanner: typeof import('./components/app/Banner.vue')['default']
|
||||||
@@ -145,6 +145,7 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||||
|
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
@@ -154,6 +155,7 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||||
@@ -209,5 +211,4 @@ declare module '@vue/runtime-core' {
|
|||||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="contextMenuRef"
|
ref="contextMenuRef"
|
||||||
class="fixed translate-y-8 transform rounded border border-dividerDark bg-popover p-2 shadow-lg"
|
class="fixed transform -translate-x-10 -translate-y-8 rounded border border-dividerDark bg-popover p-2 shadow-lg"
|
||||||
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 100;`"
|
||||||
>
|
>
|
||||||
<div v-if="contextMenuOptions" class="flex flex-col">
|
<div v-if="contextMenuOptions" class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle', undefined, 'mouseclick')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
<icon-lucide-search class="svg-icons mr-2" />
|
<icon-lucide-search class="svg-icons mr-2" />
|
||||||
@@ -330,11 +330,16 @@ const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
|||||||
|
|
||||||
const workspace = workspaceService.currentWorkspace
|
const workspace = workspaceService.currentWorkspace
|
||||||
|
|
||||||
const workspaceName = computed(() =>
|
const workspaceName = computed(() => {
|
||||||
workspace.value.type === "personal"
|
if (workspace.value.type === "personal") {
|
||||||
? t("workspace.personal")
|
return currentUser.value?.displayName
|
||||||
: workspace.value.teamName
|
? t("workspace.personal_workspace", {
|
||||||
)
|
name: currentUser.value.displayName,
|
||||||
|
})
|
||||||
|
: t("workspace.personal")
|
||||||
|
}
|
||||||
|
return workspace.value.teamName
|
||||||
|
})
|
||||||
|
|
||||||
const refetchTeams = () => {
|
const refetchTeams = () => {
|
||||||
teamListAdapter.fetchList()
|
teamListAdapter.fetchList()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
styles="sm:max-w-lg"
|
styles="sm:max-w-lg"
|
||||||
full-width
|
full-width
|
||||||
@close="emit('hide-modal')"
|
@close="closeSpotlightModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col border-b border-divider transition">
|
<div class="flex flex-col border-b border-divider transition">
|
||||||
@@ -86,35 +86,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
import { computed, ref, watch } from "vue"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||||
import {
|
import {
|
||||||
SpotlightService,
|
|
||||||
SpotlightSearchState,
|
SpotlightSearchState,
|
||||||
SpotlightSearcherResult,
|
SpotlightSearcherResult,
|
||||||
|
SpotlightService,
|
||||||
} from "~/services/spotlight"
|
} from "~/services/spotlight"
|
||||||
import { isEqual } from "lodash-es"
|
|
||||||
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
|
||||||
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
|
||||||
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
|
||||||
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
|
||||||
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
||||||
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
|
||||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
|
||||||
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
|
||||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
|
||||||
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
|
||||||
import {
|
import {
|
||||||
EnvironmentsSpotlightSearcherService,
|
EnvironmentsSpotlightSearcherService,
|
||||||
SwitchEnvSpotlightSearcherService,
|
SwitchEnvSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/environment.searcher"
|
} from "~/services/spotlight/searchers/environment.searcher"
|
||||||
|
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
||||||
|
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
||||||
|
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||||
|
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
||||||
|
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
||||||
|
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
||||||
|
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
||||||
|
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
||||||
|
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
||||||
|
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
||||||
import {
|
import {
|
||||||
SwitchWorkspaceSpotlightSearcherService,
|
SwitchWorkspaceSpotlightSearcherService,
|
||||||
WorkspaceSpotlightSearcherService,
|
WorkspaceSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -290,4 +291,17 @@ function newUseArrowKeysForNavigation() {
|
|||||||
|
|
||||||
return { selectedEntry }
|
return { selectedEntry }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeSpotlightModal() {
|
||||||
|
const analyticsData: HoppSpotlightSessionEventData = {
|
||||||
|
action: "close",
|
||||||
|
searcherID: null,
|
||||||
|
rank: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the action indicating `close` and rank as `null` in the state for analytics event logging
|
||||||
|
spotlightService.setAnalyticsData(analyticsData)
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
|
|||||||
step: UrlSource({
|
step: UrlSource({
|
||||||
caption: "import.from_url",
|
caption: "import.from_url",
|
||||||
onImportFromURL: async (content) => {
|
onImportFromURL: async (content) => {
|
||||||
const res = await hoppOpenAPIImporter(content)()
|
const res = await hoppOpenAPIImporter([content])()
|
||||||
|
|
||||||
if (E.isRight(res)) {
|
if (E.isRight(res)) {
|
||||||
handleImportToStore(res.right)
|
handleImportToStore(res.right)
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
|||||||
let target = collections[indexPaths.shift() as number]
|
let target = collections[indexPaths.shift() as number]
|
||||||
|
|
||||||
while (indexPaths.length > 0)
|
while (indexPaths.length > 0)
|
||||||
target = target.folders[indexPaths.shift() as number]
|
target = target?.folders[indexPaths.shift() as number]
|
||||||
|
|
||||||
return target !== undefined ? target : null
|
return target !== undefined ? target : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,8 +614,8 @@ const addNewRootCollection = (name: string) => {
|
|||||||
requests: [],
|
requests: [],
|
||||||
headers: [],
|
headers: [],
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: false,
|
authActive: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'cookie')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -102,6 +102,8 @@ import {
|
|||||||
useCopyResponse,
|
useCopyResponse,
|
||||||
useDownloadResponse,
|
useDownloadResponse,
|
||||||
} from "~/composables/lens-actions"
|
} from "~/composables/lens-actions"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
// TODO: Build Managed Mode!
|
// TODO: Build Managed Mode!
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ const toast = useToast()
|
|||||||
|
|
||||||
const cookieEditor = ref<HTMLElement>()
|
const cookieEditor = ref<HTMLElement>()
|
||||||
const rawCookieString = ref("")
|
const rawCookieString = ref("")
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "cookie")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
cookieEditor,
|
cookieEditor,
|
||||||
@@ -131,7 +133,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/plain",
|
mode: "text/plain",
|
||||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -32,9 +32,13 @@
|
|||||||
{{ tab.document.request.method }}
|
{{ tab.document.request.method }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
|
class="flex items-center flex-1 flex-shrink-0 min-w-0 truncate rounded-r"
|
||||||
>
|
>
|
||||||
{{ tab.document.request.endpoint }}
|
<SmartEnvInput
|
||||||
|
v-model="tab.document.request.endpoint"
|
||||||
|
:readonly="true"
|
||||||
|
:envs="tabRequestVariables"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 space-x-2 sm:mt-0">
|
<div class="flex mt-2 space-x-2 sm:mt-0">
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
v-model="tab.document.request"
|
v-model="tab.document.request"
|
||||||
v-model:option-tab="selectedOptionTab"
|
v-model:option-tab="selectedOptionTab"
|
||||||
:properties="properties"
|
:properties="properties"
|
||||||
|
:envs="tabRequestVariables"
|
||||||
/>
|
/>
|
||||||
<HttpResponse
|
<HttpResponse
|
||||||
v-if="tab.document.response"
|
v-if="tab.document.response"
|
||||||
@@ -117,6 +122,15 @@ const sharedRequestURL = computed(() => {
|
|||||||
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tabRequestVariables = computed(() => {
|
||||||
|
return tab.value.document.request.requestVariables.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
secret: false,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<SmartEnvInput
|
||||||
v-model="editingValue"
|
v-model="editingValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="input"
|
class="input"
|
||||||
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
|
|||||||
addGlobalEnvVariable({
|
addGlobalEnvVariable({
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else if (scope.value.type === "my-environment") {
|
} else if (scope.value.type === "my-environment") {
|
||||||
addEnvironmentVariable(scope.value.index, {
|
addEnvironmentVariable(scope.value.index, {
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportToStore([res.right])
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -166,19 +166,14 @@ const insomniaEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalEnvIndex = res.right.findIndex(
|
const globalEnvs = res.right.filter(
|
||||||
(env) => env.name === "Base Environment"
|
(env) => env.name === "Base Environment"
|
||||||
)
|
)
|
||||||
|
const otherEnvs = res.right.filter(
|
||||||
|
(env) => env.name !== "Base Environment"
|
||||||
|
)
|
||||||
|
|
||||||
const globalEnv =
|
handleImportToStore(otherEnvs, globalEnvs)
|
||||||
globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
|
|
||||||
|
|
||||||
// remove the global env from the environments array to prevent it from being imported twice
|
|
||||||
if (globalEnvIndex !== -1) {
|
|
||||||
res.right.splice(globalEnvIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImportToStore(res.right, globalEnv)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -340,14 +335,14 @@ const showImportFailedError = () => {
|
|||||||
|
|
||||||
const handleImportToStore = async (
|
const handleImportToStore = async (
|
||||||
environments: Environment[],
|
environments: Environment[],
|
||||||
globalEnv?: Environment
|
globalEnvs: NonSecretEnvironment[] = []
|
||||||
) => {
|
) => {
|
||||||
// if there's a global env, add them to the store
|
// Add global envs to the store
|
||||||
if (globalEnv) {
|
globalEnvs.forEach(({ variables }) => {
|
||||||
globalEnv.variables.forEach(({ key, value }) => {
|
variables.forEach(({ key, value, secret }) => {
|
||||||
addGlobalEnvVariable({ key, value })
|
addGlobalEnvVariable({ key, value, secret })
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
appendEnvironments(environments)
|
appendEnvironments(environments)
|
||||||
|
|||||||
@@ -210,7 +210,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||||
@@ -265,7 +268,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
|
|||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: props.modelValue.index,
|
index: props.modelValue.index,
|
||||||
name: props.modelValue.environment?.name,
|
name: props.modelValue.environment?.name,
|
||||||
|
variables: props.modelValue.environment?.variables,
|
||||||
}
|
}
|
||||||
} else if (props.modelValue?.type === "team-environment") {
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
return {
|
return {
|
||||||
type: "TEAM_ENV",
|
type: "TEAM_ENV",
|
||||||
name: props.modelValue.environment.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: props.modelValue.environment.id,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
|
variables: props.modelValue.environment.environment.variables,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: "global", name: "Global" }
|
return {
|
||||||
|
type: "global",
|
||||||
|
name: "Global",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
const environment =
|
const environment =
|
||||||
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const editGlobalEnv = () => {
|
const editGlobalEnv = () => {
|
||||||
invokeAction("modals.my.environment.edit", {
|
invokeAction("modals.global.environment.update", {})
|
||||||
envName: "Global",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editEnv = () => {
|
const editEnv = () => {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:env-vars="envVars"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsAdd
|
<EnvironmentsAdd
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="showConfirmRemoveEnvModal"
|
:show="showConfirmRemoveEnvModal"
|
||||||
:title="t('confirm.remove_team')"
|
:title="`${t('confirm.remove_environment')}`"
|
||||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||||
@resolve="removeSelectedEnvironment()"
|
@resolve="removeSelectedEnvironment()"
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
|
|||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
|
|||||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
const globalEnvironment = computed(() => ({
|
const globalEnvironment = computed(() => ({
|
||||||
|
v: 1 as const,
|
||||||
|
id: "Global",
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: globalEnv.value,
|
variables: globalEnv.value,
|
||||||
}))
|
}))
|
||||||
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
|
|||||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
const editingVariableValue = ref("")
|
const editingVariableValue = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const position = ref({ top: 0, left: 0 })
|
const position = ref({ top: 0, left: 0 })
|
||||||
|
|
||||||
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
|||||||
const editEnvironment = (environmentIndex: "Global") => {
|
const editEnvironment = (environmentIndex: "Global") => {
|
||||||
editingEnvironmentIndex.value = environmentIndex
|
editingEnvironmentIndex.value = environmentIndex
|
||||||
action.value = "edit"
|
action.value = "edit"
|
||||||
|
editingVariableName.value = ""
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
|
|||||||
|
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
editingVariableValue.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("modals.environment.new", () => {
|
defineActionHandler("modals.environment.new", () => {
|
||||||
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
|||||||
showConfirmRemoveEnvModal.value = true
|
showConfirmRemoveEnvModal.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const additionalVars = ref<Environment["variables"]>([])
|
||||||
|
|
||||||
|
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.global.environment.update",
|
||||||
({ envName, variableName }) => {
|
({ variables, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variables) {
|
||||||
envName === "Global" && editEnvironment("Global")
|
additionalVars.value = variables
|
||||||
|
}
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
editEnvironment("Global")
|
||||||
|
editingVariableName.value = "Global"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,76 +16,103 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
/>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconHelpCircle"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="clearIcon"
|
||||||
|
@click="clearContent()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import { clone } from "lodash-es"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { environmentsStore } from "~/newstore/environments"
|
import { environmentsStore } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
import { uniqueId } from "lodash-es"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
|
||||||
value: string
|
value: string
|
||||||
|
key: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +186,7 @@ const props = withDefaults(
|
|||||||
action: "edit" | "new"
|
action: "edit" | "new"
|
||||||
editingEnvironmentIndex?: number | "Global" | null
|
editingEnvironmentIndex?: number | "Global" | null
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -162,6 +194,7 @@ const props = withDefaults(
|
|||||||
action: "edit",
|
action: "edit",
|
||||||
editingEnvironmentIndex: null,
|
editingEnvironmentIndex: null,
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -172,11 +205,55 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secrets"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string>("")
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
|||||||
|
|
||||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const workingEnv = computed(() => {
|
const workingEnv = computed(() => {
|
||||||
if (props.editingEnvironmentIndex === "Global") {
|
if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
const vars =
|
||||||
|
props.editingVariableName === "Global"
|
||||||
|
? props.envVars()
|
||||||
|
: getGlobalVariables()
|
||||||
return {
|
return {
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: getGlobalVariables(),
|
variables: vars,
|
||||||
} as Environment
|
} as Environment
|
||||||
} else if (props.action === "new") {
|
} else if (props.action === "new") {
|
||||||
return {
|
return {
|
||||||
|
id: uniqueId(),
|
||||||
name: "",
|
name: "",
|
||||||
variables: props.envVars(),
|
variables: props.envVars(),
|
||||||
}
|
}
|
||||||
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
|
|||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
variables,
|
variables,
|
||||||
|
A.filter(({ secret }) => !secret),
|
||||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -239,11 +326,29 @@ watch(
|
|||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = workingEnv.value?.name ?? null
|
editingName.value = workingEnv.value?.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
|
|
||||||
|
if (props.editingEnvironmentIndex !== "Global") {
|
||||||
|
editingID.value = workingEnv.value?.id ?? uniqueId()
|
||||||
|
}
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
workingEnv.value?.variables ?? [],
|
workingEnv.value?.variables ?? [],
|
||||||
A.map((e) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
props.editingEnvironmentIndex === "Global"
|
||||||
|
? "Global"
|
||||||
|
: workingEnv.value?.id,
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -251,7 +356,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
vars.value = []
|
vars.value = vars.value.filter((e) =>
|
||||||
|
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||||
|
)
|
||||||
|
|
||||||
clearIcon.value = IconDone
|
clearIcon.value = IconDone
|
||||||
toast.success(`${t("state.cleared")}`)
|
toast.success(`${t("state.cleared")}`)
|
||||||
}
|
}
|
||||||
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEnvironment = () => {
|
const saveEnvironment = () => {
|
||||||
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterdVariables = pipe(
|
const filteredVariables = pipe(
|
||||||
vars.value,
|
vars.value,
|
||||||
A.filterMap(
|
A.filterMap(
|
||||||
flow(
|
flow(
|
||||||
@@ -286,14 +398,43 @@ const saveEnvironment = () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (editingID.value) {
|
||||||
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
editingID.value,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
} else if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const environmentUpdated: Environment = {
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: uniqueId(),
|
||||||
name: editingName.value,
|
name: editingName.value,
|
||||||
variables: filterdVariables,
|
variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
// Creating a new environment
|
// Creating a new environment
|
||||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
createEnvironment(
|
||||||
|
editingName.value,
|
||||||
|
environmentUpdated.variables,
|
||||||
|
editingID.value
|
||||||
|
)
|
||||||
setSelectedEnvironmentIndex({
|
setSelectedEnvironmentIndex({
|
||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: envList.value.length - 1,
|
index: envList.value.length - 1,
|
||||||
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -150,6 +152,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () => {
|
const exportEnvironmentAsJSON = () => {
|
||||||
const { environment, environmentIndex } = props
|
const { environment, environmentIndex } = props
|
||||||
exportAsJSON(environment, environmentIndex)
|
exportAsJSON(environment, environmentIndex)
|
||||||
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
|
|||||||
if (props.environmentIndex === null) return
|
if (props.environmentIndex === null) return
|
||||||
if (props.environmentIndex !== "Global") {
|
if (props.environmentIndex !== "Global") {
|
||||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironmentIndex = ref<number | null>(null)
|
const editingEnvironmentIndex = ref<number | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||||
action.value = "new"
|
action.value = "new"
|
||||||
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.my.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const envIndex: number = environments.value.findIndex(
|
const envIndex: number = environments.value.findIndex(
|
||||||
(environment: Environment) => {
|
(environment: Environment) => {
|
||||||
return environment.name === envName
|
return environment.name === envName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (envName !== "Global") editEnvironment(envIndex)
|
if (envName !== "Global") {
|
||||||
|
editEnvironment(envIndex)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,90 +16,112 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:class="isViewer && 'opacity-25'"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
:disabled="isViewer"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
:readonly="isViewer"
|
|
||||||
/>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
v-if="isViewer"
|
:icon="IconHelpCircle"
|
||||||
disabled
|
/>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
v-if="!isViewer"
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
<HoppButtonSecondary
|
:title="t('action.clear_all')"
|
||||||
v-else
|
:icon="clearIcon"
|
||||||
:label="`${t('add.new')}`"
|
@click="clearContent()"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-if="!isViewer"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="!isViewer"
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
:disabled="isViewer"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:readonly="isViewer && !tab.isSecret"
|
||||||
|
/>
|
||||||
|
<div v-if="!isViewer" class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isViewer" #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="`${t('action.save')}`"
|
:label="`${t('action.save')}`"
|
||||||
@@ -119,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -141,13 +163,17 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +189,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: string | undefined
|
editingTeamId: string | undefined
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
isViewer?: boolean
|
isViewer?: boolean
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -172,6 +199,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: "",
|
editingTeamId: "",
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
isViewer: false,
|
isViewer: false,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -182,11 +210,59 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secrets"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string | null>(null)
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -215,22 +291,34 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
editingName.value = props.editingEnvironment?.environment.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
editingName.value = null
|
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.envVars() ?? [],
|
props.envVars() ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.map((e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: clone(e),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
} else if (props.editingEnvironment !== null) {
|
} else if (props.editingEnvironment !== null) {
|
||||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
editingID.value = props.editingEnvironment.id
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.editingEnvironment.environment.variables ?? [],
|
props.editingEnvironment.environment.variables ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
editingID.value ?? "",
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -250,12 +338,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -278,52 +370,102 @@ const saveEnvironment = async () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: editingID.value ?? "",
|
||||||
|
name: editingName.value,
|
||||||
|
variables,
|
||||||
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_CREATE_ENVIRONMENT",
|
type: "HOPP_CREATE_ENVIRONMENT",
|
||||||
workspaceType: "team",
|
workspaceType: "team",
|
||||||
})
|
})
|
||||||
|
|
||||||
await pipe(
|
if (!props.isViewer) {
|
||||||
createTeamEnvironment(
|
await pipe(
|
||||||
JSON.stringify(filterdVariables),
|
createTeamEnvironment(
|
||||||
props.editingTeamId,
|
JSON.stringify(environmentUpdated.variables),
|
||||||
editingName.value
|
props.editingTeamId,
|
||||||
),
|
environmentUpdated.name
|
||||||
TE.match(
|
),
|
||||||
(err: GQLError<string>) => {
|
TE.match(
|
||||||
console.error(err)
|
(err: GQLError<string>) => {
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
console.error(err)
|
||||||
},
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
() => {
|
isLoading.value = false
|
||||||
hideModal()
|
},
|
||||||
toast.success(`${t("environment.created")}`)
|
(res) => {
|
||||||
}
|
const envID = res.createTeamEnvironment.id
|
||||||
)
|
if (envID) {
|
||||||
)()
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
envID,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.created")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!props.editingEnvironment) {
|
if (!props.editingEnvironment) {
|
||||||
console.error("No Environment Found")
|
console.error("No Environment Found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipe(
|
if (editingID.value) {
|
||||||
updateTeamEnvironment(
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
JSON.stringify(filterdVariables),
|
editingID.value,
|
||||||
props.editingEnvironment.id,
|
secretVariables
|
||||||
editingName.value
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
hideModal()
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)()
|
|
||||||
|
// If the user is a viewer, we don't need to update the environment in BE
|
||||||
|
// just update the secret environment in the local storage
|
||||||
|
if (props.isViewer) {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isViewer) {
|
||||||
|
await pipe(
|
||||||
|
updateTeamEnvironment(
|
||||||
|
JSON.stringify(environmentUpdated.variables),
|
||||||
|
props.editingEnvironment.id,
|
||||||
|
environmentUpdated.name
|
||||||
|
),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
isLoading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
v-if="!isViewer"
|
|
||||||
ref="options"
|
ref="options"
|
||||||
interactive
|
interactive
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -57,6 +56,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="duplicate"
|
ref="duplicate"
|
||||||
:icon="IconCopy"
|
:icon="IconCopy"
|
||||||
:label="`${t('action.duplicate')}`"
|
:label="`${t('action.duplicate')}`"
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="exportAsJsonEl"
|
ref="exportAsJsonEl"
|
||||||
:icon="IconEdit"
|
:icon="IconEdit"
|
||||||
:label="`${t('export.as_json')}`"
|
:label="`${t('export.as_json')}`"
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
:icon="IconTrash2"
|
:icon="IconTrash2"
|
||||||
:label="`${t('action.delete')}`"
|
:label="`${t('action.delete')}`"
|
||||||
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -137,6 +141,8 @@ const emit = defineEmits<{
|
|||||||
(e: "edit-environment"): void
|
(e: "edit-environment"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () =>
|
const exportEnvironmentAsJSON = () =>
|
||||||
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
toast.success(`${t("team_environment.deleted")}`)
|
toast.success(`${t("team_environment.deleted")}`)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
:editing-environment="editingEnvironment"
|
:editing-environment="editingEnvironment"
|
||||||
:editing-team-id="team?.id"
|
:editing-team-id="team?.id"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
:is-viewer="team?.myRole === 'VIEWER'"
|
:is-viewer="team?.myRole === 'VIEWER'"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||||
|
|
||||||
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironment.value = null
|
editingEnvironment.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.team.environment.edit",
|
"modals.team.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const teamEnvToEdit = props.teamEnvironments.find(
|
const teamEnvToEdit = props.teamEnvironments.find(
|
||||||
(environment) => environment.environment.name === envName
|
(environment) => environment.environment.name === envName
|
||||||
)
|
)
|
||||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
if (teamEnvToEdit) {
|
||||||
|
editEnvironment(teamEnvToEdit)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,17 +31,6 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -54,6 +43,17 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -284,7 +284,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "none",
|
authType: "inherit",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlHeaders')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -315,6 +315,8 @@ import { commonHeaders } from "~/helpers/headers"
|
|||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { HoppGQLHeader } from "~/helpers/graphql"
|
import { HoppGQLHeader } from "~/helpers/graphql"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
@@ -338,7 +340,7 @@ const request = useVModel(props, "modelValue", emit)
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlHeaders")
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkHeaders = ref("")
|
const bulkHeaders = ref("")
|
||||||
|
|
||||||
@@ -353,7 +355,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -61,9 +61,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlQuery')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -112,6 +112,8 @@ import {
|
|||||||
socketDisconnect,
|
socketDisconnect,
|
||||||
subscriptionState,
|
subscriptionState,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const queryEditor = ref<any | null>(null)
|
const queryEditor = ref<any | null>(null)
|
||||||
@@ -137,7 +139,7 @@ const prettifyQueryIcon = refAutoReset<
|
|||||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||||
>(IconWand, 1000)
|
>(IconWand, 1000)
|
||||||
|
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlQuery")
|
||||||
|
|
||||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||||
|
|
||||||
@@ -184,7 +186,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "graphql",
|
mode: "graphql",
|
||||||
placeholder: `${t("request.query")}`,
|
placeholder: `${t("request.query")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: createGQLQueryLinter(schema),
|
linter: createGQLQueryLinter(schema),
|
||||||
completer: queryCompleter(schema),
|
completer: queryCompleter(schema),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-1 flex-col">
|
<div class="h-full">
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-model="selectedOptionTab"
|
v-model="selectedOptionTab"
|
||||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||||
|
|||||||
@@ -16,9 +16,11 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="
|
||||||
|
toggleNestedSetting('WRAP_LINES', 'graphqlResponseBody')
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -70,7 +72,9 @@
|
|||||||
</tippy>
|
</tippy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
<div class="h-full">
|
||||||
|
<div ref="schemaEditor"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="response[0].error.component"
|
:is="response[0].error.component"
|
||||||
@@ -99,6 +103,8 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||||
import {
|
import {
|
||||||
useCopyInterface,
|
useCopyInterface,
|
||||||
@@ -133,8 +139,8 @@ const responseString = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlResponseBody")
|
||||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
schemaEditor,
|
schemaEditor,
|
||||||
@@ -143,7 +149,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/ld+json",
|
mode: "application/ld+json",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -127,9 +127,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlSchema')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -145,11 +145,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="schemaString" class="h-full relative w-full">
|
||||||
v-if="schemaString"
|
<div ref="schemaEditor" class="absolute inset-0"></div>
|
||||||
ref="schemaEditor"
|
</div>
|
||||||
class="flex flex-1 flex-col"
|
|
||||||
></div>
|
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
@@ -202,6 +200,8 @@ import {
|
|||||||
subscriptionFields,
|
subscriptionFields,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||||
@@ -349,7 +349,7 @@ const handleJumpToType = async (type: GraphQLType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlSchema")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
schemaEditor,
|
schemaEditor,
|
||||||
@@ -358,7 +358,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "graphql",
|
mode: "graphql",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlVariables')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -67,7 +67,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
<div class="h-full relative">
|
||||||
|
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -93,6 +95,8 @@ import {
|
|||||||
socketDisconnect,
|
socketDisconnect,
|
||||||
subscriptionState,
|
subscriptionState,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -114,7 +118,7 @@ const variableString = useVModel(props, "modelValue", emit)
|
|||||||
|
|
||||||
const variableEditor = ref<any | null>(null)
|
const variableEditor = ref<any | null>(null)
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")
|
||||||
|
|
||||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
IconCopy,
|
IconCopy,
|
||||||
@@ -131,7 +135,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/ld+json",
|
mode: "application/ld+json",
|
||||||
placeholder: `${t("request.variables")}`,
|
placeholder: `${t("request.variables")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: computed(() =>
|
linter: computed(() =>
|
||||||
variableString.value.length > 0 ? jsonLinter : null
|
variableString.value.length > 0 ? jsonLinter : null
|
||||||
|
|||||||
@@ -331,7 +331,8 @@ const deleteHistory = (entry: HistoryEntry) => {
|
|||||||
const addToCollection = (entry: HistoryEntry) => {
|
const addToCollection = (entry: HistoryEntry) => {
|
||||||
if (props.page === "rest") {
|
if (props.page === "rest") {
|
||||||
invokeAction("request.save-as", {
|
invokeAction("request.save-as", {
|
||||||
request: entry.request,
|
requestType: "rest",
|
||||||
|
request: entry.request as HoppRESTRequest,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,6 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -54,6 +43,17 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="auth.authType === 'basic'">
|
<div v-if="auth.authType === 'basic'">
|
||||||
<HttpAuthorizationBasic v-model="auth" />
|
<HttpAuthorizationBasic v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'inherit'" class="p-4">
|
<div v-if="auth.authType === 'inherit'" class="p-4">
|
||||||
<span v-if="inheritedProperties?.auth">
|
<span v-if="inheritedProperties?.auth">
|
||||||
@@ -169,17 +169,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'bearer'">
|
<div v-if="auth.authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.token"
|
||||||
|
placeholder="Token"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'oauth-2'">
|
<div v-if="auth.authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.token"
|
||||||
|
placeholder="Token"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HttpOAuth2Authorization v-model="auth" />
|
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'api-key'">
|
<div v-if="auth.authType === 'api-key'">
|
||||||
<HttpAuthorizationApiKey v-model="auth" />
|
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -215,6 +224,7 @@ import { useColorMode } from "@composables/theming"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { onMounted } from "vue"
|
import { onMounted } from "vue"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -225,6 +235,7 @@ const props = defineProps<{
|
|||||||
isCollectionProperty?: boolean
|
isCollectionProperty?: boolean
|
||||||
isRootCollection?: boolean
|
isRootCollection?: boolean
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -265,7 +276,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "none",
|
authType: "inherit",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,10 +100,12 @@
|
|||||||
<HttpBodyParameters
|
<HttpBodyParameters
|
||||||
v-if="body.contentType === 'multipart/form-data'"
|
v-if="body.contentType === 'multipart/form-data'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
<HttpURLEncodedParams
|
<HttpURLEncodedParams
|
||||||
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -141,6 +143,7 @@ import IconExternalLink from "~icons/lucide/external-link"
|
|||||||
import IconInfo from "~icons/lucide/info"
|
import IconInfo from "~icons/lucide/info"
|
||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
import { RESTOptionTabs } from "./RequestOptions.vue"
|
import { RESTOptionTabs } from "./RequestOptions.vue"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -148,6 +151,7 @@ const t = useI18n()
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
body: HoppRESTReqBody
|
body: HoppRESTReqBody
|
||||||
headers: HoppRESTHeader[]
|
headers: HoppRESTHeader[]
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user