Compare commits

..

5 Commits

136 changed files with 3125 additions and 7656 deletions

View File

@@ -1,48 +0,0 @@
# THIS IS NOT TO BE USED FOR PERSONAL DEPLOYMENTS!
# Internal Docker Compose Image used for internal testing deployments
version: "3.7"
services:
hoppscotch-db:
image: postgres:15
user: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10
hoppscotch-aio:
container_name: hoppscotch-aio
build:
dockerfile: prod.Dockerfile
context: .
target: aio
environment:
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
- ENABLE_SUBPATH_BASED_ACCESS=true
env_file:
- ./.env
depends_on:
hoppscotch-db:
condition: service_healthy
command: ["sh", "-c", "pnpm exec prisma migrate deploy && node /usr/src/app/aio_run.mjs"]
healthcheck:
test:
- CMD
- curl
- '-f'
- 'http://localhost:80'
interval: 2s
timeout: 10s
retries: 30

View File

@@ -9,10 +9,6 @@ curlCheck() {
fi fi
} }
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then curlCheck "http://localhost:3000"
curlCheck "http://localhost:80/backend/ping" curlCheck "http://localhost:3100"
else curlCheck "http://localhost:3170/ping"
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"
fi

View File

@@ -1,22 +1,17 @@
-- This is a custom migration file which is not generated by Prisma. -- AlterTable
-- The aim of this migration is to add text search indices to the TeamCollection and TeamRequest tables. ALTER TABLE
-- Create Extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Create GIN Trigram Index for Team Collection title
CREATE INDEX
"TeamCollection_title_trgm_idx"
ON
"TeamCollection" "TeamCollection"
USING ADD
GIN (title gin_trgm_ops); titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
-- Create GIN Trigram Index for Team Collection title -- AlterTable
CREATE INDEX ALTER TABLE
"TeamRequest_title_trgm_idx"
ON
"TeamRequest" "TeamRequest"
USING ADD
GIN (title gin_trgm_ops); 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);

View File

@@ -84,12 +84,6 @@ export const USER_ALREADY_INVITED = 'admin/user_already_invited' as const;
*/ */
export const USER_UPDATE_FAILED = 'user/update_failed' as const; export const USER_UPDATE_FAILED = 'user/update_failed' as const;
/**
* User display name validation failure
* (UserService)
*/
export const USER_SHORT_DISPLAY_NAME = 'user/short_display_name' as const;
/** /**
* User deletion failure * User deletion failure
* (UserService) * (UserService)
@@ -756,8 +750,3 @@ export const DATABASE_TABLE_NOT_EXIST =
* (InfraConfigService) * (InfraConfigService)
*/ */
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized'; export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
/**
* Inputs supplied are invalid
*/
export const INVALID_PARAMS = 'invalid_parameters' as const;

View File

@@ -321,28 +321,25 @@ export class InfraConfigService implements OnModuleInit {
* Reset all the InfraConfigs to their default values (from .env) * Reset all the InfraConfigs to their default values (from .env)
*/ */
async reset() { async reset() {
// These are all the infra-configs that should not be reset
const RESET_EXCLUSION_LIST = [
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
];
try { try {
const infraConfigDefaultObjs = await getDefaultInfraConfigs(); const infraConfigDefaultObjs = await getDefaultInfraConfigs();
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(p) => RESET_EXCLUSION_LIST.includes(p.name) === false,
);
await this.prisma.infraConfig.deleteMany({ await this.prisma.infraConfig.deleteMany({
where: { where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
name: {
in: updatedInfraConfigDefaultObjs.map((p) => p.name),
},
},
}); });
// Hardcode t
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
);
await this.prisma.infraConfig.createMany({ await this.prisma.infraConfig.createMany({
data: updatedInfraConfigDefaultObjs, data: [
...updatedInfraConfigDefaultObjs,
{
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
value: 'true',
},
],
}); });
stopApp(); stopApp();

View File

@@ -1,11 +1,4 @@
import { import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
Controller,
Get,
HttpStatus,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { TeamCollectionService } from './team-collection.service'; import { TeamCollectionService } from './team-collection.service';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard'; import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
@@ -14,15 +7,13 @@ import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorat
import { TeamMemberRole } from '@prisma/client'; import { TeamMemberRole } from '@prisma/client';
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard'; import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
import { throwHTTPErr } from 'src/utils'; import { throwHTTPErr } from 'src/utils';
import { RESTError } from 'src/types/RESTError';
import { INVALID_PARAMS } from 'src/errors';
@UseGuards(ThrottlerBehindProxyGuard) @UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'team-collection', version: '1' }) @Controller({ path: 'team-collection', version: '1' })
export class TeamCollectionController { export class TeamCollectionController {
constructor(private readonly teamCollectionService: TeamCollectionService) {} constructor(private readonly teamCollectionService: TeamCollectionService) {}
@Get('search/:teamID') @Get('search/:teamID/:searchQuery')
@RequiresTeamRole( @RequiresTeamRole(
TeamMemberRole.VIEWER, TeamMemberRole.VIEWER,
TeamMemberRole.EDITOR, TeamMemberRole.EDITOR,
@@ -30,20 +21,13 @@ export class TeamCollectionController {
) )
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard) @UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
async searchByTitle( async searchByTitle(
@Query('searchQuery') searchQuery: string, @Param('searchQuery') searchQuery: string,
@Param('teamID') teamID: string, @Param('teamID') teamID: string,
@Query('take') take: string, @Query('take') take: string,
@Query('skip') skip: string, @Query('skip') skip: string,
) { ) {
if (!teamID || !searchQuery) {
return <RESTError>{
message: INVALID_PARAMS,
statusCode: HttpStatus.BAD_REQUEST,
};
}
const res = await this.teamCollectionService.searchByTitle( const res = await this.teamCollectionService.searchByTitle(
searchQuery.trim(), searchQuery,
teamID, teamID,
parseInt(take), parseInt(take),
parseInt(skip), parseInt(skip),

View File

@@ -20,7 +20,7 @@ import {
TEAM_COLL_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 { escapeSqlLikeString, isValidLength } from 'src/utils'; import { isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client'; import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
@@ -1148,20 +1148,14 @@ export class TeamCollectionService {
skip: number, skip: number,
) { ) {
const query = Prisma.sql` const query = Prisma.sql`
SELECT select id,title,'collection' AS type
id,title,'collection' AS type from "TeamCollection"
FROM where "TeamCollection"."teamID"=${teamID}
"TeamCollection" and titlesearch @@ plainto_tsquery(${searchQuery})
WHERE order by ts_rank(titlesearch,plainto_tsquery(${searchQuery}))
"TeamCollection"."teamID"=${teamID} limit ${take}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take}; OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`; `;
try { try {
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query); const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
return E.right(res); return E.right(res);
@@ -1186,17 +1180,12 @@ export class TeamCollectionService {
skip: number, skip: number,
) { ) {
const query = Prisma.sql` const query = Prisma.sql`
SELECT select id,title,request->>'method' as method,'request' AS type
id,title,request->>'method' as method,'request' AS type from "TeamRequest"
FROM where "TeamRequest"."teamID"=${teamID}
"TeamRequest" and titlesearch @@ plainto_tsquery(${searchQuery})
WHERE order by ts_rank(titlesearch,plainto_tsquery(${searchQuery}))
"TeamRequest"."teamID"=${teamID} limit ${take}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take}; OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`; `;

View File

@@ -58,29 +58,6 @@ export class UserResolver {
if (E.isLeft(updatedUser)) throwErr(updatedUser.left); if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
return updatedUser.right; return updatedUser.right;
} }
@Mutation(() => User, {
description: 'Update a users display name',
})
@UseGuards(GqlAuthGuard)
async updateDisplayName(
@GqlUser() user: AuthUser,
@Args({
name: 'updatedDisplayName',
description: 'New name of user',
type: () => String,
})
updatedDisplayName: string,
) {
const updatedUser = await this.userService.updateUserDisplayName(
user.uid,
updatedDisplayName,
);
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
return updatedUser.right;
}
@Mutation(() => Boolean, { @Mutation(() => Boolean, {
description: 'Delete an user account', description: 'Delete an user account',
}) })

View File

@@ -1,9 +1,4 @@
import { import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
JSON_INVALID,
USERS_NOT_FOUND,
USER_NOT_FOUND,
USER_SHORT_DISPLAY_NAME,
} from 'src/errors';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
@@ -485,14 +480,6 @@ describe('UserService', () => {
); );
expect(result).toEqualLeft(USER_NOT_FOUND); expect(result).toEqualLeft(USER_NOT_FOUND);
}); });
test('should resolve left and error when short display name is passed', async () => {
const newDisplayName = '';
const result = await userService.updateUserDisplayName(
user.uid,
newDisplayName,
);
expect(result).toEqualLeft(USER_SHORT_DISPLAY_NAME);
});
}); });
describe('fetchAllUsers', () => { describe('fetchAllUsers', () => {

View File

@@ -8,11 +8,7 @@ import * as T from 'fp-ts/Task';
import * as A from 'fp-ts/Array'; import * as A from 'fp-ts/Array';
import { pipe, constVoid } from 'fp-ts/function'; import { pipe, constVoid } from 'fp-ts/function';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
USERS_NOT_FOUND,
USER_NOT_FOUND,
USER_SHORT_DISPLAY_NAME,
} from 'src/errors';
import { SessionType, User } from './user.model'; import { SessionType, User } from './user.model';
import { USER_UPDATE_FAILED } from 'src/errors'; import { USER_UPDATE_FAILED } from 'src/errors';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
@@ -295,10 +291,6 @@ export class UserService {
* @returns a Either of User or error * @returns a Either of User or error
*/ */
async updateUserDisplayName(userUID: string, displayName: string) { async updateUserDisplayName(userUID: string, displayName: string) {
if (!displayName || displayName.length === 0) {
return E.left(USER_SHORT_DISPLAY_NAME);
}
try { try {
const dbUpdatedUser = await this.prisma.user.update({ const dbUpdatedUser = await this.prisma.user.update({
where: { uid: userUID }, where: { uid: userUID },

View File

@@ -250,39 +250,3 @@ export function checkEnvironmentAuthProvider(
} }
} }
} }
/**
* Adds escape backslashes to the input so that it can be used inside
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
* function.
*
* Eg. "100%" -> "100\\%"
*
* Source: https://stackoverflow.com/a/32648526
*/
export function escapeSqlLikeString(str: string) {
if (typeof str != 'string') return str;
return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
switch (char) {
case '\0':
return '\\0';
case '\x08':
return '\\b';
case '\x09':
return '\\t';
case '\x1a':
return '\\z';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '"':
case "'":
case '\\':
case '%':
return '\\' + char; // prepends a backslash to backslash, percent,
// and double/single quotes
}
});
}

View File

@@ -20,7 +20,7 @@ describe("Test `hopp test <file>` command:", () => {
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });
}); })
describe("Supplied collection export file validations", () => { describe("Supplied collection export file validations", () => {
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => { test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
@@ -66,43 +66,6 @@ describe("Test `hopp test <file>` command:", () => {
}); });
}); });
describe("Versioned entities", () => {
describe("Collections & Requests", () => {
const testFixtures = [
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
];
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Environments", () => {
const testFixtures = [
{ fileName: "env-v0.json", version: 0 },
{ fileName: "env-v1.json", version: 1 },
];
testFixtures.forEach(({ fileName, version }) => {
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
});
test("Successfully processes a supplied collection export file of the expected format", async () => { test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`; const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
@@ -112,8 +75,7 @@ describe("Test `hopp test <file>` command:", () => {
test("Successfully inherits headers and authorization set at the root collection", async () => { test("Successfully inherits headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath( const args = `test ${getTestJsonFilePath(
"collection-level-headers-auth-coll.json", "collection-level-headers-auth-coll.json", "collection"
"collection"
)}`; )}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
@@ -122,8 +84,7 @@ describe("Test `hopp test <file>` command:", () => {
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => { test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath( const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json", "pre-req-script-env-var-persistence-coll.json", "collection"
"collection"
)}`; )}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
@@ -145,8 +106,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => { test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath( const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt", "notjson-coll.txt", "collection"
"collection"
)}`; )}`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
@@ -163,10 +123,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
}); });
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => { test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
"malformed-envs.json",
"environment"
);
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`; const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
@@ -185,10 +142,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
}); });
test("Successfully resolves values from the supplied environment export file", async () => { test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath( const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`; const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
@@ -197,14 +151,8 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
}); });
test("Successfully resolves environment variables referenced in the request body", async () => { test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
"req-body-env-vars-coll.json", const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
@@ -212,10 +160,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
}); });
test("Works with shorth `-e` flag", async () => { test("Works with shorth `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath( const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`; const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
@@ -238,10 +183,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
secretHeaderValue: "secret-header-value", secretHeaderValue: "secret-header-value",
}; };
const COLL_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment"); const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
@@ -255,14 +197,8 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
// Prefers values specified in the environment export file over values set in the system environment // Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => { test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
"secret-envs-coll.json", const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args); const { error, stdout } = await runCLI(args);
@@ -276,13 +212,9 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
// Values set from the scripting context takes the highest precedence // Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => { test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json", "secret-envs-persistence-coll.json", "collection"
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
); );
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args); const { error, stdout } = await runCLI(args);
@@ -295,12 +227,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => { test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json", "secret-envs-persistence-scripting-coll.json", "collection"
"collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json", "secret-envs-persistence-scripting-envs.json", "environment"
"environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;

View File

@@ -0,0 +1,84 @@
import { isRESTCollection } from "../../../utils/checks";
describe("isRESTCollection", () => {
test("Undefined collection value.", () => {
expect(isRESTCollection(undefined)).toBeFalsy();
});
test("Invalid id value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: 1,
})
).toBeFalsy();
});
test("Invalid requests value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: null,
})
).toBeFalsy();
});
test("Invalid folders value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: undefined,
})
).toBeFalsy();
});
test("Invalid RESTCollection(s) in folders.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: [
{
v: 1,
name: "test1",
id: "2",
requests: undefined,
folders: [],
},
],
})
).toBeFalsy();
});
test("Invalid HoppRESTRequest(s) in requests.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [{}],
folders: [],
})
).toBeFalsy();
});
test("Valid RESTCollection.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: [],
})
).toBeTruthy();
});
});

View File

@@ -1,27 +0,0 @@
{
"v": 1,
"name": "coll-v1",
"folders": [],
"requests": [
{
"url": "https://httpbin.org",
"path": "/get",
"headers": [
{ "key": "Inactive-Header", "value": "Inactive Header", "active": false },
{ "key": "Authorization", "value": "Bearer token123", "active": true }
],
"params": [
{ "key": "key", "value": "value", "active": true },
{ "key": "inactive-key", "value": "inactive-param", "active": false }
],
"name": "req-v0",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"contentType": "application/json",
"body": "",
"auth": "Bearer Token",
"bearerToken": "token123"
}
]
}

View File

@@ -1,48 +0,0 @@
{
"v": 1,
"name": "coll-v1",
"folders": [],
"requests": [
{
"v": "1",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v1",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
}
}
]
}

View File

@@ -1,54 +0,0 @@
{
"v": 2,
"name": "coll-v2",
"folders": [],
"requests": [
{
"v": "2",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v2",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

View File

@@ -1,54 +0,0 @@
{
"v": 2,
"name": "coll-v2",
"folders": [],
"requests": [
{
"v": "3",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v3",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

View File

@@ -1,23 +1,23 @@
[ [
{ {
"v": 2, "v": 1,
"name": "CollectionA", "name": "CollectionA",
"folders": [ "folders": [
{ {
"v": 2, "v": 1,
"name": "FolderA", "name": "FolderA",
"folders": [ "folders": [
{ {
"v": 2, "v": 1,
"name": "FolderB", "name": "FolderB",
"folders": [ "folders": [
{ {
"v": 2, "v": 1,
"name": "FolderC", "name": "FolderC",
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestD", "name": "RequestD",
"params": [], "params": [],
@@ -53,7 +53,7 @@
], ],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestC", "name": "RequestC",
"params": [], "params": [],
@@ -90,7 +90,7 @@
], ],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestB", "name": "RequestB",
"params": [], "params": [],
@@ -119,7 +119,7 @@
], ],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestA", "name": "RequestA",
"params": [], "params": [],
@@ -153,16 +153,16 @@
} }
}, },
{ {
"v": 2, "v": 1,
"name": "CollectionB", "name": "CollectionB",
"folders": [ "folders": [
{ {
"v": 2, "v": 1,
"name": "FolderA", "name": "FolderA",
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestB", "name": "RequestB",
"params": [], "params": [],
@@ -191,7 +191,7 @@
], ],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"name": "RequestA", "name": "RequestA",
"params": [], "params": [],

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "<<URL>>", "endpoint": "<<URL>>",
"name": "test1", "name": "test1",
"params": [], "params": [],

View File

@@ -5,7 +5,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>", "endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "", "name": "",
"params": [], "params": [],
@@ -13,7 +13,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");", "preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});", "testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
@@ -21,10 +24,10 @@
"contentType": "application/json", "contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}" "body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}, },
"requestVariables": [] "requestVariables": [],
}, },
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>", "endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
"name": "success", "name": "success",
"params": [], "params": [],
@@ -32,7 +35,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");", "preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});", "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": "requests":
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>", "endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail", "name": "fail",
"params": [], "params": [],
@@ -12,7 +12,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");", "preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});", "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
@@ -23,7 +26,7 @@
"requestVariables": [], "requestVariables": [],
}, },
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>", "endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success", "name": "success",
"params": [], "params": [],
@@ -31,7 +34,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");", "preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});", "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",

View File

@@ -5,7 +5,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>", "endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "", "name": "",
"params": [], "params": [],
@@ -13,7 +13,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");", "preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});", "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
@@ -24,7 +27,7 @@
"requestVariables": [] "requestVariables": []
}, },
{ {
"v": "3", "v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>", "endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success", "name": "success",
"params": [], "params": [],
@@ -32,7 +35,10 @@
"method": "GET", "method": "GET",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
}, },
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");", "preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});", "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null }, "body": { "body": null, "contentType": null },
"name": "sample-req", "name": "sample-req",

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"name": "test-request", "name": "test-request",
"endpoint": "https://echo.hoppscotch.io", "endpoint": "https://echo.hoppscotch.io",
"method": "POST", "method": "POST",

View File

@@ -1,26 +0,0 @@
{
"v": 1,
"name": "tests",
"folders": [],
"requests": [
{
"v": "2",
"endpoint": "<<baseURL>>",
"name": "",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "none",
"authActive": true
},
"preRequestScript": "",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": null,
"body": null
},
"requestVariables": []
}
]
}

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null }, "body": { "body": null, "contentType": null },
"name": "test-secret-headers", "name": "test-secret-headers",
@@ -23,7 +23,7 @@
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" "preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"body": { "body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}", "body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
@@ -39,7 +39,7 @@
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" "preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null }, "body": { "body": null, "contentType": null },
"name": "test-secret-query-params", "name": "test-secret-query-params",
@@ -58,7 +58,7 @@
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" "preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "basic", "authType": "basic",
"password": "<<secretBasicAuthPassword>>", "password": "<<secretBasicAuthPassword>>",
@@ -76,7 +76,7 @@
"preRequestScript": "" "preRequestScript": ""
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"token": "<<secretBearerToken>>", "token": "<<secretBearerToken>>",
"authType": "bearer", "authType": "bearer",
@@ -95,7 +95,7 @@
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)" "preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null }, "body": { "body": null, "contentType": null },
"name": "test-secret-fallback", "name": "test-secret-fallback",

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true
@@ -29,7 +29,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true
@@ -54,7 +54,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true
@@ -73,7 +73,7 @@
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" "preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "none", "authType": "none",
"authActive": true "authActive": true
@@ -98,7 +98,7 @@
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" "preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"authType": "basic", "authType": "basic",
"password": "<<secretBasicAuthPassword>>", "password": "<<secretBasicAuthPassword>>",
@@ -119,7 +119,7 @@
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}" "preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
}, },
{ {
"v": "3", "v": "2",
"auth": { "auth": {
"token": "<<secretBearerToken>>", "token": "<<secretBearerToken>>",
"authType": "bearer", "authType": "bearer",

View File

@@ -4,7 +4,7 @@
"folders": [], "folders": [],
"requests": [ "requests": [
{ {
"v": "3", "v": "2",
"endpoint": "https://httpbin.org/post", "endpoint": "https://httpbin.org/post",
"name": "req", "name": "req",
"params": [], "params": [],

View File

@@ -1,9 +0,0 @@
{
"name": "env-v0",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io"
}
]
}

View File

@@ -1,10 +0,0 @@
{
"name": "env-v0",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io",
"secret": false
}
]
}

View File

@@ -6,7 +6,7 @@ import { error } from "../../types/errors";
import { import {
HoppEnvKeyPairObject, HoppEnvKeyPairObject,
HoppEnvPair, HoppEnvPair,
HoppEnvs, HoppEnvs
} from "../../types/request"; } from "../../types/request";
import { readJsonFile } from "../../utils/mutators"; import { readJsonFile } from "../../utils/mutators";
@@ -17,7 +17,7 @@ import { readJsonFile } from "../../utils/mutators";
*/ */
export async function parseEnvsData(path: string) { export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path); const contents = await readJsonFile(path);
const envPairs: Array<HoppEnvPair | Record<string, string>> = []; const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
// The legacy key-value pair format that is still supported // The legacy key-value pair format that is still supported
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents); const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
@@ -26,9 +26,7 @@ export async function parseEnvsData(path: string) {
const HoppEnvExportObjectResult = Environment.safeParse(contents); const HoppEnvExportObjectResult = Environment.safeParse(contents);
// Shape of the bulk environment export object that is exported from the app // Shape of the bulk environment export object that is exported from the app
const HoppBulkEnvExportObjectResult = z const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
.array(entityReference(Environment))
.safeParse(contents);
// CLI doesnt support bulk environments export // CLI doesnt support bulk environments export
// Hence we check for this case and throw an error if it matches the format // Hence we check for this case and throw an error if it matches the format
@@ -38,16 +36,13 @@ export async function parseEnvsData(path: string) {
// Checks if the environment file is of the correct format // Checks if the environment file is of the correct format
// If it doesnt match either of them, we throw an error // If it doesnt match either of them, we throw an error
if ( if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
!HoppEnvKeyPairResult.success &&
HoppEnvExportObjectResult.type === "err"
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: error }); throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
} }
if (HoppEnvKeyPairResult.success) { if (HoppEnvKeyPairResult.success) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) { for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value, secret: false }); envPairs.push({ key, value });
} }
} else if (HoppEnvExportObjectResult.type === "ok") { } else if (HoppEnvExportObjectResult.type === "ok") {
envPairs.push(...HoppEnvExportObjectResult.value.variables); envPairs.push(...HoppEnvExportObjectResult.value.variables);

View File

@@ -1,3 +1,5 @@
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
import * as A from "fp-ts/Array";
import { CommanderError } from "commander"; import { CommanderError } from "commander";
import { HoppCLIError, HoppErrnoException } from "../types/errors"; import { HoppCLIError, HoppErrnoException } from "../types/errors";
@@ -12,6 +14,48 @@ export const hasProperty = <P extends PropertyKey>(
prop: P prop: P
): target is Record<P, unknown> => prop in target; ): target is Record<P, unknown> => prop in target;
/**
* Typeguard to check valid Hoppscotch REST Collection.
* @param param The object to be checked.
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
* False, otherwise.
*/
export const isRESTCollection = (param: unknown): param is HoppCollection => {
if (!!param && typeof param === "object") {
if (!hasProperty(param, "v") || typeof param.v !== "number") {
return false;
}
if (!hasProperty(param, "name") || typeof param.name !== "string") {
return false;
}
if (hasProperty(param, "id") && typeof param.id !== "string") {
return false;
}
if (!hasProperty(param, "requests") || !Array.isArray(param.requests)) {
return false;
} else {
// Checks each requests array to be valid HoppRESTRequest.
const checkRequests = A.every(isHoppRESTRequest)(param.requests);
if (!checkRequests) {
return false;
}
}
if (!hasProperty(param, "folders") || !Array.isArray(param.folders)) {
return false;
} else {
// Checks each folder to be valid REST collection.
const checkFolders = A.every(isRESTCollection)(param.folders);
if (!checkFolders) {
return false;
}
}
return true;
}
return false;
};
/** /**
* Checks if given error data is of type HoppCLIError, based on existence * Checks if given error data is of type HoppCLIError, based on existence
* of code property. * of code property.

View File

@@ -131,7 +131,7 @@ const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
* path of each request within collection-json file, failed-tests-report, errors, * path of each request within collection-json file, failed-tests-report, errors,
* total execution duration for requests, pre-request-scripts, test-scripts. * total execution duration for requests, pre-request-scripts, test-scripts.
* @returns True, if collection runner executed without any errors or failed test-cases. * @returns True, if collection runner executed without any errors or failed test-cases.
* False, if errors occurred or test-cases failed. * False, if errors occured or test-cases failed.
*/ */
export const collectionsRunnerResult = ( export const collectionsRunnerResult = (
requestsReport: RequestReport[] requestsReport: RequestReport[]

View File

@@ -112,7 +112,7 @@ export const printTestsMetrics = (testsMetrics: TestMetrics) => {
/** /**
* Prints details of each reported error for a request with error code. * Prints details of each reported error for a request with error code.
* @param path Request's path in collection for which errors occurred. * @param path Request's path in collection for which errors occured.
* @param errorsReport List of errors reported. * @param errorsReport List of errors reported.
*/ */
export const printErrorsReport = ( export const printErrorsReport = (

View File

@@ -1,11 +1,8 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import fs from "fs/promises"; import fs from "fs/promises";
import { entityReference } from "verzod";
import { z } from "zod";
import { error } from "../types/errors";
import { FormDataEntry } from "../types/request"; import { FormDataEntry } from "../types/request";
import { isHoppErrnoException } from "./checks"; import { error } from "../types/errors";
import { isRESTCollection, isHoppErrnoException } from "./checks";
import { HoppCollection } from "@hoppscotch/data";
/** /**
* Parses array of FormDataEntry to FormData. * Parses array of FormDataEntry to FormData.
@@ -70,11 +67,7 @@ export async function parseCollectionData(
? contents ? contents
: [contents]; : [contents];
const collectionSchemaParsedResult = z if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
.array(entityReference(HoppCollection))
.safeParse(maybeArrayOfCollections);
if (!collectionSchemaParsedResult.success) {
throw error({ throw error({
code: "MALFORMED_COLLECTION", code: "MALFORMED_COLLECTION",
path, path,
@@ -82,22 +75,5 @@ export async function parseCollectionData(
}); });
} }
return collectionSchemaParsedResult.data.map((collection) => { return maybeArrayOfCollections as HoppCollection[];
const requestSchemaParsedResult = z
.array(entityReference(HoppRESTRequest))
.safeParse(collection.requests);
if (!requestSchemaParsedResult.success) {
throw error({
code: "MALFORMED_COLLECTION",
path,
data: "Please check the collection data.",
});
}
return {
...collection,
requests: requestSchemaParsedResult.data,
};
});
} }

View File

@@ -109,31 +109,18 @@ export function getEffectiveRESTRequest(
key: "Authorization", key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`, value: `Basic ${btoa(`${username}:${password}`)}`,
}); });
} else if (request.auth.authType === "bearer") { } else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
effectiveFinalHeaders.push({ effectiveFinalHeaders.push({
active: true, active: true,
key: "Authorization", key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.token, envVariables)}`, value: `Bearer ${parseTemplateString(
request.auth.token,
envVariables
)}`,
}); });
} else if (request.auth.authType === "oauth-2") {
const { addTo } = request.auth;
if (addTo === "HEADERS") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, envVariables)}`,
});
} else if (addTo === "QUERY_PARAMS") {
effectiveFinalParams.push({
active: true,
key: "access_token",
value: parseTemplateString(
request.auth.grantTypeInfo.token,
envVariables
),
});
}
} else if (request.auth.authType === "api-key") { } else if (request.auth.authType === "api-key") {
const { key, value, addTo } = request.auth; const { key, value, addTo } = request.auth;
if (addTo === "Headers") { if (addTo === "Headers") {

View File

@@ -41,10 +41,10 @@ const processVariables = (variable: Environment["variables"][number]) => {
...variable, ...variable,
value: value:
"value" in variable ? variable.value : process.env[variable.key] || "", "value" in variable ? variable.value : process.env[variable.key] || "",
}; }
} }
return variable; return variable
}; }
/** /**
* Processes given envs, which includes processing each variable in global * Processes given envs, which includes processing each variable in global
@@ -56,10 +56,10 @@ const processEnvs = (envs: HoppEnvs) => {
const processedEnvs = { const processedEnvs = {
global: envs.global.map(processVariables), global: envs.global.map(processVariables),
selected: envs.selected.map(processVariables), selected: envs.selected.map(processVariables),
}; }
return processedEnvs; 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
@@ -70,7 +70,7 @@ const processEnvs = (envs: HoppEnvs) => {
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, displayUrl: req.effectiveFinalDisplayURL
}; };
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest; const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
const reqParams = finalParams(req); const reqParams = finalParams(req);
@@ -131,7 +131,6 @@ export const requestRunner =
let status: number; let status: number;
const baseResponse = await axios(requestConfig); const baseResponse = await axios(requestConfig);
const { config } = baseResponse; const { config } = baseResponse;
// PR-COMMENT: type error
const runnerResponse: RequestRunnerResponse = { const runnerResponse: RequestRunnerResponse = {
...baseResponse, ...baseResponse,
endpoint: getRequest.endpoint(config.url), endpoint: getRequest.endpoint(config.url),
@@ -258,13 +257,10 @@ export const processRequest =
let updatedEnvs = <HoppEnvs>{}; let updatedEnvs = <HoppEnvs>{};
// Fetch values for secret environment variables from system environment // Fetch values for secret environment variables from system environment
const processedEnvs = processEnvs(envs); const processedEnvs = processEnvs(envs)
// Executing pre-request-script // Executing pre-request-script
const preRequestRes = await preRequestScriptRunner( const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
request,
processedEnvs
)();
if (E.isLeft(preRequestRes)) { if (E.isLeft(preRequestRes)) {
printPreRequestRunner.fail(); printPreRequestRunner.fail();
@@ -351,7 +347,7 @@ export const processRequest =
*/ */
export const preProcessRequest = ( export const preProcessRequest = (
request: HoppRESTRequest, request: HoppRESTRequest,
collection: HoppCollection collection: HoppCollection,
): HoppRESTRequest => { ): HoppRESTRequest => {
const tempRequest = Object.assign({}, request); const tempRequest = Object.assign({}, request);
const { headers: parentHeaders, auth: parentAuth } = collection; const { headers: parentHeaders, auth: parentAuth } = collection;
@@ -376,10 +372,8 @@ export const preProcessRequest = (
// Filter out header entries present in the parent (folder/collection) under the same name // Filter out header entries present in the parent (folder/collection) under the same name
// This ensures the child headers take precedence over the parent headers // This ensures the child headers take precedence over the parent headers
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => { const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
return !tempRequest.headers.some( return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
(reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key })
);
});
tempRequest.headers.push(...filteredEntries); tempRequest.headers.push(...filteredEntries);
} else if (!tempRequest.headers) { } else if (!tempRequest.headers) {
tempRequest.headers = []; tempRequest.headers = [];

View File

@@ -27,7 +27,6 @@
"hide_secret": "Hide secret", "hide_secret": "Hide secret",
"label": "Label", "label": "Label",
"learn_more": "Learn more", "learn_more": "Learn more",
"download_here": "Download here",
"less": "Less", "less": "Less",
"more": "More", "more": "More",
"new": "New", "new": "New",
@@ -104,10 +103,8 @@
"auth": { "auth": {
"account_exists": "Account exists with different credential - Login to link both accounts", "account_exists": "Account exists with different credential - Login to link both accounts",
"all_sign_in_options": "All sign in options", "all_sign_in_options": "All sign in options",
"continue_with_auth_provider": "Continue with {provider}",
"continue_with_email": "Continue with Email", "continue_with_email": "Continue with Email",
"continue_with_github": "Continue with GitHub", "continue_with_github": "Continue with GitHub",
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
"continue_with_google": "Continue with Google", "continue_with_google": "Continue with Google",
"continue_with_microsoft": "Continue with Microsoft", "continue_with_microsoft": "Continue with Microsoft",
"email": "Email", "email": "Email",
@@ -140,26 +137,7 @@
"redirect_no_token_endpoint": "No Token Endpoint Defined", "redirect_no_token_endpoint": "No Token Endpoint Defined",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect", "something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
"something_went_wrong_on_token_generation": "Something went wrong on token generation", "something_went_wrong_on_token_generation": "Something went wrong on token generation",
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed", "token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
"grant_type": "Grant Type",
"grant_type_auth_code": "Authorization Code",
"token_fetched_successfully": "Token fetched successfully",
"token_fetch_failed": "Failed to fetch token",
"validation_failed": "Validation Failed, please check the form fields",
"label_authorization_endpoint": "Authorization Endpoint",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",
"label_code_challenge": "Code Challenge",
"label_code_challenge_method": "Code Challenge Method",
"label_code_verifier": "Code Verifier",
"label_scopes": "Scopes",
"label_token_endpoint": "Token Endpoint",
"label_use_pkce": "Use PKCE",
"label_implicit": "Implicit",
"label_password": "Password",
"label_username": "Username",
"label_auth_code": "Authorization Code",
"label_client_credentials": "Client Credentials"
}, },
"pass_key_by": "Pass by", "pass_key_by": "Pass by",
"password": "Password", "password": "Password",
@@ -303,7 +281,7 @@
"updated": "Environment updated", "updated": "Environment updated",
"value": "Value", "value": "Value",
"variable": "Variable", "variable": "Variable",
"variables": "Variables", "variables":"Variables",
"variable_list": "Variable List" "variable_list": "Variable List"
}, },
"error": { "error": {
@@ -315,7 +293,6 @@
"danger_zone": "Danger zone", "danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these workspaces:", "delete_account": "Your account is currently an owner in these workspaces:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.", "delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
"empty_profile_name": "Profile name cannot be empty",
"empty_req_name": "Empty Request Name", "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",
@@ -335,7 +312,6 @@
"page_not_found": "This page could not be found", "page_not_found": "This page could not be found",
"please_install_extension": "Please install the extension and add origin to the extension.", "please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error", "proxy_error": "Proxy error",
"same_profile_name": "Updated profile name is same as the current profile name",
"script_fail": "Could not execute pre-request script", "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",
@@ -451,7 +427,7 @@
"not_found": "Environment variable “{environment}” not found." "not_found": "Environment variable “{environment}” not found."
}, },
"header": { "header": {
"cookie": "The browser doesn't allow Hoppscotch to set Cookie Headers. Please use Authorization Headers instead. However, our Hoppscotch Desktop App is live now and supports Cookies." "cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
}, },
"response": { "response": {
"401_error": "Please check your authentication credentials.", "401_error": "Please check your authentication credentials.",
@@ -985,8 +961,7 @@
"success_invites": "Success invites", "success_invites": "Success invites",
"title": "Workspaces", "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 workspace.", "we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace."
"search_title": "Team Requests"
}, },
"team_environment": { "team_environment": {
"deleted": "Environment Deleted", "deleted": "Environment Deleted",
@@ -1013,6 +988,7 @@
"workspace": { "workspace": {
"change": "Change workspace", "change": "Change workspace",
"personal": "Personal Workspace", "personal": "Personal Workspace",
"personal_workspace": "{name}'s Workspace",
"other_workspaces": "My Workspaces", "other_workspaces": "My Workspaces",
"team": "Workspace", "team": "Workspace",
"title": "Workspaces" "title": "Workspaces"

View File

@@ -3,214 +3,212 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // 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']
AppContextMenu: (typeof import("./components/app/ContextMenu.vue"))["default"] AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: (typeof import("./components/app/DeveloperOptions.vue"))["default"] AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: (typeof import("./components/app/Footer.vue"))["default"] AppFooter: typeof import('./components/app/Footer.vue')['default']
AppGitHubStarButton: (typeof import("./components/app/GitHubStarButton.vue"))["default"] AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: (typeof import("./components/app/Header.vue"))["default"] AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: (typeof import("./components/app/Inspection.vue"))["default"] AppInspection: typeof import('./components/app/Inspection.vue')['default']
AppInterceptor: (typeof import("./components/app/Interceptor.vue"))["default"] AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: (typeof import("./components/app/Logo.vue"))["default"] AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: (typeof import("./components/app/Options.vue"))["default"] AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: (typeof import("./components/app/PaneLayout.vue"))["default"] AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppShare: (typeof import("./components/app/Share.vue"))["default"] AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: (typeof import("./components/app/Shortcuts.vue"))["default"] AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: (typeof import("./components/app/ShortcutsEntry.vue"))["default"] AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: (typeof import("./components/app/ShortcutsPrompt.vue"))["default"] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: (typeof import("./components/app/Sidenav.vue"))["default"] AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSpotlight: (typeof import("./components/app/spotlight/index.vue"))["default"] AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: (typeof import("./components/app/spotlight/Entry.vue"))["default"] AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: (typeof import("./components/app/spotlight/entry/GQLHistory.vue"))["default"] AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: (typeof import("./components/app/spotlight/entry/GQLRequest.vue"))["default"] AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: (typeof import("./components/app/spotlight/entry/IconSelected.vue"))["default"] AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: (typeof import("./components/app/spotlight/entry/RESTHistory.vue"))["default"] AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: (typeof import("./components/app/spotlight/entry/RESTRequest.vue"))["default"] AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: (typeof import("./components/app/Support.vue"))["default"] AppSupport: typeof import('./components/app/Support.vue')['default']
Collections: (typeof import("./components/collections/index.vue"))["default"] Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: (typeof import("./components/collections/Add.vue"))["default"] CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
CollectionsAddFolder: (typeof import("./components/collections/AddFolder.vue"))["default"] CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
CollectionsAddRequest: (typeof import("./components/collections/AddRequest.vue"))["default"] CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
CollectionsCollection: (typeof import("./components/collections/Collection.vue"))["default"] CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
CollectionsEdit: (typeof import("./components/collections/Edit.vue"))["default"] CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
CollectionsEditFolder: (typeof import("./components/collections/EditFolder.vue"))["default"] CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
CollectionsEditRequest: (typeof import("./components/collections/EditRequest.vue"))["default"] CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
CollectionsGraphql: (typeof import("./components/collections/graphql/index.vue"))["default"] CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
CollectionsGraphqlAdd: (typeof import("./components/collections/graphql/Add.vue"))["default"] CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
CollectionsGraphqlAddFolder: (typeof import("./components/collections/graphql/AddFolder.vue"))["default"] CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
CollectionsGraphqlAddRequest: (typeof import("./components/collections/graphql/AddRequest.vue"))["default"] CollectionsGraphqlAddRequest: typeof import('./components/collections/graphql/AddRequest.vue')['default']
CollectionsGraphqlCollection: (typeof import("./components/collections/graphql/Collection.vue"))["default"] CollectionsGraphqlCollection: typeof import('./components/collections/graphql/Collection.vue')['default']
CollectionsGraphqlEdit: (typeof import("./components/collections/graphql/Edit.vue"))["default"] CollectionsGraphqlEdit: typeof import('./components/collections/graphql/Edit.vue')['default']
CollectionsGraphqlEditFolder: (typeof import("./components/collections/graphql/EditFolder.vue"))["default"] CollectionsGraphqlEditFolder: typeof import('./components/collections/graphql/EditFolder.vue')['default']
CollectionsGraphqlEditRequest: (typeof import("./components/collections/graphql/EditRequest.vue"))["default"] CollectionsGraphqlEditRequest: typeof import('./components/collections/graphql/EditRequest.vue')['default']
CollectionsGraphqlFolder: (typeof import("./components/collections/graphql/Folder.vue"))["default"] CollectionsGraphqlFolder: typeof import('./components/collections/graphql/Folder.vue')['default']
CollectionsGraphqlImportExport: (typeof import("./components/collections/graphql/ImportExport.vue"))["default"] CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
CollectionsGraphqlRequest: (typeof import("./components/collections/graphql/Request.vue"))["default"] CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
CollectionsImportExport: (typeof import("./components/collections/ImportExport.vue"))["default"] CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
CollectionsMyCollections: (typeof import("./components/collections/MyCollections.vue"))["default"] CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
CollectionsProperties: (typeof import("./components/collections/Properties.vue"))["default"] CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
CollectionsRequest: (typeof import("./components/collections/Request.vue"))["default"] CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: (typeof import("./components/collections/SaveRequest.vue"))["default"] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: (typeof import("./components/collections/TeamCollections.vue"))["default"] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: (typeof import("./components/cookies/AllModal.vue"))["default"] CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: (typeof import("./components/cookies/EditCookie.vue"))["default"] CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: (typeof import("./components/embeds/index.vue"))["default"] Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: (typeof import("./components/environments/index.vue"))["default"] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: (typeof import("./components/environments/Add.vue"))["default"] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: (typeof import("./components/environments/ImportExport.vue"))["default"] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
EnvironmentsMy: (typeof import("./components/environments/my/index.vue"))["default"] EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: (typeof import("./components/environments/my/Details.vue"))["default"] EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
EnvironmentsMyEnvironment: (typeof import("./components/environments/my/Environment.vue"))["default"] EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
EnvironmentsSelector: (typeof import("./components/environments/Selector.vue"))["default"] EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
EnvironmentsTeams: (typeof import("./components/environments/teams/index.vue"))["default"] EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
EnvironmentsTeamsDetails: (typeof import("./components/environments/teams/Details.vue"))["default"] EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
EnvironmentsTeamsEnvironment: (typeof import("./components/environments/teams/Environment.vue"))["default"] EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
FirebaseLogin: (typeof import("./components/firebase/Login.vue"))["default"] FirebaseLogin: typeof import('./components/firebase/Login.vue')['default']
FirebaseLogout: (typeof import("./components/firebase/Logout.vue"))["default"] FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
GraphqlAuthorization: (typeof import("./components/graphql/Authorization.vue"))["default"] GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
GraphqlField: (typeof import("./components/graphql/Field.vue"))["default"] GraphqlField: typeof import('./components/graphql/Field.vue')['default']
GraphqlHeaders: (typeof import("./components/graphql/Headers.vue"))["default"] GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
GraphqlQuery: (typeof import("./components/graphql/Query.vue"))["default"] GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
GraphqlRequest: (typeof import("./components/graphql/Request.vue"))["default"] GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
GraphqlRequestOptions: (typeof import("./components/graphql/RequestOptions.vue"))["default"] GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
GraphqlRequestTab: (typeof import("./components/graphql/RequestTab.vue"))["default"] GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
GraphqlResponse: (typeof import("./components/graphql/Response.vue"))["default"] GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
GraphqlSidebar: (typeof import("./components/graphql/Sidebar.vue"))["default"] GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
GraphqlSubscriptionLog: (typeof import("./components/graphql/SubscriptionLog.vue"))["default"] GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
GraphqlTabHead: (typeof import("./components/graphql/TabHead.vue"))["default"] GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
GraphqlType: (typeof import("./components/graphql/Type.vue"))["default"] GraphqlType: typeof import('./components/graphql/Type.vue')['default']
GraphqlTypeLink: (typeof import("./components/graphql/TypeLink.vue"))["default"] GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
GraphqlVariable: (typeof import("./components/graphql/Variable.vue"))["default"] GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
History: (typeof import("./components/history/index.vue"))["default"] History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: (typeof import("./components/history/graphql/Card.vue"))["default"] HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: (typeof import("./components/history/rest/Card.vue"))["default"] HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
HoppButtonPrimary: (typeof import("@hoppscotch/ui"))["HoppButtonPrimary"] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: (typeof import("@hoppscotch/ui"))["HoppButtonSecondary"] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: (typeof import("@hoppscotch/ui"))["HoppSmartAnchor"] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartCheckbox: (typeof import("@hoppscotch/ui"))["HoppSmartCheckbox"] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: (typeof import("@hoppscotch/ui"))["HoppSmartConfirmModal"] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: (typeof import("@hoppscotch/ui"))["HoppSmartExpand"] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: (typeof import("@hoppscotch/ui"))["HoppSmartFileChip"] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: (typeof import("@hoppscotch/ui"))["HoppSmartInput"] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: (typeof import("@hoppscotch/ui"))["HoppSmartIntersection"] HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: (typeof import("@hoppscotch/ui"))["HoppSmartItem"] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: (typeof import("@hoppscotch/ui"))["HoppSmartLink"] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: (typeof import("@hoppscotch/ui"))["HoppSmartModal"] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: (typeof import("@hoppscotch/ui"))["HoppSmartPicture"] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: (typeof import("@hoppscotch/ui"))["HoppSmartPlaceholder"] HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: (typeof import("@hoppscotch/ui"))["HoppSmartProgressRing"] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: (typeof import("@hoppscotch/ui"))["HoppSmartRadio"] HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: (typeof import("@hoppscotch/ui"))["HoppSmartRadioGroup"] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: (typeof import("@hoppscotch/ui"))["HoppSmartSelectWrapper"] HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: (typeof import("@hoppscotch/ui"))["HoppSmartSlideOver"] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: (typeof import("@hoppscotch/ui"))["HoppSmartSpinner"] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: (typeof import("@hoppscotch/ui"))["HoppSmartTab"] HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: (typeof import("@hoppscotch/ui"))["HoppSmartTabs"] HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: (typeof import("@hoppscotch/ui"))["HoppSmartToggle"] HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: (typeof import("@hoppscotch/ui"))["HoppSmartTree"] HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: (typeof import("@hoppscotch/ui"))["HoppSmartWindow"] HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: (typeof import("@hoppscotch/ui"))["HoppSmartWindows"] HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: (typeof import("./components/http/Authorization.vue"))["default"] HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
HttpAuthorizationApiKey: (typeof import("./components/http/authorization/ApiKey.vue"))["default"] HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
HttpAuthorizationBasic: (typeof import("./components/http/authorization/Basic.vue"))["default"] HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
HttpBody: (typeof import("./components/http/Body.vue"))["default"] HttpBody: typeof import('./components/http/Body.vue')['default']
HttpBodyParameters: (typeof import("./components/http/BodyParameters.vue"))["default"] HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
HttpCodegenModal: (typeof import("./components/http/CodegenModal.vue"))["default"] HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
HttpHeaders: (typeof import("./components/http/Headers.vue"))["default"] HttpHeaders: typeof import('./components/http/Headers.vue')['default']
HttpImportCurl: (typeof import("./components/http/ImportCurl.vue"))["default"] HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
HttpOAuth2Authorization: (typeof import("./components/http/OAuth2Authorization.vue"))["default"] HttpOAuth2Authorization: typeof import('./components/http/OAuth2Authorization.vue')['default']
HttpParameters: (typeof import("./components/http/Parameters.vue"))["default"] HttpParameters: typeof import('./components/http/Parameters.vue')['default']
HttpPreRequestScript: (typeof import("./components/http/PreRequestScript.vue"))["default"] HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
HttpRawBody: (typeof import("./components/http/RawBody.vue"))["default"] HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
HttpReqChangeConfirmModal: (typeof import("./components/http/ReqChangeConfirmModal.vue"))["default"] HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
HttpRequest: (typeof import("./components/http/Request.vue"))["default"] HttpRequest: typeof import('./components/http/Request.vue')['default']
HttpRequestOptions: (typeof import("./components/http/RequestOptions.vue"))["default"] HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
HttpRequestTab: (typeof import("./components/http/RequestTab.vue"))["default"] HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
HttpResponse: (typeof import("./components/http/Response.vue"))["default"] HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseMeta: (typeof import("./components/http/ResponseMeta.vue"))["default"] HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
HttpSidebar: (typeof import("./components/http/Sidebar.vue"))["default"] HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: (typeof import("./components/http/TabHead.vue"))["default"] HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: (typeof import("./components/http/TestResult.vue"))["default"] HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
HttpTestResultEntry: (typeof import("./components/http/TestResultEntry.vue"))["default"] HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
HttpTestResultEnv: (typeof import("./components/http/TestResultEnv.vue"))["default"] HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
HttpTestResultReport: (typeof import("./components/http/TestResultReport.vue"))["default"] HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: (typeof import("./components/http/Tests.vue"))["default"] HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: (typeof import("./components/http/URLEncodedParams.vue"))["default"] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: (typeof import("~icons/lucide/activity"))["default"] IconLucideActivity: typeof import('~icons/lucide/activity')['default']
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"] 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']
IconLucideHelpCircle: (typeof import("~icons/lucide/help-circle"))["default"] IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: (typeof import("~icons/lucide/inbox"))["default"] IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideInfo: (typeof import("~icons/lucide/info"))["default"] IconLucideInfo: typeof import('~icons/lucide/info')['default']
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"] 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']
ImportExportBase: (typeof import("./components/importExport/Base.vue"))["default"] ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: (typeof import("./components/importExport/ImportExportList.vue"))["default"] ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: (typeof import("./components/importExport/ImportExportSourcesList.vue"))["default"] ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: (typeof import("./components/importExport/ImportExportSteps/FileImport.vue"))["default"] ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: (typeof import("./components/importExport/ImportExportSteps/MyCollectionImport.vue"))["default"] ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: (typeof import("./components/importExport/ImportExportSteps/UrlImport.vue"))["default"] ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: (typeof import("./components/interceptors/ErrorPlaceholder.vue"))["default"] InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: (typeof import("./components/interceptors/ExtensionSubtitle.vue"))["default"] InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: (typeof import("./components/lenses/HeadersRenderer.vue"))["default"] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: (typeof import("./components/lenses/HeadersRendererEntry.vue"))["default"] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: (typeof import("./components/lenses/renderers/AudioLensRenderer.vue"))["default"] LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
LensesRenderersHTMLLensRenderer: (typeof import("./components/lenses/renderers/HTMLLensRenderer.vue"))["default"] LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
LensesRenderersImageLensRenderer: (typeof import("./components/lenses/renderers/ImageLensRenderer.vue"))["default"] LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
LensesRenderersJSONLensRenderer: (typeof import("./components/lenses/renderers/JSONLensRenderer.vue"))["default"] LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
LensesRenderersPDFLensRenderer: (typeof import("./components/lenses/renderers/PDFLensRenderer.vue"))["default"] LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
LensesRenderersRawLensRenderer: (typeof import("./components/lenses/renderers/RawLensRenderer.vue"))["default"] LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
LensesRenderersVideoLensRenderer: (typeof import("./components/lenses/renderers/VideoLensRenderer.vue"))["default"] LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: (typeof import("./components/lenses/renderers/XMLLensRenderer.vue"))["default"] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: (typeof import("./components/lenses/ResponseBodyRenderer.vue"))["default"] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
ProfileUserDelete: (typeof import("./components/profile/UserDelete.vue"))["default"] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: (typeof import("./components/realtime/Communication.vue"))["default"] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: (typeof import("./components/realtime/ConnectionConfig.vue"))["default"] RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
RealtimeLog: (typeof import("./components/realtime/Log.vue"))["default"] RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: (typeof import("./components/realtime/LogEntry.vue"))["default"] RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: (typeof import("./components/realtime/Subscription.vue"))["default"] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: (typeof import("./components/settings/Extension.vue"))["default"] SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: (typeof import("./components/settings/Proxy.vue"))["default"] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: (typeof import("./components/share/index.vue"))["default"] Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: (typeof import("./components/share/CreateModal.vue"))["default"] ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: (typeof import("./components/share/CustomizeModal.vue"))["default"] ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: (typeof import("./components/share/Modal.vue"))["default"] ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: (typeof import("./components/share/Request.vue"))["default"] ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareTemplatesButton: (typeof import("./components/share/templates/Button.vue"))["default"] ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: (typeof import("./components/share/templates/Embeds.vue"))["default"] ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: (typeof import("./components/share/templates/Link.vue"))["default"] ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
SmartAccentModePicker: (typeof import("./components/smart/AccentModePicker.vue"))["default"] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartChangeLanguage: (typeof import("./components/smart/ChangeLanguage.vue"))["default"] SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
SmartColorModePicker: (typeof import("./components/smart/ColorModePicker.vue"))["default"] SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
SmartEnvInput: (typeof import("./components/smart/EnvInput.vue"))["default"] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
TabPrimary: (typeof import("./components/tab/Primary.vue"))["default"] TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: (typeof import("./components/tab/Secondary.vue"))["default"] TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
Teams: (typeof import("./components/teams/index.vue"))["default"] Teams: typeof import('./components/teams/index.vue')['default']
TeamsAdd: (typeof import("./components/teams/Add.vue"))["default"] TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsEdit: (typeof import("./components/teams/Edit.vue"))["default"] TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
TeamsInvite: (typeof import("./components/teams/Invite.vue"))["default"] TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
TeamsMemberStack: (typeof import("./components/teams/MemberStack.vue"))["default"] TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
TeamsModal: (typeof import("./components/teams/Modal.vue"))["default"] TeamsModal: typeof import('./components/teams/Modal.vue')['default']
TeamsTeam: (typeof import("./components/teams/Team.vue"))["default"] TeamsTeam: typeof import('./components/teams/Team.vue')['default']
Tippy: (typeof import("vue-tippy"))["Tippy"] Tippy: typeof import('vue-tippy')['Tippy']
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']
} }
} }

View File

@@ -331,9 +331,14 @@ const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
const workspaceName = computed(() => { const workspaceName = computed(() => {
return 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 = () => {

View File

@@ -1,32 +0,0 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(title, index) in collectionTitles" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ title }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
:style="{ color: getMethodLabelColor(request.method) }"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
defineProps<{
collectionTitles: string[]
request: {
name: string
method: string
}
}>()
</script>

View File

@@ -111,7 +111,6 @@ import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher" import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher" import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher" import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { TeamsSpotlightSearcherService } from "~/services/spotlight/searchers/teamRequest.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher" import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { import {
SwitchWorkspaceSpotlightSearcherService, SwitchWorkspaceSpotlightSearcherService,
@@ -145,7 +144,6 @@ useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService) useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService) useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService) useService(InterceptorSpotlightSearcherService)
useService(TeamsSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) => platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher) useService(searcher)

View File

@@ -8,7 +8,7 @@
> >
<template #body> <template #body>
<HoppSmartTabs <HoppSmartTabs
v-model="activeTab" v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4" styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs render-inactive-tabs
> >
@@ -16,6 +16,7 @@
<HttpHeaders <HttpHeaders
v-model="editableCollection" v-model="editableCollection"
:is-collection-property="true" :is-collection-property="true"
@change-tab="changeOptionTab"
/> />
<div <div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0" class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -33,7 +34,6 @@
:is-collection-property="true" :is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection" :is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties" :inherited-properties="editingProperties?.inheritedProperties"
:source="source"
/> />
<div <div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0" class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -64,24 +64,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch, ref } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppCollection, HoppRESTAuth, HoppRESTHeaders } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { PersistenceService } from "~/services/persistence"
import { useService } from "dioc/vue"
import { ref, watch } from "vue"
import { useVModel } from "@vueuse/core"
const persistenceService = useService(PersistenceService)
const t = useI18n() const t = useI18n()
export type EditingProperties = { type EditingProperties = {
collection: Partial<HoppCollection> | null collection: HoppCollection | TeamCollection | null
isRootCollection: boolean isRootCollection: boolean
path: string path: string
inheritedProperties?: HoppInheritedProperty inheritedProperties: HoppInheritedProperty | undefined
} }
const props = withDefaults( const props = withDefaults(
@@ -89,8 +86,6 @@ const props = withDefaults(
show: boolean show: boolean
loadingState: boolean loadingState: boolean
editingProperties: EditingProperties | null editingProperties: EditingProperties | null
source: "REST" | "GraphQL"
modelValue: string
}>(), }>(),
{ {
show: false, show: false,
@@ -100,67 +95,50 @@ const props = withDefaults(
) )
const emit = defineEmits<{ const emit = defineEmits<{
( (e: "set-collection-properties", newCollection: any): void
e: "set-collection-properties",
newCollection: Omit<EditingProperties, "inheritedProperties">
): void
(e: "hide-modal"): void (e: "hide-modal"): void
(e: "update:modelValue"): void
}>() }>()
const editableCollection = ref<{ const editableCollection = ref({
headers: HoppRESTHeaders body: {
auth: HoppRESTAuth contentType: null,
}>({ body: null,
},
headers: [], headers: [],
auth: { auth: {
authType: "inherit", authType: "inherit",
authActive: false, authActive: false,
}, },
}) }) as any
watch( const selectedOptionTab = ref("headers")
editableCollection,
(updatedEditableCollection) => {
if (props.show) {
persistenceService.setLocalConfig(
"unsaved_collection_properties",
JSON.stringify(<EditingProperties>{
collection: updatedEditableCollection,
isRootCollection: props.editingProperties?.isRootCollection,
path: props.editingProperties?.path,
inheritedProperties: props.editingProperties?.inheritedProperties,
})
)
}
},
{
deep: true,
}
)
const activeTab = useVModel(props, "modelValue", emit) const changeOptionTab = (tab: RESTOptionTabs) => {
selectedOptionTab.value = tab
}
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show && props.editingProperties?.collection) { if (show && props.editingProperties?.collection) {
editableCollection.value.auth = clone( editableCollection.value.auth = clone(
props.editingProperties.collection.auth as HoppRESTAuth props.editingProperties.collection.auth
) )
editableCollection.value.headers = clone( editableCollection.value.headers = clone(
props.editingProperties.collection.headers as HoppRESTHeaders props.editingProperties.collection.headers
) )
} else { } else {
editableCollection.value = { editableCollection.value = {
body: {
contentType: null,
body: null,
},
headers: [], headers: [],
auth: { auth: {
authType: "inherit", authType: "inherit",
authActive: false, authActive: false,
}, },
} }
persistenceService.removeLocalConfig("unsaved_collection_properties")
} }
} }
) )
@@ -168,6 +146,7 @@ watch(
const saveEditedCollection = () => { const saveEditedCollection = () => {
if (!props.editingProperties) return if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value) const finalCollection = clone(editableCollection.value)
delete finalCollection.body
const collection = { const collection = {
path: props.editingProperties.path, path: props.editingProperties.path,
collection: { collection: {
@@ -176,12 +155,10 @@ const saveEditedCollection = () => {
}, },
isRootCollection: props.editingProperties.isRootCollection, isRootCollection: props.editingProperties.isRootCollection,
} }
emit("set-collection-properties", collection as EditingProperties) emit("set-collection-properties", collection)
persistenceService.removeLocalConfig("unsaved_collection_properties")
} }
const hideModal = () => { const hideModal = () => {
persistenceService.removeLocalConfig("unsaved_collection_properties")
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -9,7 +9,7 @@
" "
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="hasNoTeamAccess || isShowingSearchResults" v-if="hasNoTeamAccess"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
class="!rounded-none" class="!rounded-none"
@@ -36,9 +36,8 @@
v-if="!saveRequest" v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:disabled=" :disabled="
(collectionsType.type === 'team-collections' && collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined) || collectionsType.selectedTeam === undefined
isShowingSearchResults
" "
:icon="IconImport" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@@ -59,7 +58,7 @@
:collections-type="collectionsType.type" :collections-type="collectionsType.type"
:is-open="isOpen" :is-open="isOpen"
:export-loading="exportLoading" :export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults" :has-no-team-access="hasNoTeamAccess"
:collection-move-loading="collectionMoveLoading" :collection-move-loading="collectionMoveLoading"
:is-last-item="node.data.isLastItem" :is-last-item="node.data.isLastItem"
:is-selected=" :is-selected="
@@ -129,14 +128,6 @@
}) })
} }
" "
@click="
() => {
handleCollectionClick({
collectionID: node.id,
isOpen,
})
}
"
/> />
<CollectionsCollection <CollectionsCollection
v-if="node.data.type === 'folders'" v-if="node.data.type === 'folders'"
@@ -146,7 +137,7 @@
:collections-type="collectionsType.type" :collections-type="collectionsType.type"
:is-open="isOpen" :is-open="isOpen"
:export-loading="exportLoading" :export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults" :has-no-team-access="hasNoTeamAccess"
:collection-move-loading="collectionMoveLoading" :collection-move-loading="collectionMoveLoading"
:is-last-item="node.data.isLastItem" :is-last-item="node.data.isLastItem"
:is-selected=" :is-selected="
@@ -218,15 +209,6 @@
}) })
} }
" "
@click="
() => {
handleCollectionClick({
// for the folders, we get a path, so we need to get the last part of the path which is the folder id
collectionID: node.id.split('/').pop() as string,
isOpen,
})
}
"
/> />
<CollectionsRequest <CollectionsRequest
v-if="node.data.type === 'requests'" v-if="node.data.type === 'requests'"
@@ -236,7 +218,7 @@
:collections-type="collectionsType.type" :collections-type="collectionsType.type"
:duplicate-loading="duplicateLoading" :duplicate-loading="duplicateLoading"
:is-active="isActiveRequest(node.data.data.data.id)" :is-active="isActiveRequest(node.data.data.data.id)"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults" :has-no-team-access="hasNoTeamAccess"
:request-move-loading="requestMoveLoading" :request-move-loading="requestMoveLoading"
:is-last-item="node.data.isLastItem" :is-last-item="node.data.isLastItem"
:is-selected=" :is-selected="
@@ -301,15 +283,7 @@
</template> </template>
<template #emptyNode="{ node }"> <template #emptyNode="{ node }">
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="filterText.length !== 0 && teamCollectionList.length === 0" v-if="node === null"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node === null"
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
@@ -420,11 +394,6 @@ const props = defineProps({
default: () => ({ type: "my-collections", selectedTeam: undefined }), default: () => ({ type: "my-collections", selectedTeam: undefined }),
required: true, required: true,
}, },
filterText: {
type: String as PropType<string>,
default: "",
required: true,
},
teamCollectionList: { teamCollectionList: {
type: Array as PropType<TeamCollection[]>, type: Array as PropType<TeamCollection[]>,
default: () => [], default: () => [],
@@ -467,8 +436,6 @@ const props = defineProps({
}, },
}) })
const isShowingSearchResults = computed(() => props.filterText.length > 0)
const emit = defineEmits<{ const emit = defineEmits<{
( (
event: "add-request", event: "add-request",
@@ -576,14 +543,6 @@ const emit = defineEmits<{
} }
} }
): void ): void
(
event: "collection-click",
payload: {
// if the collection is open or not in the tree
isOpen: boolean
collectionID: string
}
): void
(event: "select", payload: Picked | null): void (event: "select", payload: Picked | null): void
(event: "expand-team-collection", payload: string): void (event: "expand-team-collection", payload: string): void
(event: "display-modal-add"): void (event: "display-modal-add"): void
@@ -596,18 +555,6 @@ const getPath = (path: string) => {
return pathArray.join("/") return pathArray.join("/")
} }
const handleCollectionClick = (payload: {
collectionID: string
isOpen: boolean
}) => {
const { collectionID, isOpen } = payload
emit("collection-click", {
collectionID,
isOpen,
})
}
const teamCollectionsList = toRef(props, "teamCollectionList") const teamCollectionsList = toRef(props, "teamCollectionList")
const hasNoTeamAccess = computed( const hasNoTeamAccess = computed(

View File

@@ -146,10 +146,8 @@
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />
<CollectionsProperties <CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties" :show="showModalEditProperties"
:editing-properties="editingProperties" :editing-properties="editingProperties"
source="GraphQL"
@hide-modal="displayModalEditProperties(false)" @hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties" @set-collection-properties="setCollectionProperties"
/> />
@@ -157,7 +155,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onMounted, ref } from "vue" import { nextTick, ref } from "vue"
import { clone, cloneDeep } from "lodash-es" import { clone, cloneDeep } from "lodash-es"
import { import {
graphqlCollections$, graphqlCollections$,
@@ -180,7 +178,6 @@ import { GQLTabService } from "~/services/tab/graphql"
import { computed } from "vue" import { computed } from "vue"
import { import {
HoppCollection, HoppCollection,
HoppGQLAuth,
HoppGQLRequest, HoppGQLRequest,
makeGQLRequest, makeGQLRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
@@ -189,10 +186,6 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection" import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { getRequestsByPath } from "~/helpers/collection/request" import { getRequestsByPath } from "~/helpers/collection/request"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { EditingProperties } from "../Properties.vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -239,52 +232,6 @@ const editingProperties = ref<{
const filterText = ref("") const filterText = ref("")
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<GQLOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "REST") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties<"GraphQL"> =
JSON.parse(unsavedCollectionPropertiesString)
const auth = unsavedCollectionProperties.collection?.auth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
const filteredCollections = computed(() => { const filteredCollections = computed(() => {
const collectionsClone = clone(collections.value) const collectionsClone = clone(collections.value)

View File

@@ -24,6 +24,7 @@
autocomplete="off" autocomplete="off"
class="flex w-full bg-transparent px-4 py-2 h-8" class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="t('action.search')" :placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/> />
</div> </div>
<CollectionsMyCollections <CollectionsMyCollections
@@ -57,15 +58,8 @@
<CollectionsTeamCollections <CollectionsTeamCollections
v-else v-else
:collections-type="collectionsType" :collections-type="collectionsType"
:team-collection-list=" :team-collection-list="teamCollectionList"
filterTexts.length > 0 ? teamsSearchResults : teamCollectionList :team-loading-collections="teamLoadingCollections"
"
:team-loading-collections="
filterTexts.length > 0
? collectionsBeingLoadedFromSearch
: teamLoadingCollections
"
:filter-text="filterTexts"
:export-loading="exportLoading" :export-loading="exportLoading"
:duplicate-loading="duplicateLoading" :duplicate-loading="duplicateLoading"
:save-request="saveRequest" :save-request="saveRequest"
@@ -93,7 +87,6 @@
@expand-team-collection="expandTeamCollection" @expand-team-collection="expandTeamCollection"
@display-modal-add="displayModalAdd(true)" @display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)" @display-modal-import-export="displayModalImportExport(true)"
@collection-click="handleCollectionClick"
/> />
<div <div
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight" class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
@@ -161,10 +154,8 @@
@hide-modal="displayTeamModalAdd(false)" @hide-modal="displayTeamModalAdd(false)"
/> />
<CollectionsProperties <CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties" :show="showModalEditProperties"
:editing-properties="editingProperties" :editing-properties="editingProperties"
source="REST"
@hide-modal="displayModalEditProperties(false)" @hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties" @set-collection-properties="setCollectionProperties"
/> />
@@ -172,7 +163,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, PropType, ref, watch } from "vue" import { computed, nextTick, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
@@ -208,7 +199,7 @@ import {
HoppRESTRequest, HoppRESTRequest,
makeCollection, makeCollection,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { cloneDeep, debounce, isEqual } from "lodash-es" import { cloneDeep, isEqual } from "lodash-es"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { import {
createNewRootCollection, createNewRootCollection,
@@ -249,11 +240,6 @@ import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { EditingProperties } from "./Properties.vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -305,7 +291,12 @@ const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null) const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null) const editingRequestID = ref<string | null>(null)
const editingProperties = ref<EditingProperties>({ const editingProperties = ref<{
collection: Omit<HoppCollection, "v"> | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
collection: null, collection: null,
isRootCollection: false, isRootCollection: false,
path: "", path: "",
@@ -345,99 +336,6 @@ const teamLoadingCollections = useReadonlyStream(
[] []
) )
const {
cascadeParentCollectionForHeaderAuthForSearchResults,
searchTeams,
teamsSearchResults,
teamsSearchResultsLoading,
expandCollection,
expandingCollections,
} = useService(TeamSearchService)
watch(teamsSearchResults, (newSearchResults) => {
if (newSearchResults.length === 1 && filterTexts.value.length > 0) {
expandCollection(newSearchResults[0].id)
}
})
const debouncedSearch = debounce(searchTeams, 400)
const collectionsBeingLoadedFromSearch = computed(() => {
const collections = []
if (teamsSearchResultsLoading.value) {
collections.push("root")
}
collections.push(...expandingCollections.value)
return collections
})
watch(
filterTexts,
(newFilterText) => {
if (collectionsType.value.type === "team-collections") {
const selectedTeamID = collectionsType.value.selectedTeam?.id
selectedTeamID &&
debouncedSearch(newFilterText, selectedTeamID)?.catch((_) => {})
}
},
{
immediate: true,
}
)
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<RESTOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "GraphQL") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties<"REST"> = JSON.parse(
unsavedCollectionPropertiesString
)
// casting because the type `EditingProperties["collection"]["auth"] and the usage in Properties.vue is different. there it's casted as an any.
// FUTURE-TODO: look into this
// @ts-expect-error because of the above reason
const auth = unsavedCollectionProperties.collection?.auth as HoppRESTAuth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
watch( watch(
() => myTeams.value, () => myTeams.value,
(newTeams) => { (newTeams) => {
@@ -466,28 +364,7 @@ const switchToMyCollections = () => {
teamCollectionAdapter.changeTeamID(null) teamCollectionAdapter.changeTeamID(null)
} }
/**
* right now, for search results, we rely on collection click + isOpen to expand the collection
*/
const handleCollectionClick = (payload: {
collectionID: string
isOpen: boolean
}) => {
if (
filterTexts.value.length > 0 &&
teamsSearchResults.value.length &&
payload.isOpen
) {
expandCollection(payload.collectionID)
return
}
}
const expandTeamCollection = (collectionID: string) => { const expandTeamCollection = (collectionID: string) => {
if (filterTexts.value.length > 0 && teamsSearchResults.value) {
return
}
teamCollectionAdapter.expandCollection(collectionID) teamCollectionAdapter.expandCollection(collectionID)
} }
@@ -862,7 +739,7 @@ const onAddRequest = (requestName: string) => {
saveContext: { saveContext: {
originLocation: "team-collection", originLocation: "team-collection",
requestID: createRequestInCollection.id, requestID: createRequestInCollection.id,
collectionID: path, collectionID: createRequestInCollection.collection.id,
teamID: createRequestInCollection.collection.team.id, teamID: createRequestInCollection.collection.team.id,
}, },
inheritedProperties: { inheritedProperties: {
@@ -1453,25 +1330,13 @@ const selectRequest = (selectedRequest: {
let possibleTab = null let possibleTab = null
if (collectionsType.value.type === "team-collections") { if (collectionsType.value.type === "team-collections") {
let inheritedProperties: HoppInheritedProperty | undefined = undefined const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
if (filterTexts.value.length > 0) { possibleTab = tabs.getTabRefWithSaveContext({
const collectionID = folderPath.split("/").at(-1)
if (!collectionID) return
inheritedProperties =
cascadeParentCollectionForHeaderAuthForSearchResults(collectionID)
} else {
inheritedProperties =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
}
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
if (possibleTab) { if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id) tabs.setActiveTab(possibleTab.value.id)
} else { } else {
@@ -1483,7 +1348,10 @@ const selectRequest = (selectedRequest: {
requestID: requestIndex, requestID: requestIndex,
collectionID: folderPath, collectionID: folderPath,
}, },
inheritedProperties: inheritedProperties, inheritedProperties: {
auth,
headers,
},
}) })
} }
} else { } else {
@@ -2153,7 +2021,7 @@ const editProperties = (payload: {
{ {
parentID: "", parentID: "",
parentName: "", parentName: "",
inheritedHeader: {}, inheritedHeaders: [],
}, },
], ],
} as HoppInheritedProperty } as HoppInheritedProperty
@@ -2171,7 +2039,7 @@ const editProperties = (payload: {
} }
editingProperties.value = { editingProperties.value = {
collection: collection as Partial<HoppCollection>, collection,
isRootCollection: isAlreadyInRoot(collectionIndex), isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex, path: collectionIndex,
inheritedProperties, inheritedProperties,
@@ -2215,7 +2083,7 @@ const editProperties = (payload: {
} }
editingProperties.value = { editingProperties.value = {
collection: coll as unknown as Partial<HoppCollection>, collection: coll,
isRootCollection: isAlreadyInRoot(collectionIndex), isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex, path: collectionIndex,
inheritedProperties, inheritedProperties,
@@ -2226,12 +2094,11 @@ const editProperties = (payload: {
} }
const setCollectionProperties = (newCollection: { const setCollectionProperties = (newCollection: {
collection: Partial<HoppCollection> | null collection: HoppCollection
isRootCollection: boolean
path: string path: string
isRootCollection: boolean
}) => { }) => {
const { collection, path, isRootCollection } = newCollection const { collection, path, isRootCollection } = newCollection
if (!collection) return
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
if (isRootCollection) { if (isRootCollection) {
@@ -2281,7 +2148,8 @@ const setCollectionProperties = (newCollection: {
auth, auth,
headers, headers,
}, },
"rest" "rest",
"team"
) )
}, 200) }, 200)
} }

View File

@@ -165,7 +165,7 @@ import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service" import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { uniqueID } from "~/helpers/utils/uniqueID" import { uniqueId } from "lodash-es"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
@@ -277,7 +277,7 @@ const workingEnv = computed(() => {
} as Environment } as Environment
} else if (props.action === "new") { } else if (props.action === "new") {
return { return {
id: uniqueID(), id: uniqueId(),
name: "", name: "",
variables: props.envVars(), variables: props.envVars(),
} }
@@ -331,7 +331,7 @@ watch(
: "variables" : "variables"
if (props.editingEnvironmentIndex !== "Global") { if (props.editingEnvironmentIndex !== "Global") {
editingID.value = workingEnv.value?.id || uniqueID() editingID.value = workingEnv.value?.id ?? uniqueId()
} }
vars.value = pipe( vars.value = pipe(
workingEnv.value?.variables ?? [], workingEnv.value?.variables ?? [],
@@ -416,12 +416,14 @@ const saveEnvironment = () => {
const variables = pipe( const variables = pipe(
filteredVariables, filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e)) A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
) )
const environmentUpdated: Environment = { const environmentUpdated: Environment = {
v: 1, v: 1,
id: uniqueID(), id: uniqueId(),
name: editingName.value, name: editingName.value,
variables, variables,
} }

View File

@@ -360,7 +360,7 @@ const saveEnvironment = async () => {
return return
} }
const filteredVariables = pipe( const filterdVariables = pipe(
vars.value, vars.value,
A.filterMap( A.filterMap(
flow( flow(
@@ -371,15 +371,17 @@ const saveEnvironment = async () => {
) )
const secretVariables = pipe( const secretVariables = pipe(
filteredVariables, filterdVariables,
A.filterMapWithIndex((i, e) => A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
) )
) )
const variables = pipe( const variables = pipe(
filteredVariables, filterdVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e)) A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
) )
const environmentUpdated: Environment = { const environmentUpdated: Environment = {

View File

@@ -23,10 +23,10 @@
@click="provider.action" @click="provider.action"
/> />
<hr v-if="additionalLoginItems.length > 0" /> <hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem <HoppSmartItem
v-for="loginItem in additionalLoginItems" v-for="loginItem in additonalLoginItems"
:key="loginItem.id" :key="loginItem.id"
:icon="loginItem.icon" :icon="loginItem.icon"
:label="loginItem.text(t)" :label="loginItem.text(t)"
@@ -170,7 +170,7 @@ type AuthProviderItem = {
} }
let allowedAuthProviders: AuthProviderItem[] = [] let allowedAuthProviders: AuthProviderItem[] = []
const additionalLoginItems: LoginItemDef[] = [] let additonalLoginItems: LoginItemDef[] = []
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => { const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
await item.onClick() await item.onClick()
@@ -199,33 +199,10 @@ onMounted(async () => {
allowedAuthProviders = enabledAuthProviders allowedAuthProviders = enabledAuthProviders
// setup the additional login items // setup the additional login items
platform.auth.additionalLoginItems?.forEach((item) => { additonalLoginItems =
if (res.right.includes(item.id)) { platform.auth.additionalLoginItems?.filter((item) =>
additionalLoginItems.push(item) res.right.includes(item.id)
} ) ?? []
// since the BE send the OIDC auth providers as OIDC:providerName,
// we need to split the string and use the providerName as the text
if (item.id === "OIDC") {
res.right
.filter((provider) => provider.startsWith("OIDC"))
.forEach((provider) => {
const OIDCName = provider.split(":")[1]
const loginItemText = OIDCName
? () =>
t("auth.continue_with_auth_provider", {
provider: OIDCName,
})
: item.text
const OIDCLoginItem = {
...item,
text: loginItemText,
}
additionalLoginItems.push(OIDCLoginItem)
})
}
})
isLoadingAllowedAuthProviders.value = false isLoadingAllowedAuthProviders.value = false
}) })
@@ -334,14 +311,6 @@ const authProvidersAvailable: AuthProviderItem[] = [
action: signInWithGithub, action: signInWithGithub,
isLoading: signingInWithGitHub, isLoading: signingInWithGitHub,
}, },
// the authprovider either send github or github:enterprise and both are handled by the same route
{
id: "GITHUB:ENTERPRISE",
icon: IconGithub,
label: t("auth.continue_with_github_enterprise"),
action: signInWithGithub,
isLoading: signingInWithGitHub,
},
{ {
id: "GOOGLE", id: "GOOGLE",
icon: IconGoogle, icon: IconGoogle,

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'" :active="authName === 'OAuth 2.0'"
@click=" @click="
() => { () => {
selectOAuth2AuthType() auth.authType = 'oauth-2'
hide() hide()
} }
" "
@@ -189,12 +189,12 @@
<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 <SmartEnvInput
v-model="auth.grantTypeInfo.token" v-model="auth.token"
:environment-highlights="false" :environment-highlights="false"
placeholder="Token" placeholder="Token"
/> />
</div> </div>
<HttpOAuth2Authorization v-model="auth" source="GraphQL" /> <HttpOAuth2Authorization v-model="auth" />
</div> </div>
<div v-if="auth.authType === 'api-key'"> <div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" /> <HttpAuthorizationApiKey v-model="auth" />
@@ -220,22 +220,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { pluckRef } from "@composables/ref"
import { useColorMode } from "@composables/theming"
import { HoppGQLAuth, HoppGQLAuthOAuth2 } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, onMounted, ref } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconExternalLink from "~icons/lucide/external-link"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode" import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppGQLAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { onMounted } from "vue"
const t = useI18n() const t = useI18n()
@@ -283,30 +280,6 @@ const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None" return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
} }
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppGQLAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppGQLAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = <HoppGQLAuth>{
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive") const authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {

View File

@@ -579,18 +579,12 @@ const getComputedAuthHeaders = (
}) })
} else if ( } else if (
request.auth.authType === "bearer" || request.auth.authType === "bearer" ||
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS") request.auth.authType === "oauth-2"
) { ) {
const requestAuth = request.auth
const isOAuth2 = requestAuth.authType === "oauth-2"
const token = isOAuth2 ? requestAuth.grantTypeInfo.token : requestAuth.token
headers.push({ headers.push({
active: true, active: true,
key: "Authorization", key: "Authorization",
value: `Bearer ${token}`, value: `Bearer ${request.auth.token}`,
}) })
} else if (request.auth.authType === "api-key") { } else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth const { key, addTo } = request.auth

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'" :active="authName === 'OAuth 2.0'"
@click=" @click="
() => { () => {
selectOAuth2AuthType() auth.authType = 'oauth-2'
hide() hide()
} }
" "
@@ -177,24 +177,15 @@
/> />
</div> </div>
</div> </div>
<div v-if="auth.authType === 'oauth-2'" class="w-full"> <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">
<!-- Ensure a new object is assigned here to avoid reactivity issues -->
<SmartEnvInput <SmartEnvInput
:model-value="auth.grantTypeInfo.token" v-model="auth.token"
placeholder="Token" placeholder="Token"
:envs="envs" :envs="envs"
@update:model-value="
auth.grantTypeInfo = { ...auth.grantTypeInfo, token: $event }
"
/> />
</div> </div>
<HttpOAuth2Authorization <HttpOAuth2Authorization v-model="auth" :envs="envs" />
v-model="auth"
:is-collection-property="isCollectionProperty"
:envs="envs"
:source="source"
/>
</div> </div>
<div v-if="auth.authType === 'api-key'"> <div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" :envs="envs" /> <HttpAuthorizationApiKey v-model="auth" :envs="envs" />
@@ -226,7 +217,7 @@ import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot" import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle" import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { HoppRESTAuth, HoppRESTAuthOAuth2 } from "@hoppscotch/data" import { HoppRESTAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref" import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
@@ -235,27 +226,17 @@ import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments" import { AggregateEnvironment } from "~/newstore/environments"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const props = withDefaults( const props = defineProps<{
defineProps<{ modelValue: HoppRESTAuth
modelValue: HoppRESTAuth isCollectionProperty?: boolean
isCollectionProperty?: boolean isRootCollection?: boolean
isRootCollection?: boolean inheritedProperties?: HoppInheritedProperty
inheritedProperties?: HoppInheritedProperty envs?: AggregateEnvironment[]
envs?: AggregateEnvironment[] }>()
source?: "REST" | "GraphQL"
}>(),
{
source: "REST",
envs: undefined,
inheritedProperties: undefined,
}
)
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuth): void (e: "update:modelValue", value: HoppRESTAuth): void
@@ -291,30 +272,6 @@ const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None" return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
} }
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppRESTAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppRESTAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = <HoppRESTAuth>{
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive") const authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {

View File

@@ -307,7 +307,6 @@ import { useColorMode } from "@composables/theming"
import { computed, reactive, ref, watch } from "vue" import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es" import { isEqual, cloneDeep } from "lodash-es"
import { import {
HoppRESTAuth,
HoppRESTHeader, HoppRESTHeader,
HoppRESTRequest, HoppRESTRequest,
parseRawKeyValueEntriesE, parseRawKeyValueEntriesE,
@@ -365,12 +364,7 @@ const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit // v-model integration with props and emit
const props = defineProps<{ const props = defineProps<{
modelValue: modelValue: HoppRESTRequest
| HoppRESTRequest
| {
headers: HoppRESTHeader[]
auth: HoppRESTAuth
}
isCollectionProperty?: boolean isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[] envs?: AggregateEnvironment[]

View File

@@ -98,7 +98,6 @@ import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { useNestedSetting } from "~/composables/settings" import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings" import { toggleNestedSetting } from "~/newstore/settings"
import { EditorView } from "@codemirror/view"
const t = useI18n() const t = useI18n()
@@ -125,7 +124,6 @@ useCodemirror(
linter: null, linter: null,
completer: null, completer: null,
environmentHighlights: false, environmentHighlights: false,
onInit: (view: EditorView) => view.focus(),
}) })
) )

View File

@@ -273,10 +273,6 @@ const loading = computed(
) )
onLoggedIn(() => { onLoggedIn(() => {
if (adapter.isInitialized()) {
return
}
try { try {
// wait for a bit to let the auth token to be set // wait for a bit to let the auth token to be set
// because in some race conditions, the token is not set this fixes that // because in some race conditions, the token is not set this fixes that

View File

@@ -20,7 +20,7 @@
: '' : ''
" "
> >
<div class="p-4 truncate"> <div class="p-4">
<label <label
class="font-semibold text-secondaryDark" class="font-semibold text-secondaryDark"
:class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }" :class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }"
@@ -131,7 +131,6 @@
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmRemove" :show="confirmRemove"
:title="t('confirm.remove_team')" :title="t('confirm.remove_team')"
:loading-state="loading"
@hide-modal="confirmRemove = false" @hide-modal="confirmRemove = false"
@resolve="deleteTeam()" @resolve="deleteTeam()"
/> />
@@ -162,8 +161,6 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
import IconUserX from "~icons/lucide/user-x" import IconUserX from "~icons/lucide/user-x"
import IconUserPlus from "~icons/lucide/user-plus" import IconUserPlus from "~icons/lucide/user-plus"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
const t = useI18n() const t = useI18n()
@@ -176,7 +173,6 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: "edit-team"): void (e: "edit-team"): void
(e: "invite-team"): void (e: "invite-team"): void
(e: "refetch-teams"): void
}>() }>()
const toast = useToast() const toast = useToast()
@@ -184,12 +180,7 @@ const toast = useToast()
const confirmRemove = ref(false) const confirmRemove = ref(false)
const confirmExit = ref(false) const confirmExit = ref(false)
const loading = ref(false)
const workspaceService = useService(WorkspaceService)
const deleteTeam = () => { const deleteTeam = () => {
loading.value = true
pipe( pipe(
backendDeleteTeam(props.teamID), backendDeleteTeam(props.teamID),
TE.match( TE.match(
@@ -197,25 +188,9 @@ const deleteTeam = () => {
// TODO: Better errors ? We know the possible errors now // TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`) toast.error(`${t("error.something_went_wrong")}`)
console.error(err) console.error(err)
loading.value = false
confirmRemove.value = false
}, },
() => { () => {
toast.success(`${t("team.deleted")}`) toast.success(`${t("team.deleted")}`)
loading.value = false
emit("refetch-teams")
const currentWorkspace = workspaceService.currentWorkspace.value
// If the current workspace is the deleted workspace, change the workspace to personal
if (
currentWorkspace.type === "team" &&
currentWorkspace.teamID === props.teamID
) {
workspaceService.changeWorkspace({ type: "personal" })
}
confirmRemove.value = false
} }
) )
)() // Tasks (and TEs) are lazy, so call the function returned )() // Tasks (and TEs) are lazy, so call the function returned

View File

@@ -4,7 +4,6 @@
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('team.create_new')}`" :label="`${t('team.create_new')}`"
outline outline
:icon="IconPlus"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
<div v-if="loading" class="flex flex-col items-center justify-center"> <div v-if="loading" class="flex flex-col items-center justify-center">
@@ -17,6 +16,13 @@
:alt="`${t('empty.teams')}`" :alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`" :text="`${t('empty.teams')}`"
> >
<template #body>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div <div
v-else-if="!loading" v-else-if="!loading"
@@ -33,7 +39,6 @@
:compact="modal" :compact="modal"
@edit-team="editTeam(team, team.id)" @edit-team="editTeam(team, team.id)"
@invite-team="inviteTeam(team, team.id)" @invite-team="inviteTeam(team, team.id)"
@refetch-teams="refetchTeams"
/> />
</div> </div>
<div v-if="!loading && adapterError" class="flex flex-col items-center"> <div v-if="!loading && adapterError" class="flex flex-col items-center">
@@ -71,7 +76,6 @@ import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import IconPlus from "~icons/lucide/plus"
const t = useI18n() const t = useI18n()

View File

@@ -15,6 +15,8 @@ import { computed } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform"
const props = defineProps<{ const props = defineProps<{
section?: string section?: string
@@ -26,11 +28,23 @@ const t = useI18n()
const workspaceService = useService(WorkspaceService) const workspaceService = useService(WorkspaceService)
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(),
platform.auth.getProbableUser()
)
const currentWorkspace = computed(() => { const currentWorkspace = computed(() => {
if (props.isOnlyPersonal || workspace.value.type === "personal") { const personalWorkspaceName = currentUser.value?.displayName
return t("workspace.personal") ? t("workspace.personal_workspace", { name: currentUser.value.displayName })
: t("workspace.personal")
if (props.isOnlyPersonal) {
return personalWorkspaceName
} }
return teamWorkspaceName.value if (workspace.value.type === "team") {
return teamWorkspaceName.value
}
return personalWorkspaceName
}) })
const teamWorkspaceName = computed(() => { const teamWorkspaceName = computed(() => {

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<HoppSmartItem <HoppSmartItem
:label="t('workspace.personal')" :label="personalWorkspaceName"
:icon="IconUser" :icon="IconUser"
:info-icon="workspace.type === 'personal' ? IconDone : undefined" :info-icon="workspace.type === 'personal' ? IconDone : undefined"
:active-info-icon="workspace.type === 'personal'" :active-info-icon="workspace.type === 'personal'"
@@ -96,6 +96,12 @@ const currentUser = useReadonlyStream(
platform.auth.getProbableUser() platform.auth.getProbableUser()
) )
const personalWorkspaceName = computed(() =>
currentUser.value?.displayName
? t("workspace.personal_workspace", { name: currentUser.value.displayName })
: t("workspace.personal")
)
const workspaceService = useService(WorkspaceService) const workspaceService = useService(WorkspaceService)
const teamListadapter = workspaceService.acquireTeamListAdapter(null) const teamListadapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListadapter.teamList$, []) const myTeams = useReadonlyStream(teamListadapter.teamList$, [])

View File

@@ -39,7 +39,7 @@ export function onLoggedIn(exec: (user: HoppUser) => void) {
* the auth system. * the auth system.
* *
* NOTE: Unlike `onLoggedIn` for which the callback will be called once on mount with the current state, * NOTE: Unlike `onLoggedIn` for which the callback will be called once on mount with the current state,
* here the callback will only be called on authentication event occurrences. * here the callback will only be called on authentication event occurances.
* You might want to check the auth state from an `onMounted` hook or something * You might want to check the auth state from an `onMounted` hook or something
* if you want to access the initial state * if you want to access the initial state
* *

View File

@@ -68,9 +68,6 @@ type CodeMirrorOptions = {
// callback on editor update // callback on editor update
onUpdate?: (view: ViewUpdate) => void onUpdate?: (view: ViewUpdate) => void
// callback on view initialization
onInit?: (view: EditorView) => void
} }
const hoppCompleterExt = (completer: Completer): Extension => { const hoppCompleterExt = (completer: Completer): Extension => {
@@ -211,9 +208,7 @@ export function useCodemirror(
el: Ref<any | null>, el: Ref<any | null>,
value: Ref<string | undefined>, value: Ref<string | undefined>,
options: CodeMirrorOptions options: CodeMirrorOptions
): { ): { cursor: Ref<{ line: number; ch: number }> } {
cursor: Ref<{ line: number; ch: number }>
} {
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
// Set default value for contextMenuEnabled if not provided // Set default value for contextMenuEnabled if not provided
@@ -388,8 +383,6 @@ export function useCodemirror(
extensions, extensions,
}), }),
}) })
options.onInit?.(view.value)
} }
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,4 @@
import { customRef, onBeforeUnmount, ref, Ref, UnwrapRef, watch } from "vue" import { customRef, onBeforeUnmount, Ref, watch } from "vue"
export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> { export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
return customRef((track, trigger) => { return customRef((track, trigger) => {
@@ -31,16 +31,3 @@ export function pluckMultipleFromRef<T, K extends Array<keyof T>>(
): { [key in K[number]]: Ref<T[key]> } { ): { [key in K[number]]: Ref<T[key]> } {
return Object.fromEntries(keys.map((x) => [x, pluckRef(sourceRef, x)])) as any return Object.fromEntries(keys.map((x) => [x, pluckRef(sourceRef, x)])) as any
} }
export const refWithCallbackOnChange = <T>(
initialValue: T,
callback: (value: UnwrapRef<T>) => void
) => {
const targetRef = ref(initialValue)
watch(targetRef, (value) => {
callback(value)
})
return targetRef
}

View File

@@ -109,6 +109,7 @@ export function updateSaveContextForAffectedRequests(
} }
} }
} }
/** /**
* Used to check the new folder path is close to the save context folder path or not * Used to check the new folder path is close to the save context folder path or not
* @param folderPathCurrent The path saved as the inherited path in the inherited properties * @param folderPathCurrent The path saved as the inherited path in the inherited properties
@@ -122,109 +123,120 @@ function folderPathCloseToSaveContext(
saveContextPath: string saveContextPath: string
) { ) {
if (!folderPathCurrent) return newFolderPath if (!folderPathCurrent) return newFolderPath
const folderPathCurrentArray = folderPathCurrent.split("/") const folderPathCurrentArray = folderPathCurrent.split("/")
const newFolderPathArray = newFolderPath.split("/") const newFolderPathArray = newFolderPath.split("/")
const saveContextFolderPathArray = saveContextPath.split("/") const saveContextFolderPathArray = saveContextPath.split("/")
const folderPathCurrentMatch = folderPathCurrentArray.filter( let folderPathCurrentMatch = 0
(folder, i) => folder === saveContextFolderPathArray[i]
).length
const newFolderPathMatch = newFolderPathArray.filter( for (let i = 0; i < folderPathCurrentArray.length; i++) {
(folder, i) => folder === saveContextFolderPathArray[i] if (folderPathCurrentArray[i] === saveContextFolderPathArray[i]) {
).length folderPathCurrentMatch++
return folderPathCurrentMatch > newFolderPathMatch
? folderPathCurrent
: newFolderPath
}
function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
const keyMap: { [key: string]: number[] } = {} // Map to store array of indices for each key
// Populate keyMap with the indices of each key
arr.forEach((item, index) => {
const key = item.inheritedHeader.key
if (!(key in keyMap)) {
keyMap[key] = []
}
keyMap[key].push(index)
})
// Create a new array containing only the last occurrence of each key
const result = []
for (const key in keyMap) {
if (Object.prototype.hasOwnProperty.call(keyMap, key)) {
const lastIndex = keyMap[key][keyMap[key].length - 1]
result.push(arr[lastIndex])
} }
} }
// Sort the result array based on the parentID let newFolderPathMatch = 0
result.sort((a, b) => a.parentID.localeCompare(b.parentID))
return result for (let i = 0; i < newFolderPathArray.length; i++) {
if (newFolderPathArray[i] === saveContextFolderPathArray[i]) {
newFolderPathMatch++
}
}
if (folderPathCurrentMatch > newFolderPathMatch) {
return folderPathCurrent
}
return newFolderPath
} }
export function updateInheritedPropertiesForAffectedRequests( export function updateInheritedPropertiesForAffectedRequests(
path: string, path: string,
inheritedProperties: HoppInheritedProperty, inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql" type: "rest" | "graphql",
workspace: "personal" | "team" = "personal"
) { ) {
const tabService = const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService) type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
const effectedTabs = tabService.getTabsRefTo((tab) => { let tabs
const saveContext = tab.document.saveContext if (workspace === "personal") {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(path)
)
})
} else {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.collectionID?.startsWith(path)
)
})
}
const saveContextPath = const tabsEffectedByAuth = tabs.filter((tab) => {
saveContext?.originLocation === "team-collection" if (workspace === "personal") {
? saveContext.collectionID return (
: saveContext?.folderPath tab.value.document.saveContext?.originLocation === "user-collection" &&
tab.value.document.saveContext.folderPath.startsWith(path) &&
return saveContextPath?.startsWith(path) ?? false path ===
}) folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
effectedTabs.map((tab) => { path,
const inheritedParentID = tab.value.document.saveContext.folderPath
tab.value.document.inheritedProperties?.auth.parentID )
)
const contextPath =
tab.value.document.saveContext?.originLocation === "team-collection"
? tab.value.document.saveContext.collectionID
: tab.value.document.saveContext?.folderPath
const effectedPath = folderPathCloseToSaveContext(
inheritedParentID,
path,
contextPath ?? ""
)
if (effectedPath === path) {
if (tab.value.document.inheritedProperties) {
tab.value.document.inheritedProperties.auth = inheritedProperties.auth
}
} }
if (tab.value.document.inheritedProperties?.headers) { return (
// filter out the headers with the parentID not as the path tab.value.document.saveContext?.originLocation === "team-collection" &&
const headers = tab.value.document.inheritedProperties.headers.filter( tab.value.document.saveContext.collectionID?.startsWith(path) &&
(header) => header.parentID !== path path ===
) folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.collectionID
)
)
})
// filter out the headers with the parentID as the path in the inheritedProperties const tabsEffectedByHeaders = tabs.filter((tab) => {
const inheritedHeaders = inheritedProperties.headers.filter( return (
tab.value.document.inheritedProperties &&
tab.value.document.inheritedProperties.headers.some(
(header) => header.parentID === path (header) => header.parentID === path
) )
)
// merge the headers with the parentID as the path
const mergedHeaders = removeDuplicatesAndKeepLast([
...new Set([...inheritedHeaders, ...headers]),
])
tab.value.document.inheritedProperties.headers = mergedHeaders
}
}) })
for (const tab of tabsEffectedByAuth) {
tab.value.document.inheritedProperties = inheritedProperties
}
for (const tab of tabsEffectedByHeaders) {
const headers = tab.value.document.inheritedProperties?.headers.map(
(header) => {
if (header.parentID === path) {
return {
...header,
inheritedHeader: inheritedProperties.headers.find(
(inheritedHeader) =>
inheritedHeader.inheritedHeader?.key ===
header.inheritedHeader?.key
)?.inheritedHeader,
}
}
return header
}
)
tab.value.document.inheritedProperties = {
...tab.value.document.inheritedProperties,
headers,
}
}
} }
function resetSaveContextForAffectedRequests(folderPath: string) { function resetSaveContextForAffectedRequests(folderPath: string) {

View File

@@ -66,20 +66,12 @@ export function getRequestsByPath(
let currentCollection = collections[pathArray[0]] let currentCollection = collections[pathArray[0]]
if (pathArray.length === 1) { if (pathArray.length === 1) {
const latestVersionedRequests = currentCollection.requests.filter( return currentCollection.requests
(req): req is HoppRESTRequest => req.v === "3"
)
return latestVersionedRequests
} }
for (let i = 1; i < pathArray.length; i++) { for (let i = 1; i < pathArray.length; i++) {
const folder = currentCollection.folders[pathArray[i]] const folder = currentCollection.folders[pathArray[i]]
if (folder) currentCollection = folder if (folder) currentCollection = folder
} }
const latestVersionedRequests = currentCollection.requests.filter( return currentCollection.requests
(req): req is HoppRESTRequest => req.v === "3"
)
return latestVersionedRequests
} }

View File

@@ -868,38 +868,6 @@ const samples = [
requestVariables: [], requestVariables: [],
}), }),
}, },
{
command: `curl --location 'https://api.example.net/id/1164/requests' \
--header 'Accept: application/vnd.test-data.v2.1+json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'data={"type":"test","typeId":"101"}' \
--data-urlencode 'data2={"type":"test2","typeId":"123"}'`,
response: makeRESTRequest({
method: "POST",
name: "Untitled",
endpoint: "https://api.example.net/id/1164/requests",
auth: {
authType: "inherit",
authActive: true,
},
body: {
contentType: "application/x-www-form-urlencoded",
body: `data: {"type":"test","typeId":"101"}
data2: {"type":"test2","typeId":"123"}`,
},
params: [],
headers: [
{
active: true,
key: "Accept",
value: "application/vnd.test-data.v2.1+json",
},
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
] ]
describe("Parse curl command to Hopp REST Request", () => { describe("Parse curl command to Hopp REST Request", () => {

View File

@@ -33,27 +33,7 @@ export const parseCurlCommand = (curlCommand: string) => {
// const compressed = !!parsedArguments.compressed // const compressed = !!parsedArguments.compressed
curlCommand = preProcessCurlCommand(curlCommand) curlCommand = preProcessCurlCommand(curlCommand)
const parsedArguments = parser(curlCommand)
const args: parser.Arguments = parser(curlCommand)
const parsedArguments = pipe(
args,
O.fromPredicate(
(args) =>
objHasProperty("dataUrlencode", "string")(args) ||
objHasProperty("dataUrlencode", "object")(args)
),
O.map((args) => {
const urlEncodedData: string[] = Array.isArray(args.dataUrlencode)
? args.dataUrlencode
: [args.dataUrlencode]
const data = A.map(decodeURIComponent)(urlEncodedData)
return { ...args, d: data }
}),
O.getOrElse(() => args)
)
const headerObject = getHeaders(parsedArguments) const headerObject = getHeaders(parsedArguments)
const { headers } = headerObject const { headers } = headerObject

View File

@@ -1,10 +1,10 @@
/** /**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record. * Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": "value" }) * (for eg. output -> { "key": "value" })
* NOTE: This function will discard duplicate key occurrences and only keep the last occurrence. If you do not want that behaviour, * NOTE: This function will discard duplicate key occurances and only keep the last occurance. If you do not want that behaviour,
* use `tupleWithSamesKeysToRecord`. * use `tupleWithSamesKeysToRecord`.
* @param tuples Array of tuples ([key, value]) * @param tuples Array of tuples ([key, value])
* @returns A record with value corresponding to the last occurrence of that key * @returns A record with value corresponding to the last occurance of that key
*/ */
export const tupleToRecord = < export const tupleToRecord = <
KeyType extends string | number | symbol, KeyType extends string | number | symbol,

View File

@@ -269,16 +269,8 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
const username = auth.username const username = auth.username
const password = auth.password const password = auth.password
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}` finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
} else if (auth.authType === "bearer") { } else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
finalHeaders.Authorization = `Bearer ${auth.token}` finalHeaders.Authorization = `Bearer ${auth.token}`
} else if (auth.authType === "oauth-2") {
const { addTo } = auth
if (addTo === "HEADERS") {
finalHeaders.Authorization = `Bearer ${auth.grantTypeInfo.token}`
} else if (addTo === "QUERY_PARAMS") {
params["access_token"] = auth.grantTypeInfo.token
}
} else if (auth.authType === "api-key") { } else if (auth.authType === "api-key") {
const { key, value, addTo } = auth const { key, value, addTo } = auth
if (addTo === "Headers") { if (addTo === "Headers") {

View File

@@ -1,19 +1,14 @@
import { import { pipe, flow } from "fp-ts/function"
HoppCollection, import * as TE from "fp-ts/TaskEither"
HoppRESTRequest,
getDefaultGQLRequest,
getDefaultRESTRequest,
translateToNewRESTCollection,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray" import * as RA from "fp-ts/ReadonlyArray"
import * as TE from "fp-ts/TaskEither" import * as A from "fp-ts/Array"
import { flow, pipe } from "fp-ts/function" import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es"
import { HoppGQLRequest, translateToNewGQLCollection } from "@hoppscotch/data"
import { safeParseJSON } from "~/helpers/functional/json"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
export const hoppRESTImporter = (content: string[]) => export const hoppRESTImporter = (content: string[]) =>
pipe( pipe(
@@ -31,30 +26,26 @@ export const hoppRESTImporter = (content: string[]) =>
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT) TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
) )
/**
* checks if a value is a plain object
*/
const isPlainObject = (value: any): value is object => _isPlainObject(value)
/**
* checks if a collection matches the schema for a hoppscotch collection.
* here 2 is the latest version of the schema.
*/
const isValidCollection = (collection: unknown): collection is HoppCollection =>
isPlainObject(collection) && "v" in collection && collection.v === 2
/** /**
* checks if a collection is a valid hoppscotch collection. * checks if a collection is a valid hoppscotch collection.
* else translate it into one. * else translate it into one.
*/ */
const validateCollection = (collection: unknown) => { const validateCollection = (collection: unknown) => {
const collectionSchemaParsedResult = HoppCollection.safeParse(collection) if (isValidCollection(collection)) {
return O.some(collection)
if (collectionSchemaParsedResult.type === "ok") {
const requests = collectionSchemaParsedResult.value.requests.map(
(request) => {
const requestSchemaParsedResult = HoppRESTRequest.safeParse(request)
return requestSchemaParsedResult.type === "ok"
? requestSchemaParsedResult.value
: getDefaultRESTRequest()
}
)
return O.some({
...collectionSchemaParsedResult.value,
requests,
})
} }
return O.some(translateToNewRESTCollection(collection)) return O.some(translateToNewRESTCollection(collection))
} }
@@ -84,24 +75,8 @@ export const hoppGQLImporter = (content: string) =>
* @returns the collection if it is valid, else a translated version of the collection * @returns the collection if it is valid, else a translated version of the collection
*/ */
export const validateGQLCollection = (collection: unknown) => { export const validateGQLCollection = (collection: unknown) => {
const collectionSchemaParsedResult = HoppCollection.safeParse(collection) if (isValidCollection(collection)) {
return O.some(collection)
if (collectionSchemaParsedResult.type === "ok") {
const requests = collectionSchemaParsedResult.value.requests.map(
(request) => {
const requestSchemaParsedResult = HoppGQLRequest.safeParse(request)
return requestSchemaParsedResult.type === "ok"
? requestSchemaParsedResult.value
: getDefaultGQLRequest()
}
)
return O.some({
...collectionSchemaParsedResult.value,
requests,
})
} }
return O.some(translateToNewGQLCollection(collection)) return O.some(translateToNewGQLCollection(collection))
} }

View File

@@ -111,16 +111,12 @@ const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: !(auth.disabled ?? false), authActive: !(auth.disabled ?? false),
grantTypeInfo: { accessTokenURL: replaceVarTemplating(auth.accessTokenUrl ?? ""),
authEndpoint: replaceVarTemplating(auth.authorizationUrl ?? ""), authURL: replaceVarTemplating(auth.authorizationUrl ?? ""),
clientID: replaceVarTemplating(auth.clientId ?? ""), clientID: replaceVarTemplating(auth.clientId ?? ""),
clientSecret: "", oidcDiscoveryURL: "",
grantType: "AUTHORIZATION_CODE", scope: replaceVarTemplating(auth.scope ?? ""),
scopes: replaceVarTemplating(auth.scope ?? ""), token: "",
token: "",
isPKCE: false,
tokenEndpoint: replaceVarTemplating(auth.accessTokenUrl ?? ""),
},
} }
else if (auth.type === "bearer") else if (auth.type === "bearer")
return { return {

View File

@@ -6,7 +6,7 @@ import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { z } from "zod" import { z } from "zod"
import { NonSecretEnvironment } from "@hoppscotch/data" import { NonSecretEnvironment } from "@hoppscotch/data"
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml" import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
import { uniqueID } from "~/helpers/utils/uniqueID" import { uniqueId } from "lodash-es"
const insomniaResourcesSchema = z.object({ const insomniaResourcesSchema = z.object({
resources: z.array( resources: z.array(
@@ -67,7 +67,7 @@ export const insomniaEnvImporter = (contents: string[]) => {
if (parsedInsomniaEnv.success) { if (parsedInsomniaEnv.success) {
const environment: NonSecretEnvironment = { const environment: NonSecretEnvironment = {
id: uniqueID(), id: uniqueId(),
v: 1, v: 1,
name: parsedInsomniaEnv.data.name, name: parsedInsomniaEnv.data.name,
variables: Object.entries(parsedInsomniaEnv.data.data).map( variables: Object.entries(parsedInsomniaEnv.data.data).map(

View File

@@ -279,92 +279,67 @@ const resolveOpenAPIV3SecurityObj = (
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: scheme.flows.authorizationCode.tokenUrl ?? "",
grantType: "AUTHORIZATION_CODE", authURL: scheme.flows.authorizationCode.authorizationUrl ?? "",
authEndpoint: scheme.flows.authorizationCode.authorizationUrl ?? "", clientID: "",
clientID: "", oidcDiscoveryURL: "",
scopes: _schemeData.join(" "), scope: _schemeData.join(" "),
token: "", token: "",
isPKCE: false,
tokenEndpoint: scheme.flows.authorizationCode.tokenUrl ?? "",
clientSecret: "",
},
addTo: "HEADERS",
} }
} else if (scheme.flows.implicit) { } else if (scheme.flows.implicit) {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { authURL: scheme.flows.implicit.authorizationUrl ?? "",
grantType: "IMPLICIT", accessTokenURL: "",
authEndpoint: scheme.flows.implicit.authorizationUrl ?? "", clientID: "",
clientID: "", oidcDiscoveryURL: "",
token: "", scope: _schemeData.join(" "),
scopes: _schemeData.join(" "), token: "",
},
addTo: "HEADERS",
} }
} else if (scheme.flows.password) { } else if (scheme.flows.password) {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { authURL: "",
grantType: "PASSWORD", accessTokenURL: scheme.flows.password.tokenUrl ?? "",
clientID: "", clientID: "",
authEndpoint: scheme.flows.password.tokenUrl, oidcDiscoveryURL: "",
clientSecret: "", scope: _schemeData.join(" "),
password: "", token: "",
username: "",
token: "",
scopes: _schemeData.join(" "),
},
addTo: "HEADERS",
} }
} else if (scheme.flows.clientCredentials) { } else if (scheme.flows.clientCredentials) {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: scheme.flows.clientCredentials.tokenUrl ?? "",
grantType: "CLIENT_CREDENTIALS", authURL: "",
authEndpoint: scheme.flows.clientCredentials.tokenUrl ?? "", clientID: "",
clientID: "", oidcDiscoveryURL: "",
clientSecret: "", scope: _schemeData.join(" "),
scopes: _schemeData.join(" "), token: "",
token: "",
},
addTo: "HEADERS",
} }
} }
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: "",
grantType: "AUTHORIZATION_CODE", authURL: "",
authEndpoint: "", clientID: "",
clientID: "", oidcDiscoveryURL: "",
scopes: _schemeData.join(" "), scope: _schemeData.join(" "),
token: "", token: "",
isPKCE: false,
tokenEndpoint: "",
clientSecret: "",
},
addTo: "HEADERS",
} }
} else if (scheme.type === "openIdConnect") { } else if (scheme.type === "openIdConnect") {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: "",
grantType: "AUTHORIZATION_CODE", authURL: "",
authEndpoint: "", clientID: "",
clientID: "", oidcDiscoveryURL: scheme.openIdConnectUrl ?? "",
scopes: _schemeData.join(" "), scope: _schemeData.join(" "),
token: "", token: "",
isPKCE: false,
tokenEndpoint: "",
clientSecret: "",
},
addTo: "HEADERS",
} }
} }
@@ -441,76 +416,56 @@ const resolveOpenAPIV2SecurityScheme = (
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: scheme.tokenUrl ?? "",
authEndpoint: scheme.authorizationUrl ?? "", authURL: scheme.authorizationUrl ?? "",
clientID: "", clientID: "",
clientSecret: "", oidcDiscoveryURL: "",
grantType: "AUTHORIZATION_CODE", scope: _schemeData.join(" "),
scopes: _schemeData.join(" "), token: "",
token: "",
isPKCE: false,
tokenEndpoint: scheme.tokenUrl ?? "",
},
addTo: "HEADERS",
} }
} else if (scheme.flow === "implicit") { } else if (scheme.flow === "implicit") {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: "",
authEndpoint: scheme.authorizationUrl ?? "", authURL: scheme.authorizationUrl ?? "",
clientID: "", clientID: "",
grantType: "IMPLICIT", oidcDiscoveryURL: "",
scopes: _schemeData.join(" "), scope: _schemeData.join(" "),
token: "", token: "",
},
addTo: "HEADERS",
} }
} else if (scheme.flow === "application") { } else if (scheme.flow === "application") {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: scheme.tokenUrl ?? "",
authEndpoint: scheme.tokenUrl ?? "", authURL: "",
clientID: "", clientID: "",
clientSecret: "", oidcDiscoveryURL: "",
grantType: "CLIENT_CREDENTIALS", scope: _schemeData.join(" "),
scopes: _schemeData.join(" "), token: "",
token: "",
},
addTo: "HEADERS",
} }
} else if (scheme.flow === "password") { } else if (scheme.flow === "password") {
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: scheme.tokenUrl ?? "",
grantType: "PASSWORD", authURL: "",
authEndpoint: scheme.tokenUrl ?? "", clientID: "",
clientID: "", oidcDiscoveryURL: "",
clientSecret: "", scope: _schemeData.join(" "),
password: "", token: "",
scopes: _schemeData.join(" "),
token: "",
username: "",
},
addTo: "HEADERS",
} }
} }
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: "",
authEndpoint: "", authURL: "",
clientID: "", clientID: "",
clientSecret: "", oidcDiscoveryURL: "",
grantType: "AUTHORIZATION_CODE", scope: _schemeData.join(" "),
scopes: _schemeData.join(" "), token: "",
token: "",
isPKCE: false,
tokenEndpoint: "",
},
addTo: "HEADERS",
} }
} }

View File

@@ -162,36 +162,25 @@ const getHoppReqAuth = (item: Item): HoppRESTAuth => {
), ),
} }
} else if (auth.type === "oauth2") { } else if (auth.type === "oauth2") {
const accessTokenURL = replacePMVarTemplating(
getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
)
const authURL = replacePMVarTemplating(
getVariableValue(auth.oauth2, "authUrl") ?? ""
)
const clientId = replacePMVarTemplating(
getVariableValue(auth.oauth2, "clientId") ?? ""
)
const scope = replacePMVarTemplating(
getVariableValue(auth.oauth2, "scope") ?? ""
)
const token = replacePMVarTemplating(
getVariableValue(auth.oauth2, "accessToken") ?? ""
)
return { return {
authType: "oauth-2", authType: "oauth-2",
authActive: true, authActive: true,
grantTypeInfo: { accessTokenURL: replacePMVarTemplating(
grantType: "AUTHORIZATION_CODE", getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
authEndpoint: authURL, ),
clientID: clientId, authURL: replacePMVarTemplating(
scopes: scope, getVariableValue(auth.oauth2, "authUrl") ?? ""
token: token, ),
tokenEndpoint: accessTokenURL, clientID: replacePMVarTemplating(
clientSecret: "", getVariableValue(auth.oauth2, "clientId") ?? ""
isPKCE: false, ),
}, scope: replacePMVarTemplating(
addTo: "HEADERS", getVariableValue(auth.oauth2, "scope") ?? ""
),
token: replacePMVarTemplating(
getVariableValue(auth.oauth2, "accessToken") ?? ""
),
oidcDiscoveryURL: "",
} }
} }

View File

@@ -1,11 +1,11 @@
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { uniqueId } from "lodash-es"
import { z } from "zod" import { z } from "zod"
import { safeParseJSON } from "~/helpers/functional/json" import { safeParseJSON } from "~/helpers/functional/json"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { uniqueID } from "~/helpers/utils/uniqueID"
const postmanEnvSchema = z.object({ const postmanEnvSchema = z.object({
name: z.string(), name: z.string(),
@@ -49,7 +49,7 @@ export const postmanEnvImporter = (contents: string[]) => {
// Convert `values` to `variables` to match the format expected by the system // Convert `values` to `variables` to match the format expected by the system
const environments: Environment[] = validationResult.data.map( const environments: Environment[] = validationResult.data.map(
({ name, values }) => ({ ({ name, values }) => ({
id: uniqueID(), id: uniqueId(),
v: 1, v: 1,
name, name,
variables: values.map((entires) => ({ ...entires, secret: false })), variables: values.map((entires) => ({ ...entires, secret: false })),

View File

@@ -1,625 +0,0 @@
import {
HoppRESTAuth,
HoppRESTHeader,
HoppRESTRequest,
getDefaultRESTRequest,
} from "@hoppscotch/data"
import axios from "axios"
import { Service } from "dioc"
import * as E from "fp-ts/Either"
import { Ref, ref } from "vue"
import { runGQLQuery } from "../backend/GQLClient"
import {
GetCollectionChildrenDocument,
GetCollectionRequestsDocument,
GetSingleCollectionDocument,
GetSingleRequestDocument,
} from "../backend/graphql"
import { TeamCollection } from "./TeamCollection"
import { platform } from "~/platform"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { TeamRequest } from "./TeamRequest"
type CollectionSearchMeta = {
isSearchResult?: boolean
insertedWhileExpanding?: boolean
}
type CollectionSearchNode =
| {
type: "request"
title: string
method: string
id: string
// parent collections
path: CollectionSearchNode[]
}
| {
type: "collection"
title: string
id: string
// parent collections
path: CollectionSearchNode[]
}
type _SearchCollection = TeamCollection & {
parentID: string | null
meta?: CollectionSearchMeta
}
type _SearchRequest = {
id: string
collectionID: string
title: string
request: {
name: string
method: string
}
meta?: CollectionSearchMeta
}
function convertToTeamCollection(
node: CollectionSearchNode & {
meta?: CollectionSearchMeta
},
existingCollections: Record<string, _SearchCollection>,
existingRequests: Record<string, _SearchRequest>
) {
if (node.type === "request") {
existingRequests[node.id] = {
id: node.id,
collectionID: node.path[0].id,
title: node.title,
request: {
name: node.title,
method: node.method,
},
meta: {
isSearchResult: node.meta?.isSearchResult || false,
},
}
if (node.path[0]) {
// add parent collections to the collections array recursively
convertToTeamCollection(
node.path[0],
existingCollections,
existingRequests
)
}
} else {
existingCollections[node.id] = {
id: node.id,
title: node.title,
children: [],
requests: [],
data: null,
parentID: node.path[0]?.id,
meta: {
isSearchResult: node.meta?.isSearchResult || false,
},
}
if (node.path[0]) {
// add parent collections to the collections array recursively
convertToTeamCollection(
node.path[0],
existingCollections,
existingRequests
)
}
}
return {
existingCollections,
existingRequests,
}
}
function convertToTeamTree(
collections: (TeamCollection & { parentID: string | null })[],
requests: TeamRequest[]
) {
const collectionTree: TeamCollection[] = []
collections.forEach((collection) => {
const parentCollection = collection.parentID
? collections.find((c) => c.id === collection.parentID)
: null
const isAlreadyInserted = parentCollection?.children?.find(
(c) => c.id === collection.id
)
if (isAlreadyInserted) return
if (parentCollection) {
parentCollection.children = parentCollection.children || []
parentCollection.children.push(collection)
} else {
collectionTree.push(collection)
}
})
requests.forEach((request) => {
const parentCollection = collections.find(
(c) => c.id === request.collectionID
)
const isAlreadyInserted = parentCollection?.requests?.find(
(r) => r.id === request.id
)
if (isAlreadyInserted) return
if (parentCollection) {
const requestSchemaParsedResult = HoppRESTRequest.safeParse(
request.request
)
const effectiveRequest =
requestSchemaParsedResult.type === "ok"
? requestSchemaParsedResult.value
: getDefaultRESTRequest()
parentCollection.requests = parentCollection.requests || []
parentCollection.requests.push({
id: request.id,
collectionID: request.collectionID,
title: request.title,
request: effectiveRequest,
})
}
})
return collectionTree
}
export class TeamSearchService extends Service {
public static readonly ID = "TeamSearchService"
public endpoint = import.meta.env.VITE_BACKEND_API_URL
public teamsSearchResultsLoading = ref(false)
public teamsSearchResults = ref<TeamCollection[]>([])
public teamsSearchResultsFormattedForSpotlight = ref<
{
collectionTitles: string[]
request: {
id: string
name: string
method: string
}
}[]
>([])
searchResultsCollections: Record<string, _SearchCollection> = {}
searchResultsRequests: Record<string, _SearchRequest> = {}
expandingCollections: Ref<string[]> = ref([])
expandedCollections: Ref<string[]> = ref([])
// FUTURE-TODO: ideally this should return the search results / formatted results instead of directly manipulating the result set
// eg: do the spotlight formatting in the spotlight searcher and not here
searchTeams = async (query: string, teamID: string) => {
if (!query.length) {
return
}
this.teamsSearchResultsLoading.value = true
this.searchResultsCollections = {}
this.searchResultsRequests = {}
this.expandedCollections.value = []
const axiosPlatformConfig = platform.auth.axiosPlatformConfig?.() ?? {}
try {
const searchResponse = await axios.get(
`${
this.endpoint
}/team-collection/search/${teamID}?searchQuery=${encodeURIComponent(
query
)}`,
axiosPlatformConfig
)
if (searchResponse.status !== 200) {
return
}
const searchResults = searchResponse.data.data as CollectionSearchNode[]
searchResults
.map((node) => {
const { existingCollections, existingRequests } =
convertToTeamCollection(
{
...node,
meta: {
isSearchResult: true,
},
},
{},
{}
)
return {
collections: existingCollections,
requests: existingRequests,
}
})
.forEach(({ collections, requests }) => {
this.searchResultsCollections = {
...this.searchResultsCollections,
...collections,
}
this.searchResultsRequests = {
...this.searchResultsRequests,
...requests,
}
})
const collectionFetchingPromises = Object.values(
this.searchResultsCollections
).map((col) => {
return getSingleCollection(col.id)
})
const requestFetchingPromises = Object.values(
this.searchResultsRequests
).map((req) => {
return getSingleRequest(req.id)
})
const collectionResponses = await Promise.all(collectionFetchingPromises)
const requestResponses = await Promise.all(requestFetchingPromises)
requestResponses.map((res) => {
if (E.isLeft(res)) {
return
}
const request = res.right.request
if (!request) return
this.searchResultsRequests[request.id] = {
id: request.id,
title: request.title,
request: JSON.parse(request.request) as TeamRequest["request"],
collectionID: request.collectionID,
}
})
collectionResponses.map((res) => {
if (E.isLeft(res)) {
return
}
const collection = res.right.collection
if (!collection) return
this.searchResultsCollections[collection.id].data =
collection.data ?? null
})
const collectionTree = convertToTeamTree(
Object.values(this.searchResultsCollections),
// asserting because we've already added the missing properties after fetching the full details
Object.values(this.searchResultsRequests) as TeamRequest[]
)
this.teamsSearchResults.value = collectionTree
this.teamsSearchResultsFormattedForSpotlight.value = Object.values(
this.searchResultsRequests
).map((request) => {
return formatTeamsSearchResultsForSpotlight(
{
collectionID: request.collectionID,
name: request.title,
method: request.request.method,
id: request.id,
},
Object.values(this.searchResultsCollections)
)
})
} catch (error) {
console.error(error)
}
this.teamsSearchResultsLoading.value = false
}
cascadeParentCollectionForHeaderAuthForSearchResults = (
collectionID: string
): HoppInheritedProperty => {
const defaultInheritedAuth: HoppInheritedProperty["auth"] = {
parentID: "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const defaultInheritedHeaders: HoppInheritedProperty["headers"] = []
const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID
)
if (!collection)
return { auth: defaultInheritedAuth, headers: defaultInheritedHeaders }
const inheritedAuthData = this.findInheritableParentAuth(collectionID)
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
return {
auth: E.isRight(inheritedAuthData)
? inheritedAuthData.right
: defaultInheritedAuth,
headers: E.isRight(inheritedHeadersData)
? Object.values(inheritedHeadersData.right)
: defaultInheritedHeaders,
}
}
findInheritableParentAuth = (
collectionID: string
): E.Either<
string,
{
parentID: string
parentName: string
inheritedAuth: HoppRESTAuth
}
> => {
const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID
)
if (!collection) {
return E.left("PARENT_NOT_FOUND" as const)
}
// has inherited data
if (collection.data) {
const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
}
const inheritedAuth = parentInheritedData.auth
if (inheritedAuth.authType !== "inherit") {
return E.right({
parentID: collectionID,
parentName: collection.title,
inheritedAuth: inheritedAuth,
})
}
}
if (!collection.parentID) {
return E.left("PARENT_INHERITED_DATA_NOT_FOUND")
}
return this.findInheritableParentAuth(collection.parentID)
}
findInheritableParentHeaders = (
collectionID: string,
existingHeaders: Record<
string,
HoppInheritedProperty["headers"][number]
> = {}
): E.Either<
string,
Record<string, HoppInheritedProperty["headers"][number]>
> => {
const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID
)
if (!collection) {
return E.left("PARENT_NOT_FOUND" as const)
}
// see if it has headers to inherit, if yes, add it to the existing headers
if (collection.data) {
const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
}
const inheritedHeaders = parentInheritedData.headers
if (inheritedHeaders) {
inheritedHeaders.forEach((header) => {
if (!existingHeaders[header.key]) {
existingHeaders[header.key] = {
parentID: collection.id,
parentName: collection.title,
inheritedHeader: header,
}
}
})
}
}
if (collection.parentID) {
return this.findInheritableParentHeaders(
collection.parentID,
existingHeaders
)
}
return E.right(existingHeaders)
}
expandCollection = async (collectionID: string) => {
if (this.expandingCollections.value.includes(collectionID)) return
const collectionToExpand = Object.values(
this.searchResultsCollections
).find((col) => col.id === collectionID)
const isAlreadyExpanded =
this.expandedCollections.value.includes(collectionID)
// only allow search result collections to be expanded
if (
isAlreadyExpanded ||
!collectionToExpand ||
!(
collectionToExpand.meta?.isSearchResult ||
collectionToExpand.meta?.insertedWhileExpanding
)
)
return
this.expandingCollections.value.push(collectionID)
const childCollectionsPromise = getCollectionChildCollections(collectionID)
const childRequestsPromise = getCollectionChildRequests(collectionID)
const [childCollections, childRequests] = await Promise.all([
childCollectionsPromise,
childRequestsPromise,
])
if (E.isLeft(childCollections)) {
return
}
if (E.isLeft(childRequests)) {
return
}
childCollections.right.collection?.children
.map((child) => ({
id: child.id,
title: child.title,
data: child.data ?? null,
children: [],
requests: [],
}))
.forEach((child) => {
this.searchResultsCollections[child.id] = {
...child,
parentID: collectionID,
meta: {
isSearchResult: false,
insertedWhileExpanding: true,
},
}
})
childRequests.right.requestsInCollection
.map((request) => ({
id: request.id,
collectionID: collectionID,
title: request.title,
request: JSON.parse(request.request) as TeamRequest["request"],
}))
.forEach((request) => {
this.searchResultsRequests[request.id] = {
...request,
meta: {
isSearchResult: false,
insertedWhileExpanding: true,
},
}
})
this.teamsSearchResults.value = convertToTeamTree(
Object.values(this.searchResultsCollections),
// asserting because we've already added the missing properties after fetching the full details
Object.values(this.searchResultsRequests) as TeamRequest[]
)
// remove the collection after expanding
this.expandingCollections.value = this.expandingCollections.value.filter(
(colID) => colID !== collectionID
)
this.expandedCollections.value.push(collectionID)
}
}
const getSingleCollection = (collectionID: string) =>
runGQLQuery({
query: GetSingleCollectionDocument,
variables: {
collectionID,
},
})
const getSingleRequest = (requestID: string) =>
runGQLQuery({
query: GetSingleRequestDocument,
variables: {
requestID,
},
})
const getCollectionChildCollections = (collectionID: string) =>
runGQLQuery({
query: GetCollectionChildrenDocument,
variables: {
collectionID,
},
})
const getCollectionChildRequests = (collectionID: string) =>
runGQLQuery({
query: GetCollectionRequestsDocument,
variables: {
collectionID,
},
})
const formatTeamsSearchResultsForSpotlight = (
request: {
collectionID: string
name: string
method: string
id: string
},
parentCollections: (TeamCollection & { parentID: string | null })[]
) => {
let collectionTitles: string[] = []
let parentCollectionID: string | null = request.collectionID
while (true) {
if (!parentCollectionID) {
break
}
const parentCollection = parentCollections.find(
(col) => col.id === parentCollectionID
)
if (!parentCollection) {
break
}
collectionTitles = [parentCollection.title, ...collectionTitles]
parentCollectionID = parentCollection.parentID
}
return {
collectionTitles,
request: {
name: request.name,
method: request.method,
id: request.id,
},
}
}

View File

@@ -18,8 +18,6 @@ import {
HoppRESTParam, HoppRESTParam,
parseRawKeyValueEntriesE, parseRawKeyValueEntriesE,
parseTemplateStringE, parseTemplateStringE,
HoppRESTAuth,
HoppRESTHeaders,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { arrayFlatMap, arraySort } from "../functional/array" import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData" import { toFormData } from "../functional/formData"
@@ -46,12 +44,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
*/ */
export const getComputedAuthHeaders = ( export const getComputedAuthHeaders = (
envVars: Environment["variables"], envVars: Environment["variables"],
req?: req?: HoppRESTRequest,
| HoppRESTRequest
| {
auth: HoppRESTAuth
headers: HoppRESTHeaders
},
auth?: HoppRESTRequest["auth"], auth?: HoppRESTRequest["auth"],
parse = true parse = true
) => { ) => {
@@ -82,17 +75,16 @@ export const getComputedAuthHeaders = (
}) })
} else if ( } else if (
request.auth.authType === "bearer" || request.auth.authType === "bearer" ||
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS") request.auth.authType === "oauth-2"
) { ) {
const token =
request.auth.authType === "bearer"
? request.auth.token
: request.auth.grantTypeInfo.token
headers.push({ headers.push({
active: true, active: true,
key: "Authorization", key: "Authorization",
value: `Bearer ${parse ? parseTemplateString(token, envVars) : token}`, value: `Bearer ${
parse
? parseTemplateString(request.auth.token, envVars)
: request.auth.token
}`,
}) })
} else if (request.auth.authType === "api-key") { } else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth const { key, addTo } = request.auth
@@ -116,12 +108,7 @@ export const getComputedAuthHeaders = (
* @returns The list of headers * @returns The list of headers
*/ */
export const getComputedBodyHeaders = ( export const getComputedBodyHeaders = (
req: req: HoppRESTRequest
| HoppRESTRequest
| {
auth: HoppRESTAuth
headers: HoppRESTHeaders
}
): HoppRESTHeader[] => { ): HoppRESTHeader[] => {
// If a content-type is already defined, that will override this // If a content-type is already defined, that will override this
if ( if (
@@ -131,10 +118,8 @@ export const getComputedBodyHeaders = (
) )
return [] return []
if (!("body" in req)) return []
// Body should have a non-null content-type // Body should have a non-null content-type
if (!req.body || req.body.contentType === null) return [] if (req.body.contentType === null) return []
return [ return [
{ {
@@ -158,12 +143,7 @@ export type ComputedHeader = {
* @returns The headers that are generated along with the source of that header * @returns The headers that are generated along with the source of that header
*/ */
export const getComputedHeaders = ( export const getComputedHeaders = (
req: req: HoppRESTRequest,
| HoppRESTRequest
| {
auth: HoppRESTAuth
headers: HoppRESTHeaders
},
envVars: Environment["variables"], envVars: Environment["variables"],
parse = true parse = true
): ComputedHeader[] => { ): ComputedHeader[] => {
@@ -197,40 +177,17 @@ export const getComputedParams = (
): ComputedParam[] => { ): ComputedParam[] => {
// When this gets complex, its best to split this function off (like with getComputedHeaders) // When this gets complex, its best to split this function off (like with getComputedHeaders)
// API-key auth can be added to query params // API-key auth can be added to query params
if (!req.auth || !req.auth.authActive) { if (!req.auth || !req.auth.authActive) return []
return [] if (req.auth.authType !== "api-key") return []
} if (req.auth.addTo !== "Query params") return []
if (req.auth.authType !== "api-key" && req.auth.authType !== "oauth-2") {
return []
}
if (req.auth.addTo !== "QUERY_PARAMS") {
return []
}
if (req.auth.authType === "api-key") {
return [
{
source: "auth" as const,
param: {
active: true,
key: parseTemplateString(req.auth.key, envVars),
value: parseTemplateString(req.auth.value, envVars),
},
},
]
}
const { grantTypeInfo } = req.auth
return [ return [
{ {
source: "auth", source: "auth",
param: { param: {
active: true, active: true,
key: "access_token", key: parseTemplateString(req.auth.key, envVars),
value: parseTemplateString(grantTypeInfo.token, envVars), value: parseTemplateString(req.auth.value, envVars),
}, },
}, },
] ]
@@ -274,7 +231,7 @@ function getFinalBodyFromRequest(
if (request.body.contentType === "application/x-www-form-urlencoded") { if (request.body.contentType === "application/x-www-form-urlencoded") {
const parsedBodyRecord = pipe( const parsedBodyRecord = pipe(
request.body.body ?? "", request.body.body,
parseRawKeyValueEntriesE, parseRawKeyValueEntriesE,
E.map( E.map(
flow( flow(
@@ -311,7 +268,7 @@ function getFinalBodyFromRequest(
if (request.body.contentType === "multipart/form-data") { if (request.body.contentType === "multipart/form-data") {
return pipe( return pipe(
request.body.body ?? [], request.body.body,
A.filter((x) => (x.key !== "" || x.isFile) && x.active), // Remove empty keys A.filter((x) => (x.key !== "" || x.isFile) && x.active), // Remove empty keys
// Sort files down // Sort files down

View File

@@ -1,3 +0,0 @@
export const uniqueID = (length = 16) => {
return Math.random().toString(36).substring(2, length)
}

View File

@@ -1,8 +1,7 @@
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es" import { cloneDeep, isEqual, uniqueId } from "lodash-es"
import { combineLatest, Observable } from "rxjs" import { combineLatest, Observable } from "rxjs"
import { distinctUntilChanged, map, pluck } from "rxjs/operators" import { distinctUntilChanged, map, pluck } from "rxjs/operators"
import { uniqueID } from "~/helpers/utils/uniqueID"
import { getService } from "~/modules/dioc" import { getService } from "~/modules/dioc"
import DispatchingStore, { import DispatchingStore, {
defineDispatchers, defineDispatchers,
@@ -23,7 +22,7 @@ const defaultEnvironmentsState = {
environments: [ environments: [
{ {
v: 1, v: 1,
id: uniqueID(), id: uniqueId(),
name: "My Environment Variables", name: "My Environment Variables",
variables: [], variables: [],
}, },
@@ -101,7 +100,7 @@ const dispatchers = defineDispatchers({
} }
: { : {
v: 1, v: 1,
id: uniqueID(), id: "",
name, name,
variables, variables,
}, },
@@ -124,7 +123,7 @@ const dispatchers = defineDispatchers({
...environments, ...environments,
{ {
...cloneDeep(newEnvironment), ...cloneDeep(newEnvironment),
id: uniqueID(), id: uniqueId(),
name: `${newEnvironment.name} - Duplicate`, name: `${newEnvironment.name} - Duplicate`,
}, },
], ],

View File

@@ -79,7 +79,7 @@ const importCollections = (url: unknown, type: unknown) =>
content.data, content.data,
TO.fromPredicate(isOfType("string")), TO.fromPredicate(isOfType("string")),
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT), TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT),
TE.chain((data) => importer.importer([data])) TE.chain((data) => importer.importer(data))
) )
) )
) )

View File

@@ -5,31 +5,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "~/composables/i18n" import { handleOAuthRedirect } from "~/helpers/oauth"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { onMounted } from "vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { onMounted } from "vue"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import {
PersistedOAuthConfig,
routeOAuthRedirect,
} from "~/services/oauth/oauth.service"
import { PersistenceService } from "~/services/persistence"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
const gqlTabs = useService(GQLTabService) const tabs = useService(RESTTabService)
const persistenceService = useService(PersistenceService)
const restTabs = useService(RESTTabService)
function translateOAuthRedirectError(error: string) { function translateOAuthRedirectError(error: string) {
switch (error) { switch (error) {
@@ -68,58 +60,22 @@ function translateOAuthRedirectError(error: string) {
} }
onMounted(async () => { onMounted(async () => {
const localOAuthTempConfig = const tokenInfo = await handleOAuthRedirect()
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) { if (E.isLeft(tokenInfo)) {
toast.error(t("authorization.oauth.something_went_wrong_on_oauth_redirect")) toast.error(translateOAuthRedirectError(tokenInfo.left))
router.push("/") router.push("/")
return return
} }
const persistedOAuthConfig: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
const { context, source } = persistedOAuthConfig
const tokenInfo = await routeOAuthRedirect()
if (E.isLeft(tokenInfo)) {
toast.error(translateOAuthRedirectError(tokenInfo.left))
router.push(source === "REST" ? "/" : "/graphql")
return
}
// Indicates the access token generation flow originated from the modal for setting authorization/headers at the collection level
if (context?.type === "collection-properties") {
// Set the access token in `localStorage` to retrieve from the modal while redirecting back
persistenceService.setLocalConfig(
"oauth_temp_config",
JSON.stringify(<PersistedOAuthConfig>{
...persistedOAuthConfig,
token: tokenInfo.right.access_token,
})
)
toast.success(t("authorization.oauth.token_fetched_successfully"))
router.push(source === "REST" ? "/" : "/graphql")
return
}
const routeToRedirect = source === "GraphQL" ? "/graphql" : "/"
const tabService = source === "GraphQL" ? gqlTabs : restTabs
if ( if (
tabService.currentActiveTab.value.document.request.auth.authType === tabs.currentActiveTab.value.document.request.auth.authType === "oauth-2"
"oauth-2"
) { ) {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token = tabs.currentActiveTab.value.document.request.auth.token =
tokenInfo.right.access_token tokenInfo.right.access_token
toast.success(t("authorization.oauth.token_fetched_successfully")) router.push("/")
return
} }
router.push(routeToRedirect)
}) })
</script> </script>

View File

@@ -210,8 +210,6 @@ import { toggleSetting } from "~/newstore/settings"
import IconVerified from "~icons/lucide/verified" import IconVerified from "~icons/lucide/verified"
import IconSettings from "~icons/lucide/settings" import IconSettings from "~icons/lucide/settings"
import * as E from "fp-ts/Either"
type ProfileTabs = "sync" | "teams" type ProfileTabs = "sync" | "teams"
const selectedProfileTab = ref<ProfileTabs>("sync") const selectedProfileTab = ref<ProfileTabs>("sync")
@@ -246,28 +244,19 @@ const displayName = ref(currentUser.value?.displayName || "")
const updatingDisplayName = ref(false) const updatingDisplayName = ref(false)
watchEffect(() => (displayName.value = currentUser.value?.displayName || "")) watchEffect(() => (displayName.value = currentUser.value?.displayName || ""))
const updateDisplayName = async () => { const updateDisplayName = () => {
if (!displayName.value) {
toast.error(`${t("error.empty_profile_name")}`)
return
}
if (currentUser.value?.displayName === displayName.value) {
toast.error(`${t("error.same_profile_name")}`)
return
}
updatingDisplayName.value = true updatingDisplayName.value = true
platform.auth
const res = await platform.auth.setDisplayName(displayName.value) .setDisplayName(displayName.value as string)
.then(() => {
if (E.isLeft(res)) { toast.success(`${t("profile.updated")}`)
toast.error(t("error.something_went_wrong")) })
} else if (E.isRight(res)) { .catch(() => {
toast.success(`${t("profile.updated")}`) toast.error(`${t("error.something_went_wrong")}`)
} })
.finally(() => {
updatingDisplayName.value = false updatingDisplayName.value = false
})
} }
const emailAddress = ref(currentUser.value?.email || "") const emailAddress = ref(currentUser.value?.email || "")

View File

@@ -3,8 +3,6 @@ import { Observable } from "rxjs"
import { Component } from "vue" import { Component } from "vue"
import { getI18n } from "~/modules/i18n" import { getI18n } from "~/modules/i18n"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { AxiosRequestConfig } from "axios"
import { GQLError } from "~/helpers/backend/GQLClient"
/** /**
* A common (and required) set of fields that describe a user. * A common (and required) set of fields that describe a user.
@@ -137,15 +135,6 @@ export type AuthPlatformDef = {
*/ */
getGQLClientOptions?: () => Partial<ClientOptions> getGQLClientOptions?: () => Partial<ClientOptions>
/**
* called by the platform to provide additional/different config options when
* sending requests with axios
* eg: SH needs to include cookies in the request, while Central doesn't and throws a cors error if it does
*
* @returns AxiosRequestConfig
*/
axiosPlatformConfig?: () => AxiosRequestConfig
/** /**
* Returns the string content that should be returned when the user selects to * Returns the string content that should be returned when the user selects to
* copy auth token from Developer Options. * copy auth token from Developer Options.
@@ -230,11 +219,9 @@ export type AuthPlatformDef = {
/** /**
* Updates the display name of the user * Updates the display name of the user
* @param name The new name to set this to. * @param name The new name to set this to.
* @returns A promise that resolves with the display name update status when the operation is complete * @returns An empty promise that is resolved when the operation is complete
*/ */
setDisplayName: ( setDisplayName: (name: string) => Promise<void>
name: string
) => Promise<E.Either<GQLError<string>, undefined>>
/** /**
* Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML ) * Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML )

View File

@@ -231,7 +231,6 @@ export class ExtensionInterceptorService
try { try {
const result = await extensionHook.sendRequest({ const result = await extensionHook.sendRequest({
...req, ...req,
headers: req.headers ?? {},
wantsBinary: true, wantsBinary: true,
}) })

View File

@@ -66,8 +66,8 @@ export class HeaderInspectorService extends Service implements Inspector {
index: index, index: index,
}, },
doc: { doc: {
text: this.t("action.download_here"), text: this.t("action.learn_more"),
link: "https://hoppscotch.com/download", link: "https://docs.hoppscotch.io/documentation/features/inspections",
}, },
}) })
} }

View File

@@ -1,293 +0,0 @@
import { PersistenceService } from "~/services/persistence"
import {
OauthAuthService,
PersistedOAuthConfig,
createFlowConfig,
decodeResponseAsJSON,
generateRandomString,
} from "../oauth.service"
import { z } from "zod"
import { getService } from "~/modules/dioc"
import * as E from "fp-ts/Either"
import { InterceptorService } from "~/services/interceptor.service"
import { AuthCodeGrantTypeParams } from "@hoppscotch/data"
const persistenceService = getService(PersistenceService)
const interceptorService = getService(InterceptorService)
const AuthCodeOauthFlowParamsSchema = AuthCodeGrantTypeParams.pick({
authEndpoint: true,
tokenEndpoint: true,
clientID: true,
clientSecret: true,
scopes: true,
isPKCE: true,
codeVerifierMethod: true,
})
.refine(
(params) => {
return (
params.authEndpoint.length >= 1 &&
params.tokenEndpoint.length >= 1 &&
params.clientID.length >= 1 &&
params.clientSecret.length >= 1 &&
(!params.scopes || params.scopes.trim().length >= 1)
)
},
{
message: "Minimum length requirement not met for one or more parameters",
}
)
.refine((params) => (params.isPKCE ? !!params.codeVerifierMethod : true), {
message: "codeVerifierMethod is required when using PKCE",
path: ["codeVerifierMethod"],
})
export type AuthCodeOauthFlowParams = z.infer<
typeof AuthCodeOauthFlowParamsSchema
>
export const getDefaultAuthCodeOauthFlowParams =
(): AuthCodeOauthFlowParams => ({
authEndpoint: "",
tokenEndpoint: "",
clientID: "",
clientSecret: "",
scopes: undefined,
isPKCE: false,
codeVerifierMethod: "S256",
})
const initAuthCodeOauthFlow = async ({
tokenEndpoint,
clientID,
clientSecret,
scopes,
authEndpoint,
isPKCE,
codeVerifierMethod,
}: AuthCodeOauthFlowParams) => {
const state = generateRandomString()
let codeVerifier: string | undefined
let codeChallenge: string | undefined
if (isPKCE) {
codeVerifier = generateCodeVerifier()
codeChallenge = await generateCodeChallenge(
codeVerifier,
codeVerifierMethod
)
}
let oauthTempConfig: {
state: string
grant_type: "AUTHORIZATION_CODE"
authEndpoint: string
tokenEndpoint: string
clientSecret: string
clientID: string
isPKCE: boolean
codeVerifier?: string
codeVerifierMethod?: string
codeChallenge?: string
scopes?: string
} = {
state,
grant_type: "AUTHORIZATION_CODE",
authEndpoint,
tokenEndpoint,
clientSecret,
clientID,
isPKCE,
codeVerifierMethod,
scopes,
}
if (codeVerifier && codeChallenge) {
oauthTempConfig = {
...oauthTempConfig,
codeVerifier,
codeChallenge,
}
}
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
const persistedOAuthConfig: PersistedOAuthConfig = localOAuthTempConfig
? { ...JSON.parse(localOAuthTempConfig) }
: {}
const { grant_type, ...rest } = oauthTempConfig
// persist the state so we can compare it when we get redirected back
// also persist the grant_type,tokenEndpoint and clientSecret so we can use them when we get redirected back
persistenceService.setLocalConfig(
"oauth_temp_config",
JSON.stringify(<PersistedOAuthConfig>{
...persistedOAuthConfig,
fields: rest,
grant_type,
})
)
let url: URL
try {
url = new URL(authEndpoint)
} catch (e) {
return E.left("INVALID_AUTH_ENDPOINT")
}
url.searchParams.set("grant_type", "authorization_code")
url.searchParams.set("client_id", clientID)
url.searchParams.set("state", state)
url.searchParams.set("response_type", "code")
url.searchParams.set("redirect_uri", OauthAuthService.redirectURI)
if (scopes) url.searchParams.set("scope", scopes)
if (codeVerifierMethod && codeChallenge) {
url.searchParams.set("code_challenge", codeChallenge)
url.searchParams.set("code_challenge_method", codeVerifierMethod)
}
// Redirect to the authorization server
window.location.assign(url.toString())
return E.right(undefined)
}
const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
// parse the query string
const params = new URLSearchParams(window.location.search)
const code = params.get("code")
const state = params.get("state")
const error = params.get("error")
if (error) {
return E.left("AUTH_SERVER_RETURNED_ERROR")
}
if (!code) {
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
const expectedSchema = z.object({
source: z.optional(z.string()),
state: z.string(),
tokenEndpoint: z.string(),
clientSecret: z.string(),
clientID: z.string(),
codeVerifier: z.string().optional(),
codeChallenge: z.string().optional(),
})
const decodedLocalConfig = expectedSchema.safeParse(
JSON.parse(localConfig).fields
)
if (!decodedLocalConfig.success) {
return E.left("INVALID_LOCAL_CONFIG")
}
// check if the state matches
if (decodedLocalConfig.data.state !== state) {
return E.left("INVALID_STATE")
}
// exchange the code for a token
const formData = new URLSearchParams()
formData.append("grant_type", "authorization_code")
formData.append("code", code)
formData.append("client_id", decodedLocalConfig.data.clientID)
formData.append("client_secret", decodedLocalConfig.data.clientSecret)
formData.append("redirect_uri", OauthAuthService.redirectURI)
if (decodedLocalConfig.data.codeVerifier) {
formData.append("code_verifier", decodedLocalConfig.data.codeVerifier)
}
const { response } = interceptorService.runRequest({
url: decodedLocalConfig.data.tokenEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = decodeResponseAsJSON(res.right)
if (E.isLeft(responsePayload)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
responsePayload.right
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
const generateCodeVerifier = () => {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
const length = Math.floor(Math.random() * (128 - 43 + 1)) + 43 // Random length between 43 and 128
let codeVerifier = ""
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length)
codeVerifier += characters[randomIndex]
}
return codeVerifier
}
const generateCodeChallenge = async (
codeVerifier: string,
strategy: AuthCodeOauthFlowParams["codeVerifierMethod"]
) => {
if (strategy === "plain") {
return codeVerifier
}
const encoder = new TextEncoder()
const data = encoder.encode(codeVerifier)
const buffer = await crypto.subtle.digest("SHA-256", data)
return encodeArrayBufferAsUrlEncodedBase64(buffer)
}
const encodeArrayBufferAsUrlEncodedBase64 = (buffer: ArrayBuffer) => {
const hashArray = Array.from(new Uint8Array(buffer))
const hashBase64URL = btoa(String.fromCharCode(...hashArray))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_")
return hashBase64URL
}
export default createFlowConfig(
"AUTHORIZATION_CODE" as const,
AuthCodeOauthFlowParamsSchema,
initAuthCodeOauthFlow,
handleRedirectForAuthCodeOauthFlow
)

View File

@@ -1,183 +0,0 @@
import {
OauthAuthService,
createFlowConfig,
decodeResponseAsJSON,
} from "../oauth.service"
import { z } from "zod"
import { getService } from "~/modules/dioc"
import * as E from "fp-ts/Either"
import { InterceptorService } from "~/services/interceptor.service"
import { useToast } from "~/composables/toast"
import { ClientCredentialsGrantTypeParams } from "@hoppscotch/data"
const interceptorService = getService(InterceptorService)
const ClientCredentialsFlowParamsSchema = ClientCredentialsGrantTypeParams.pick(
{
authEndpoint: true,
clientID: true,
clientSecret: true,
scopes: true,
}
).refine(
(params) => {
return (
params.authEndpoint.length >= 1 &&
params.clientID.length >= 1 &&
params.clientSecret.length >= 1 &&
(!params.scopes || params.scopes.length >= 1)
)
},
{
message: "Minimum length requirement not met for one or more parameters",
}
)
export type ClientCredentialsFlowParams = z.infer<
typeof ClientCredentialsFlowParamsSchema
>
export const getDefaultClientCredentialsFlowParams =
(): ClientCredentialsFlowParams => ({
authEndpoint: "",
clientID: "",
clientSecret: "",
scopes: undefined,
})
const initClientCredentialsOAuthFlow = async ({
clientID,
clientSecret,
scopes,
authEndpoint,
}: ClientCredentialsFlowParams) => {
const toast = useToast()
const formData = new URLSearchParams()
formData.append("grant_type", "client_credentials")
formData.append("client_id", clientID)
formData.append("client_secret", clientSecret)
if (scopes) {
formData.append("scope", scopes)
}
const { response } = interceptorService.runRequest({
url: authEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = decodeResponseAsJSON(res.right)
if (E.isLeft(responsePayload)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
responsePayload.right
)
if (!parsedTokenResponse.success) {
toast.error("AUTH_TOKEN_REQUEST_INVALID_RESPONSE")
}
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
// parse the query string
const params = new URLSearchParams(window.location.search)
const code = params.get("code")
const state = params.get("state")
const error = params.get("error")
if (error) {
return E.left("AUTH_SERVER_RETURNED_ERROR")
}
if (!code) {
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
const expectedSchema = z.object({
state: z.string(),
tokenEndpoint: z.string(),
clientSecret: z.string(),
clientID: z.string(),
})
const decodedLocalConfig = expectedSchema.safeParse(JSON.parse(localConfig))
if (!decodedLocalConfig.success) {
return E.left("INVALID_LOCAL_CONFIG")
}
// check if the state matches
if (decodedLocalConfig.data.state !== state) {
return E.left("INVALID_STATE")
}
// exchange the code for a token
const formData = new URLSearchParams()
formData.append("code", code)
formData.append("client_id", decodedLocalConfig.data.clientID)
formData.append("client_secret", decodedLocalConfig.data.clientSecret)
formData.append("redirect_uri", OauthAuthService.redirectURI)
const { response } = interceptorService.runRequest({
url: decodedLocalConfig.data.tokenEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = new TextDecoder("utf-8")
.decode(res.right.data as any)
.replaceAll("\x00", "")
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
JSON.parse(responsePayload)
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
export default createFlowConfig(
"CLIENT_CREDENTIALS" as const,
ClientCredentialsFlowParamsSchema,
initClientCredentialsOAuthFlow,
handleRedirectForAuthCodeOauthFlow
)

View File

@@ -1,135 +0,0 @@
import { PersistenceService } from "~/services/persistence"
import {
OauthAuthService,
PersistedOAuthConfig,
createFlowConfig,
generateRandomString,
} from "../oauth.service"
import { z } from "zod"
import { getService } from "~/modules/dioc"
import * as E from "fp-ts/Either"
import { ImplicitOauthFlowParams } from "@hoppscotch/data"
const persistenceService = getService(PersistenceService)
const ImplicitOauthFlowParamsSchema = ImplicitOauthFlowParams.pick({
authEndpoint: true,
clientID: true,
scopes: true,
}).refine((params) => {
return (
params.authEndpoint.length >= 1 &&
params.clientID.length >= 1 &&
(params.scopes === undefined || params.scopes.length >= 1)
)
})
export type ImplicitOauthFlowParams = z.infer<
typeof ImplicitOauthFlowParamsSchema
>
export const getDefaultImplicitOauthFlowParams =
(): ImplicitOauthFlowParams => ({
authEndpoint: "",
clientID: "",
scopes: undefined,
})
const initImplicitOauthFlow = async ({
clientID,
scopes,
authEndpoint,
}: ImplicitOauthFlowParams) => {
const state = generateRandomString()
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
const persistedOAuthConfig: PersistedOAuthConfig = localOAuthTempConfig
? { ...JSON.parse(localOAuthTempConfig) }
: {}
// Persist the necessary information for retrieval while getting redirected back
persistenceService.setLocalConfig(
"oauth_temp_config",
JSON.stringify(<PersistedOAuthConfig>{
...persistedOAuthConfig,
fields: {
clientID,
authEndpoint,
scopes,
state,
},
grant_type: "IMPLICIT",
})
)
let url: URL
try {
url = new URL(authEndpoint)
} catch {
return E.left("INVALID_AUTH_ENDPOINT")
}
url.searchParams.set("client_id", clientID)
url.searchParams.set("state", state)
url.searchParams.set("response_type", "token")
url.searchParams.set("redirect_uri", OauthAuthService.redirectURI)
if (scopes) url.searchParams.set("scope", scopes)
// Redirect to the authorization server
window.location.assign(url.toString())
return E.right(undefined)
}
const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
// parse the query string
const params = new URLSearchParams(window.location.search)
const paramsFromHash = new URLSearchParams(window.location.hash.substring(1))
const accessToken =
params.get("access_token") || paramsFromHash.get("access_token")
const state = params.get("state") || paramsFromHash.get("state")
const error = params.get("error") || paramsFromHash.get("error")
if (error) {
return E.left("AUTH_SERVER_RETURNED_ERROR")
}
if (!accessToken) {
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
const expectedSchema = z.object({
source: z.optional(z.string()),
state: z.string(),
clientID: z.string(),
})
const decodedLocalConfig = expectedSchema.safeParse(
JSON.parse(localConfig).fields
)
if (!decodedLocalConfig.success) {
return E.left("INVALID_LOCAL_CONFIG")
}
// check if the state matches
if (decodedLocalConfig.data.state !== state) {
return E.left("INVALID_STATE")
}
return E.right({
access_token: accessToken,
})
}
export default createFlowConfig(
"IMPLICIT" as const,
ImplicitOauthFlowParamsSchema,
initImplicitOauthFlow,
handleRedirectForAuthCodeOauthFlow
)

View File

@@ -1,189 +0,0 @@
import {
OauthAuthService,
createFlowConfig,
decodeResponseAsJSON,
} from "../oauth.service"
import { z } from "zod"
import { getService } from "~/modules/dioc"
import * as E from "fp-ts/Either"
import { InterceptorService } from "~/services/interceptor.service"
import { useToast } from "~/composables/toast"
import { PasswordGrantTypeParams } from "@hoppscotch/data"
const interceptorService = getService(InterceptorService)
const PasswordFlowParamsSchema = PasswordGrantTypeParams.pick({
authEndpoint: true,
clientID: true,
clientSecret: true,
scopes: true,
username: true,
password: true,
}).refine(
(params) => {
return (
params.authEndpoint.length >= 1 &&
params.clientID.length >= 1 &&
params.clientSecret.length >= 1 &&
params.username.length >= 1 &&
params.password.length >= 1 &&
(!params.scopes || params.scopes.length >= 1)
)
},
{
message: "Minimum length requirement not met for one or more parameters",
}
)
export type PasswordFlowParams = z.infer<typeof PasswordFlowParamsSchema>
export const getDefaultPasswordFlowParams = (): PasswordFlowParams => ({
authEndpoint: "",
clientID: "",
clientSecret: "",
scopes: undefined,
username: "",
password: "",
})
const initPasswordOauthFlow = async ({
password,
username,
clientID,
clientSecret,
scopes,
authEndpoint,
}: PasswordFlowParams) => {
const toast = useToast()
const formData = new URLSearchParams()
formData.append("grant_type", "password")
formData.append("client_id", clientID)
formData.append("client_secret", clientSecret)
formData.append("username", username)
formData.append("password", password)
if (scopes) {
formData.append("scope", scopes)
}
const { response } = interceptorService.runRequest({
url: authEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res) || res.right.status !== 200) {
toast.error("AUTH_TOKEN_REQUEST_FAILED")
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = new TextDecoder("utf-8")
.decode(res.right.data as any)
.replaceAll("\x00", "")
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
JSON.parse(responsePayload)
)
if (!parsedTokenResponse.success) {
toast.error("AUTH_TOKEN_REQUEST_INVALID_RESPONSE")
}
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
// parse the query string
const params = new URLSearchParams(window.location.search)
const code = params.get("code")
const state = params.get("state")
const error = params.get("error")
if (error) {
return E.left("AUTH_SERVER_RETURNED_ERROR")
}
if (!code) {
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
const expectedSchema = z.object({
state: z.string(),
tokenEndpoint: z.string(),
clientSecret: z.string(),
clientID: z.string(),
})
const decodedLocalConfig = expectedSchema.safeParse(JSON.parse(localConfig))
if (!decodedLocalConfig.success) {
return E.left("INVALID_LOCAL_CONFIG")
}
// check if the state matches
if (decodedLocalConfig.data.state !== state) {
return E.left("INVALID_STATE")
}
// exchange the code for a token
const formData = new URLSearchParams()
formData.append("code", code)
formData.append("client_id", decodedLocalConfig.data.clientID)
formData.append("client_secret", decodedLocalConfig.data.clientSecret)
formData.append("redirect_uri", OauthAuthService.redirectURI)
const { response } = interceptorService.runRequest({
url: decodedLocalConfig.data.tokenEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = decodeResponseAsJSON(res.right)
if (E.isLeft(responsePayload)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
responsePayload.right
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
export default createFlowConfig(
"PASSWORD" as const,
PasswordFlowParamsSchema,
initPasswordOauthFlow,
handleRedirectForAuthCodeOauthFlow
)

View File

@@ -1,124 +0,0 @@
import { Service } from "dioc"
import { PersistenceService } from "../persistence"
import { ZodType, z } from "zod"
import * as E from "fp-ts/Either"
import authCode, { AuthCodeOauthFlowParams } from "./flows/authCode"
import implicit, { ImplicitOauthFlowParams } from "./flows/implicit"
import { getService } from "~/modules/dioc"
import { HoppCollection } from "@hoppscotch/data"
import { TeamCollection } from "~/helpers/backend/graphql"
export type PersistedOAuthConfig = {
source: "REST" | "GraphQL"
context?: {
type: "collection-properties" | "request-tab"
metadata: {
collection?: HoppCollection | TeamCollection
collectionID?: string
}
}
grant_type: string
fields?: (AuthCodeOauthFlowParams | ImplicitOauthFlowParams) & {
state: string
}
token?: string
}
const persistenceService = getService(PersistenceService)
export const grantTypesInvolvingRedirect = ["AUTHORIZATION_CODE", "IMPLICIT"]
export const routeOAuthRedirect = async () => {
// get the temp data from the local storage
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return E.left("INVALID_STATE")
}
const expectedSchema = z.object({
source: z.optional(z.string()),
grant_type: z.string(),
})
const decodedLocalConfig = expectedSchema.safeParse(
JSON.parse(localOAuthTempConfig)
)
if (!decodedLocalConfig.success) {
return E.left("INVALID_STATE")
}
// route the request to the correct flow
const flowConfig = [authCode, implicit].find(
(flow) => flow.flow === decodedLocalConfig.data.grant_type
)
if (!flowConfig) {
return E.left("INVALID_STATE")
}
return flowConfig?.onRedirectReceived(localOAuthTempConfig)
}
export function createFlowConfig<
Flow extends string,
AuthParams extends Record<string, unknown>,
InitFuncReturnObject extends Record<string, unknown>,
>(
flow: Flow,
params: ZodType<AuthParams>,
init: (
params: AuthParams
) =>
| E.Either<string, InitFuncReturnObject>
| Promise<E.Either<string, InitFuncReturnObject>>
| E.Either<string, undefined>
| Promise<E.Either<string, undefined>>,
onRedirectReceived: (localConfig: string) => Promise<
E.Either<
string,
{
access_token: string
}
>
>
) {
return {
flow,
params,
init,
onRedirectReceived,
}
}
export const decodeResponseAsJSON = (response: { data: any }) => {
try {
const responsePayload = new TextDecoder("utf-8")
.decode(response.data as any)
.replaceAll("\x00", "")
return E.right(JSON.parse(responsePayload) as Record<string, unknown>)
} catch (error) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
}
export class OauthAuthService extends Service {
public static readonly ID = "OAUTH_AUTH_SERVICE"
static redirectURI = `${window.location.origin}/oauth`
constructor() {
super()
}
}
export const generateRandomString = () => {
const length = 64
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
const values = crypto.getRandomValues(new Uint8Array(length))
return values.reduce((acc, x) => acc + possible[x % possible.length], "")
}

View File

@@ -25,7 +25,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
folders: [], folders: [],
requests: [ requests: [
{ {
v: "3", v: "2",
endpoint: "https://echo.hoppscotch.io", endpoint: "https://echo.hoppscotch.io",
name: "Echo test", name: "Echo test",
params: [], params: [],
@@ -50,7 +50,7 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
folders: [], folders: [],
requests: [ requests: [
{ {
v: 3, v: 2,
name: "Echo test", name: "Echo test",
url: "https://echo.hoppscotch.io/graphql", url: "https://echo.hoppscotch.io/graphql",
headers: [], headers: [],
@@ -138,7 +138,7 @@ export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
preRequestScript: "", preRequestScript: "",
testScript: "", testScript: "",
requestVariables: [], requestVariables: [],
v: "3", v: "2",
}, },
responseMeta: { duration: 807, statusCode: 200 }, responseMeta: { duration: 807, statusCode: 200 },
star: false, star: false,
@@ -150,7 +150,7 @@ export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [
{ {
v: 1, v: 1,
request: { request: {
v: 3, v: 2,
name: "Untitled", name: "Untitled",
url: "https://echo.hoppscotch.io/graphql", url: "https://echo.hoppscotch.io/graphql",
query: "query Request { url }", query: "query Request { url }",
@@ -171,7 +171,7 @@ export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc", tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
doc: { doc: {
request: { request: {
v: 3, v: 2,
name: "Untitled", name: "Untitled",
url: "https://echo.hoppscotch.io/graphql", url: "https://echo.hoppscotch.io/graphql",
headers: [], headers: [],
@@ -194,7 +194,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa", tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
doc: { doc: {
request: { request: {
v: "3", v: "2",
endpoint: "https://echo.hoppscotch.io", endpoint: "https://echo.hoppscotch.io",
name: "Echo test", name: "Echo test",
params: [], params: [],

View File

@@ -169,16 +169,7 @@ export class CollectionsSpotlightSearcherService
} }
scopeHandle.run(() => { scopeHandle.run(() => {
const isPersonalWorkspace = computed(
() => this.workspaceService.currentWorkspace.value.type === "personal"
)
watch(query, (query) => { watch(query, (query) => {
if (!isPersonalWorkspace.value) {
results.value = []
return
}
if (pageCategory === "other") { if (pageCategory === "other") {
results.value = [] results.value = []
return return

View File

@@ -1,159 +0,0 @@
import { Service } from "dioc"
import {
SpotlightSearcher,
SpotlightSearcherResult,
SpotlightSearcherSessionState,
SpotlightService,
} from ".."
import { getI18n } from "~/modules/i18n"
import { Ref, computed, effectScope, markRaw, watch } from "vue"
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
import { cloneDeep, debounce } from "lodash-es"
import IconFolder from "~icons/lucide/folder"
import { WorkspaceService } from "~/services/workspace.service"
import RESTTeamRequestEntry from "~/components/app/spotlight/entry/RESTTeamRequestEntry.vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { HoppRESTRequest } from "@hoppscotch/data"
export class TeamsSpotlightSearcherService
extends Service
implements SpotlightSearcher
{
public static readonly ID = "TEAMS_SPOTLIGHT_SEARCHER_SERVICE"
private t = getI18n()
public searcherID = "teams"
public searcherSectionTitle = this.t("team.search_title")
private readonly spotlight = this.bind(SpotlightService)
private readonly teamsSearch = this.bind(TeamSearchService)
private readonly workspaceService = this.bind(WorkspaceService)
private readonly tabs = this.bind(RESTTabService)
constructor() {
super()
this.spotlight.registerSearcher(this)
}
createSearchSession(
query: Readonly<Ref<string>>
): [Ref<SpotlightSearcherSessionState>, () => void] {
const isTeamWorkspace = computed(
() => this.workspaceService.currentWorkspace.value.type === "team"
)
const scopeHandle = effectScope()
scopeHandle.run(() => {
const debouncedSearch = debounce(this.teamsSearch.searchTeams, 400)
watch(
query,
(query) => {
if (this.workspaceService.currentWorkspace.value.type === "team") {
const teamID = this.workspaceService.currentWorkspace.value.teamID
debouncedSearch(query, teamID)?.catch(() => {})
}
},
{
immediate: true,
}
)
// set the search section title based on the current workspace
const teamName = computed(() => {
return (
(this.workspaceService.currentWorkspace.value.type === "team" &&
this.workspaceService.currentWorkspace.value.teamName) ||
this.t("team.search_title")
)
})
watch(
teamName,
(newTeamName) => {
this.searcherSectionTitle = newTeamName
},
{
immediate: true,
}
)
})
const onSessionEnd = () => {
scopeHandle.stop()
}
const resultObj = computed<SpotlightSearcherSessionState>(() => {
return isTeamWorkspace.value
? {
loading: this.teamsSearch.teamsSearchResultsLoading.value,
results:
this.teamsSearch.teamsSearchResultsFormattedForSpotlight.value.map(
(result) => ({
id: result.request.id,
icon: markRaw(IconFolder),
score: 1, // make a better scoring system for this
text: {
type: "custom",
component: markRaw(RESTTeamRequestEntry),
componentProps: {
collectionTitles: result.collectionTitles,
request: result.request,
},
},
})
),
}
: {
loading: false,
results: [],
}
})
return [resultObj, onSessionEnd]
}
onResultSelect(result: SpotlightSearcherResult): void {
let inheritedProperties: HoppInheritedProperty | undefined = undefined
const selectedRequest = this.teamsSearch.searchResultsRequests[result.id]
if (!selectedRequest) return
const collectionID = result.id
if (!collectionID) return
inheritedProperties =
this.teamsSearch.cascadeParentCollectionForHeaderAuthForSearchResults(
collectionID
)
const possibleTab = this.tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: result.id,
})
if (possibleTab) {
this.tabs.setActiveTab(possibleTab.value.id)
} else {
this.tabs.createNewTab({
request: cloneDeep(selectedRequest.request as HoppRESTRequest),
isDirty: false,
saveContext: {
originLocation: "team-collection",
requestID: selectedRequest.id,
collectionID: selectedRequest.collectionID,
},
inheritedProperties: inheritedProperties,
})
}
}
}

View File

@@ -10,7 +10,6 @@
"dist/*" "dist/*"
], ],
"scripts": { "scripts": {
"dev": "vite build --watch",
"build:code": "vite build", "build:code": "vite build",
"build:decl": "tsc --project tsconfig.decl.json", "build:decl": "tsc --project tsconfig.decl.json",
"build": "pnpm run build:code && pnpm run build:decl", "build": "pnpm run build:code && pnpm run build:decl",
@@ -47,4 +46,4 @@
"verzod": "0.2.2", "verzod": "0.2.2",
"zod": "3.22.4" "zod": "3.22.4"
} }
} }

View File

@@ -8,7 +8,8 @@ import { translateToNewRequest } from "../rest"
import { translateToGQLRequest } from "../graphql" import { translateToGQLRequest } from "../graphql"
const versionedObject = z.object({ const versionedObject = z.object({
v: z.number(), // v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
}) })
export const HoppCollection = createVersionedEntity({ export const HoppCollection = createVersionedEntity({
@@ -25,7 +26,7 @@ export const HoppCollection = createVersionedEntity({
// For V1 we have to check the schema // For V1 we have to check the schema
const result = V1_VERSION.schema.safeParse(data) const result = V1_VERSION.schema.safeParse(data)
return result.success ? 1 : null return result.success ? 0 : null
}, },
}) })
@@ -51,6 +52,8 @@ export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
* @returns The proper new collection format * @returns The proper new collection format
*/ */
export function translateToNewRESTCollection(x: any): HoppCollection { export function translateToNewRESTCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy // Legacy
const name = x.name ?? "Untitled" const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewRESTCollection) const folders = (x.folders ?? []).map(translateToNewRESTCollection)
@@ -78,6 +81,8 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
* @returns The proper new collection format * @returns The proper new collection format
*/ */
export function translateToNewGQLCollection(x: any): HoppCollection { export function translateToNewGQLCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy // Legacy
const name = x.name ?? "Untitled" const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewGQLCollection) const folders = (x.folders ?? []).map(translateToNewGQLCollection)

View File

@@ -5,7 +5,7 @@ import { InferredEntity, createVersionedEntity } from "verzod"
import { z } from "zod" import { z } from "zod"
import V0_VERSION from "./v/0" import V0_VERSION from "./v/0"
import V1_VERSION, { uniqueID } from "./v/1" import V1_VERSION from "./v/1"
const versionedObject = z.object({ const versionedObject = z.object({
v: z.number(), v: z.number(),
@@ -165,7 +165,7 @@ export const translateToNewEnvironment = (x: any): Environment => {
if (x.v && x.v === EnvironmentSchemaVersion) return x if (x.v && x.v === EnvironmentSchemaVersion) return x
// Legacy // Legacy
const id = x.id || uniqueID() const id = x.id ?? ""
const name = x.name ?? "Untitled" const name = x.name ?? "Untitled"
const variables = (x.variables ?? []).map(translateToNewEnvironmentVariables) const variables = (x.variables ?? []).map(translateToNewEnvironmentVariables)

View File

@@ -2,8 +2,6 @@ import { z } from "zod"
import { defineVersion } from "verzod" import { defineVersion } from "verzod"
import { V0_SCHEMA } from "./0" import { V0_SCHEMA } from "./0"
export const uniqueID = () => Math.random().toString(36).substring(2, 16)
export const V1_SCHEMA = z.object({ export const V1_SCHEMA = z.object({
v: z.literal(1), v: z.literal(1),
id: z.string(), id: z.string(),
@@ -30,7 +28,7 @@ export default defineVersion({
const result: z.infer<typeof V1_SCHEMA> = { const result: z.infer<typeof V1_SCHEMA> = {
...old, ...old,
v: 1, v: 1,
id: old.id || uniqueID(), id: old.id ?? "",
variables: old.variables.map((variable) => { variables: old.variables.map((variable) => {
return { return {
...variable, ...variable,

View File

@@ -2,32 +2,29 @@ import { InferredEntity, createVersionedEntity } from "verzod"
import { z } from "zod" import { z } from "zod"
import V1_VERSION from "./v/1" import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2" import V2_VERSION from "./v/2"
import V3_VERSION from "./v/3"
export { GQLHeader } from "./v/1" export { GQLHeader } from "./v/1"
export { export {
HoppGQLAuth,
HoppGQLAuthAPIKey, HoppGQLAuthAPIKey,
HoppGQLAuthBasic, HoppGQLAuthBasic,
HoppGQLAuthBearer, HoppGQLAuthBearer,
HoppGQLAuthNone, HoppGQLAuthNone,
HoppGQLAuthOAuth2,
HoppGQLAuthInherit, HoppGQLAuthInherit,
} from "./v/2" } from "./v/2"
export { HoppGQLAuth } from "./v/3" export const GQL_REQ_SCHEMA_VERSION = 2
export { HoppGQLAuthOAuth2 } from "./v/3"
export const GQL_REQ_SCHEMA_VERSION = 3
const versionedObject = z.object({ const versionedObject = z.object({
v: z.number(), v: z.number(),
}) })
export const HoppGQLRequest = createVersionedEntity({ export const HoppGQLRequest = createVersionedEntity({
latestVersion: 3, latestVersion: 2,
versionMap: { versionMap: {
1: V1_VERSION, 1: V1_VERSION,
2: V2_VERSION, 2: V2_VERSION,
3: V3_VERSION,
}, },
getVersion(x) { getVersion(x) {
const result = versionedObject.safeParse(x) const result = versionedObject.safeParse(x)

View File

@@ -71,7 +71,7 @@ export const HoppGQLAuth = z
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth> export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
export const V2_SCHEMA = z.object({ const V2_SCHEMA = z.object({
id: z.optional(z.string()), id: z.optional(z.string()),
v: z.literal(2), v: z.literal(2),

View File

@@ -1,77 +0,0 @@
import { z } from "zod"
import { defineVersion } from "verzod"
import { HoppRESTAuthOAuth2 } from "../../rest"
import {
HoppGQLAuthAPIKey,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthInherit,
HoppGQLAuthNone,
V2_SCHEMA,
} from "./2"
export { HoppRESTAuthOAuth2 as HoppGQLAuthOAuth2 } from "../../rest"
export type HoppGqlAuthOAuth2 = z.infer<typeof HoppRESTAuthOAuth2>
export const HoppGQLAuth = z
.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthInherit,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthAPIKey,
HoppRESTAuthOAuth2, // both rest and gql have the same auth type for oauth2
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
export const V3_SCHEMA = V2_SCHEMA.extend({
v: z.literal(3),
auth: HoppGQLAuth,
})
export default defineVersion({
initial: false,
schema: V3_SCHEMA,
up(old: z.infer<typeof V2_SCHEMA>) {
if (old.auth.authType === "oauth-2") {
const { token, accessTokenURL, scope, clientID, authURL } = old.auth
return {
...old,
v: 3 as const,
auth: {
...old.auth,
authType: "oauth-2" as const,
grantTypeInfo: {
grantType: "AUTHORIZATION_CODE" as const,
authEndpoint: authURL,
tokenEndpoint: accessTokenURL,
clientID: clientID,
clientSecret: "",
scopes: scope,
isPKCE: false,
token,
},
addTo: "HEADERS" as const,
},
}
}
return {
...old,
v: 3 as const,
auth: {
...old.auth,
},
}
},
})

Some files were not shown because too many files have changed in this diff Show More