Compare commits
25 Commits
refactor/c
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
787aab650f | ||
|
|
1f7a8edb14 | ||
|
|
81f1e05a6c | ||
|
|
0a71783eaa | ||
|
|
c326f54f7e | ||
|
|
1113c79e20 | ||
|
|
6fd30f9aca | ||
|
|
2c5b0dcd1b | ||
|
|
6f4455ba03 | ||
|
|
ba8c4480d9 | ||
|
|
380397cc55 | ||
|
|
d19807b212 | ||
|
|
c89c2a5f5c | ||
|
|
256553b9bb | ||
|
|
89d9951f3b | ||
|
|
dd65ad3103 | ||
|
|
018ed3db26 | ||
|
|
a9cd6c0c01 | ||
|
|
e53382666a | ||
|
|
7621ff2961 | ||
|
|
fc20b76080 | ||
|
|
146c73d7b6 | ||
|
|
6b58915caa | ||
|
|
457857a711 | ||
|
|
a3f3e3e62d |
@@ -1,40 +1,14 @@
|
||||
# Docker Compose config used for internal test and QA deployments
|
||||
# This just spins up the AIO container along with an attached DB to the standard HTTP ports with subpath access mode
|
||||
|
||||
# TODO: Add Healthcheck for the AIO container
|
||||
# THIS IS NOT TO BE USED FOR PERSONAL DEPLOYMENTS!
|
||||
# Internal Docker Compose Image used for internal testing deployments
|
||||
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
# The service that spins up all 3 services at once in one container
|
||||
hoppscotch-aio:
|
||||
container_name: hoppscotch-aio
|
||||
restart: unless-stopped
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
context: .
|
||||
target: aio
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
|
||||
- ENABLE_SUBPATH_BASED_ACCESS=true
|
||||
depends_on:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3080:80"
|
||||
|
||||
# The preset DB service, you can delete/comment the below lines if
|
||||
# you are using an external postgres instance
|
||||
# This will be exposed at port 5432
|
||||
hoppscotch-db:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
user: postgres
|
||||
environment:
|
||||
# The default user defined by the docker image
|
||||
POSTGRES_USER: postgres
|
||||
# NOTE: Please UPDATE THIS PASSWORD!
|
||||
POSTGRES_PASSWORD: testpass
|
||||
POSTGRES_DB: hoppscotch
|
||||
healthcheck:
|
||||
@@ -46,3 +20,29 @@ services:
|
||||
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
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ curlCheck() {
|
||||
fi
|
||||
}
|
||||
|
||||
curlCheck "http://localhost:3000"
|
||||
curlCheck "http://localhost:3100"
|
||||
curlCheck "http://localhost:3170/ping"
|
||||
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
|
||||
curlCheck "http://localhost:80/backend/ping"
|
||||
else
|
||||
curlCheck "http://localhost:3000"
|
||||
curlCheck "http://localhost:3100"
|
||||
curlCheck "http://localhost:3170/ping"
|
||||
fi
|
||||
|
||||
@@ -84,6 +84,12 @@ export const USER_ALREADY_INVITED = 'admin/user_already_invited' as const;
|
||||
*/
|
||||
export const USER_UPDATE_FAILED = 'user/update_failed' as const;
|
||||
|
||||
/**
|
||||
* User display name validation failure
|
||||
* (UserService)
|
||||
*/
|
||||
export const USER_SHORT_DISPLAY_NAME = 'user/short_display_name' as const;
|
||||
|
||||
/**
|
||||
* User deletion failure
|
||||
* (UserService)
|
||||
@@ -750,3 +756,8 @@ export const DATABASE_TABLE_NOT_EXIST =
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||
|
||||
/**
|
||||
* Inputs supplied are invalid
|
||||
*/
|
||||
export const INVALID_PARAMS = 'invalid_parameters' as const;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { TeamCollectionService } from './team-collection.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||
@@ -7,6 +14,8 @@ import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorat
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
|
||||
import { throwHTTPErr } from 'src/utils';
|
||||
import { RESTError } from 'src/types/RESTError';
|
||||
import { INVALID_PARAMS } from 'src/errors';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'team-collection', version: '1' })
|
||||
@@ -26,8 +35,15 @@ export class TeamCollectionController {
|
||||
@Query('take') take: string,
|
||||
@Query('skip') skip: string,
|
||||
) {
|
||||
if (!teamID || !searchQuery) {
|
||||
return <RESTError>{
|
||||
message: INVALID_PARAMS,
|
||||
statusCode: HttpStatus.BAD_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.teamCollectionService.searchByTitle(
|
||||
searchQuery,
|
||||
searchQuery.trim(),
|
||||
teamID,
|
||||
parseInt(take),
|
||||
parseInt(skip),
|
||||
|
||||
@@ -58,6 +58,29 @@ export class UserResolver {
|
||||
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
||||
return updatedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => User, {
|
||||
description: 'Update a users display name',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async updateDisplayName(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'updatedDisplayName',
|
||||
description: 'New name of user',
|
||||
type: () => String,
|
||||
})
|
||||
updatedDisplayName: string,
|
||||
) {
|
||||
const updatedUser = await this.userService.updateUserDisplayName(
|
||||
user.uid,
|
||||
updatedDisplayName,
|
||||
);
|
||||
|
||||
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
|
||||
return updatedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete an user account',
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import {
|
||||
JSON_INVALID,
|
||||
USERS_NOT_FOUND,
|
||||
USER_NOT_FOUND,
|
||||
USER_SHORT_DISPLAY_NAME,
|
||||
} from 'src/errors';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
@@ -480,6 +485,14 @@ describe('UserService', () => {
|
||||
);
|
||||
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', () => {
|
||||
|
||||
@@ -8,7 +8,11 @@ import * as T from 'fp-ts/Task';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { pipe, constVoid } from 'fp-ts/function';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import {
|
||||
USERS_NOT_FOUND,
|
||||
USER_NOT_FOUND,
|
||||
USER_SHORT_DISPLAY_NAME,
|
||||
} from 'src/errors';
|
||||
import { SessionType, User } from './user.model';
|
||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
@@ -291,6 +295,10 @@ export class UserService {
|
||||
* @returns a Either of User or error
|
||||
*/
|
||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||
if (!displayName || displayName.length === 0) {
|
||||
return E.left(USER_SHORT_DISPLAY_NAME);
|
||||
}
|
||||
|
||||
try {
|
||||
const dbUpdatedUser = await this.prisma.user.update({
|
||||
where: { uid: userUID },
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("Test `hopp test <file>` command:", () => {
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("Supplied collection export file validations", () => {
|
||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||
@@ -66,6 +66,43 @@ describe("Test `hopp test <file>` command:", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Versioned entities", () => {
|
||||
describe("Collections & Requests", () => {
|
||||
const testFixtures = [
|
||||
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
|
||||
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
|
||||
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
|
||||
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
|
||||
];
|
||||
|
||||
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
|
||||
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
|
||||
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environments", () => {
|
||||
const testFixtures = [
|
||||
{ fileName: "env-v0.json", version: 0 },
|
||||
{ fileName: "env-v1.json", version: 1 },
|
||||
];
|
||||
|
||||
testFixtures.forEach(({ fileName, version }) => {
|
||||
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
|
||||
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
|
||||
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
@@ -75,7 +112,8 @@ describe("Test `hopp test <file>` command:", () => {
|
||||
|
||||
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"collection-level-headers-auth-coll.json", "collection"
|
||||
"collection-level-headers-auth-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
@@ -84,7 +122,8 @@ describe("Test `hopp test <file>` command:", () => {
|
||||
|
||||
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"pre-req-script-env-var-persistence-coll.json", "collection"
|
||||
"pre-req-script-env-var-persistence-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
@@ -106,7 +145,8 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
|
||||
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
|
||||
"notjson-coll.txt", "collection"
|
||||
"notjson-coll.txt",
|
||||
"collection"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -123,7 +163,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
});
|
||||
|
||||
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
|
||||
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
|
||||
const ENV_PATH = getTestJsonFilePath(
|
||||
"malformed-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -142,7 +185,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
});
|
||||
|
||||
test("Successfully resolves values from the supplied environment export file", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"env-flag-tests-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
|
||||
@@ -151,8 +197,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
});
|
||||
|
||||
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
@@ -160,7 +212,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
});
|
||||
|
||||
test("Works with shorth `-e` flag", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"env-flag-tests-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
|
||||
|
||||
@@ -183,7 +238,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
secretHeaderValue: "secret-header-value",
|
||||
};
|
||||
|
||||
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
@@ -197,8 +255,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
|
||||
// Prefers values specified in the environment export file over values set in the system environment
|
||||
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
||||
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-supplied-values-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error, stdout } = await runCLI(args);
|
||||
@@ -212,9 +276,13 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
// Values set from the scripting context takes the highest precedence
|
||||
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-persistence-coll.json", "collection"
|
||||
"secret-envs-persistence-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-supplied-values-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error, stdout } = await runCLI(args);
|
||||
@@ -227,10 +295,12 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
|
||||
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath(
|
||||
"secret-envs-persistence-scripting-coll.json", "collection"
|
||||
"secret-envs-persistence-scripting-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENVS_PATH = getTestJsonFilePath(
|
||||
"secret-envs-persistence-scripting-envs.json", "environment"
|
||||
"secret-envs-persistence-scripting-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { isRESTCollection } from "../../../utils/checks";
|
||||
|
||||
describe("isRESTCollection", () => {
|
||||
test("Undefined collection value.", () => {
|
||||
expect(isRESTCollection(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Invalid id value.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: 1,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Invalid requests value.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: "1",
|
||||
requests: null,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Invalid folders value.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: "1",
|
||||
requests: [],
|
||||
folders: undefined,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Invalid RESTCollection(s) in folders.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: "1",
|
||||
requests: [],
|
||||
folders: [
|
||||
{
|
||||
v: 1,
|
||||
name: "test1",
|
||||
id: "2",
|
||||
requests: undefined,
|
||||
folders: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Invalid HoppRESTRequest(s) in requests.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: "1",
|
||||
requests: [{}],
|
||||
folders: [],
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Valid RESTCollection.", () => {
|
||||
expect(
|
||||
isRESTCollection({
|
||||
v: 1,
|
||||
name: "test",
|
||||
id: "1",
|
||||
requests: [],
|
||||
folders: [],
|
||||
})
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
[
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "CollectionA",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderA",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderC",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestD",
|
||||
"params": [],
|
||||
@@ -53,7 +53,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestC",
|
||||
"params": [],
|
||||
@@ -90,7 +90,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
@@ -119,7 +119,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
@@ -153,16 +153,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "CollectionB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"name": "FolderA",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
@@ -191,7 +191,7 @@
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "<<URL>>",
|
||||
"name": "test1",
|
||||
"params": [],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "",
|
||||
"params": [],
|
||||
@@ -13,10 +13,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
||||
@@ -24,10 +21,10 @@
|
||||
"contentType": "application/json",
|
||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||
},
|
||||
"requestVariables": [],
|
||||
"requestVariables": []
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -35,10 +32,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests":
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "fail",
|
||||
"params": [],
|
||||
@@ -12,10 +12,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
|
||||
@@ -26,7 +23,7 @@
|
||||
"requestVariables": [],
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -34,10 +31,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||
"name": "",
|
||||
"params": [],
|
||||
@@ -13,10 +13,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||
@@ -27,7 +24,7 @@
|
||||
"requestVariables": []
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||
"name": "success",
|
||||
"params": [],
|
||||
@@ -35,10 +32,7 @@
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "",
|
||||
"value": ""
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": { "body": null, "contentType": null },
|
||||
"name": "sample-req",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"name": "test-request",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"method": "POST",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"v": 1,
|
||||
"name": "tests",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"endpoint": "<<baseURL>>",
|
||||
"name": "",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": { "body": null, "contentType": null },
|
||||
"name": "test-secret-headers",
|
||||
@@ -23,7 +23,7 @@
|
||||
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": {
|
||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||
@@ -39,7 +39,7 @@
|
||||
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": { "body": null, "contentType": null },
|
||||
"name": "test-secret-query-params",
|
||||
@@ -58,7 +58,7 @@
|
||||
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "basic",
|
||||
"password": "<<secretBasicAuthPassword>>",
|
||||
@@ -76,7 +76,7 @@
|
||||
"preRequestScript": ""
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"token": "<<secretBearerToken>>",
|
||||
"authType": "bearer",
|
||||
@@ -95,7 +95,7 @@
|
||||
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"body": { "body": null, "contentType": null },
|
||||
"name": "test-secret-fallback",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"authType": "basic",
|
||||
"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}"
|
||||
},
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"auth": {
|
||||
"token": "<<secretBearerToken>>",
|
||||
"authType": "bearer",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "2",
|
||||
"v": "3",
|
||||
"endpoint": "https://httpbin.org/post",
|
||||
"name": "req",
|
||||
"params": [],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "env-v0",
|
||||
"variables": [
|
||||
{
|
||||
"key": "baseURL",
|
||||
"value": "https://echo.hoppscotch.io"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "env-v0",
|
||||
"variables": [
|
||||
{
|
||||
"key": "baseURL",
|
||||
"value": "https://echo.hoppscotch.io",
|
||||
"secret": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { error } from "../../types/errors";
|
||||
import {
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvPair,
|
||||
HoppEnvs
|
||||
HoppEnvs,
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { readJsonFile } from "../../utils/mutators";
|
||||
*/
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
||||
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
|
||||
|
||||
// The legacy key-value pair format that is still supported
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
@@ -26,7 +26,9 @@ export async function parseEnvsData(path: string) {
|
||||
const HoppEnvExportObjectResult = Environment.safeParse(contents);
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app
|
||||
const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
|
||||
const HoppBulkEnvExportObjectResult = z
|
||||
.array(entityReference(Environment))
|
||||
.safeParse(contents);
|
||||
|
||||
// CLI doesnt support bulk environments export
|
||||
// Hence we check for this case and throw an error if it matches the format
|
||||
@@ -36,13 +38,16 @@ export async function parseEnvsData(path: string) {
|
||||
|
||||
// Checks if the environment file is of the correct format
|
||||
// If it doesnt match either of them, we throw an error
|
||||
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
||||
if (
|
||||
!HoppEnvKeyPairResult.success &&
|
||||
HoppEnvExportObjectResult.type === "err"
|
||||
) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
envPairs.push({ key, value, secret: false });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { CommanderError } from "commander";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
|
||||
@@ -14,48 +12,6 @@ export const hasProperty = <P extends PropertyKey>(
|
||||
prop: P
|
||||
): target is Record<P, unknown> => prop in target;
|
||||
|
||||
/**
|
||||
* Typeguard to check valid Hoppscotch REST Collection.
|
||||
* @param param The object to be checked.
|
||||
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
|
||||
* False, otherwise.
|
||||
*/
|
||||
export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
||||
if (!!param && typeof param === "object") {
|
||||
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (!hasProperty(param, "name") || typeof param.name !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (hasProperty(param, "id") && typeof param.id !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!hasProperty(param, "requests") || !Array.isArray(param.requests)) {
|
||||
return false;
|
||||
} else {
|
||||
// Checks each requests array to be valid HoppRESTRequest.
|
||||
const checkRequests = A.every(isHoppRESTRequest)(param.requests);
|
||||
if (!checkRequests) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!hasProperty(param, "folders") || !Array.isArray(param.folders)) {
|
||||
return false;
|
||||
} else {
|
||||
// Checks each folder to be valid REST collection.
|
||||
const checkFolders = A.every(isRESTCollection)(param.folders);
|
||||
if (!checkFolders) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if given error data is of type HoppCLIError, based on existence
|
||||
* of code property.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import fs from "fs/promises";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { error } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection } from "@hoppscotch/data";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { isHoppErrnoException } from "./checks";
|
||||
|
||||
/**
|
||||
* Parses array of FormDataEntry to FormData.
|
||||
@@ -67,7 +70,11 @@ export async function parseCollectionData(
|
||||
? contents
|
||||
: [contents];
|
||||
|
||||
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
const collectionSchemaParsedResult = z
|
||||
.array(entityReference(HoppCollection))
|
||||
.safeParse(maybeArrayOfCollections);
|
||||
|
||||
if (!collectionSchemaParsedResult.success) {
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
@@ -75,5 +82,22 @@ export async function parseCollectionData(
|
||||
});
|
||||
}
|
||||
|
||||
return maybeArrayOfCollections as HoppCollection[];
|
||||
return collectionSchemaParsedResult.data.map((collection) => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,18 +109,31 @@ export function getEffectiveRESTRequest(
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
});
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
) {
|
||||
} else if (request.auth.authType === "bearer") {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(
|
||||
request.auth.token,
|
||||
envVariables
|
||||
)}`,
|
||||
value: `Bearer ${parseTemplateString(request.auth.token, envVariables)}`,
|
||||
});
|
||||
} else if (request.auth.authType === "oauth-2") {
|
||||
const { addTo } = request.auth;
|
||||
|
||||
if (addTo === "HEADERS") {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, envVariables)}`,
|
||||
});
|
||||
} else if (addTo === "QUERY_PARAMS") {
|
||||
effectiveFinalParams.push({
|
||||
active: true,
|
||||
key: "access_token",
|
||||
value: parseTemplateString(
|
||||
request.auth.grantTypeInfo.token,
|
||||
envVariables
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
const { key, value, addTo } = request.auth;
|
||||
if (addTo === "Headers") {
|
||||
|
||||
@@ -41,10 +41,10 @@ const processVariables = (variable: Environment["variables"][number]) => {
|
||||
...variable,
|
||||
value:
|
||||
"value" in variable ? variable.value : process.env[variable.key] || "",
|
||||
}
|
||||
};
|
||||
}
|
||||
return variable
|
||||
}
|
||||
return variable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes given envs, which includes processing each variable in global
|
||||
@@ -56,10 +56,10 @@ const processEnvs = (envs: HoppEnvs) => {
|
||||
const processedEnvs = {
|
||||
global: envs.global.map(processVariables),
|
||||
selected: envs.selected.map(processVariables),
|
||||
}
|
||||
};
|
||||
|
||||
return processedEnvs
|
||||
}
|
||||
return processedEnvs;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL
|
||||
displayUrl: req.effectiveFinalDisplayURL,
|
||||
};
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
const reqParams = finalParams(req);
|
||||
@@ -131,6 +131,7 @@ export const requestRunner =
|
||||
let status: number;
|
||||
const baseResponse = await axios(requestConfig);
|
||||
const { config } = baseResponse;
|
||||
// PR-COMMENT: type error
|
||||
const runnerResponse: RequestRunnerResponse = {
|
||||
...baseResponse,
|
||||
endpoint: getRequest.endpoint(config.url),
|
||||
@@ -257,10 +258,13 @@ export const processRequest =
|
||||
let updatedEnvs = <HoppEnvs>{};
|
||||
|
||||
// Fetch values for secret environment variables from system environment
|
||||
const processedEnvs = processEnvs(envs)
|
||||
const processedEnvs = processEnvs(envs);
|
||||
|
||||
// Executing pre-request-script
|
||||
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
|
||||
const preRequestRes = await preRequestScriptRunner(
|
||||
request,
|
||||
processedEnvs
|
||||
)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
||||
@@ -347,7 +351,7 @@ export const processRequest =
|
||||
*/
|
||||
export const preProcessRequest = (
|
||||
request: HoppRESTRequest,
|
||||
collection: HoppCollection,
|
||||
collection: HoppCollection
|
||||
): HoppRESTRequest => {
|
||||
const tempRequest = Object.assign({}, request);
|
||||
const { headers: parentHeaders, auth: parentAuth } = collection;
|
||||
@@ -372,8 +376,10 @@ export const preProcessRequest = (
|
||||
// Filter out header entries present in the parent (folder/collection) under the same name
|
||||
// This ensures the child headers take precedence over the parent headers
|
||||
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
|
||||
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
|
||||
})
|
||||
return !tempRequest.headers.some(
|
||||
(reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key
|
||||
);
|
||||
});
|
||||
tempRequest.headers.push(...filteredEntries);
|
||||
} else if (!tempRequest.headers) {
|
||||
tempRequest.headers = [];
|
||||
|
||||
@@ -10,6 +10,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
ecmaFeatures: {
|
||||
jsx: false,
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
"@vue/typescript/recommended",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"hide_secret": "Hide secret",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"download_here": "Download here",
|
||||
"less": "Less",
|
||||
"more": "More",
|
||||
"new": "New",
|
||||
@@ -103,8 +104,10 @@
|
||||
"auth": {
|
||||
"account_exists": "Account exists with different credential - Login to link both accounts",
|
||||
"all_sign_in_options": "All sign in options",
|
||||
"continue_with_auth_provider": "Continue with {provider}",
|
||||
"continue_with_email": "Continue with Email",
|
||||
"continue_with_github": "Continue with GitHub",
|
||||
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
|
||||
"continue_with_google": "Continue with Google",
|
||||
"continue_with_microsoft": "Continue with Microsoft",
|
||||
"email": "Email",
|
||||
@@ -137,7 +140,26 @@
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
|
||||
"grant_type": "Grant Type",
|
||||
"grant_type_auth_code": "Authorization Code",
|
||||
"token_fetched_successfully": "Token fetched successfully",
|
||||
"token_fetch_failed": "Failed to fetch token",
|
||||
"validation_failed": "Validation Failed, please check the form fields",
|
||||
"label_authorization_endpoint": "Authorization Endpoint",
|
||||
"label_client_id": "Client ID",
|
||||
"label_client_secret": "Client Secret",
|
||||
"label_code_challenge": "Code Challenge",
|
||||
"label_code_challenge_method": "Code Challenge Method",
|
||||
"label_code_verifier": "Code Verifier",
|
||||
"label_scopes": "Scopes",
|
||||
"label_token_endpoint": "Token Endpoint",
|
||||
"label_use_pkce": "Use PKCE",
|
||||
"label_implicit": "Implicit",
|
||||
"label_password": "Password",
|
||||
"label_username": "Username",
|
||||
"label_auth_code": "Authorization Code",
|
||||
"label_client_credentials": "Client Credentials"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Password",
|
||||
@@ -281,7 +303,7 @@
|
||||
"updated": "Environment updated",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"variables":"Variables",
|
||||
"variables": "Variables",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
@@ -293,6 +315,7 @@
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these workspaces:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
|
||||
"empty_profile_name": "Profile name cannot be empty",
|
||||
"empty_req_name": "Empty Request Name",
|
||||
"f12_details": "(F12 for details)",
|
||||
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
||||
@@ -312,6 +335,7 @@
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"same_profile_name": "Updated profile name is same as the current profile name",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script",
|
||||
@@ -427,7 +451,7 @@
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set Cookie Headers. Please use Authorization Headers instead. However, our Hoppscotch Desktop App is live now and supports Cookies."
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
@@ -961,7 +985,8 @@
|
||||
"success_invites": "Success invites",
|
||||
"title": "Workspaces",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the 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": {
|
||||
"deleted": "Environment Deleted",
|
||||
|
||||
@@ -127,8 +127,8 @@
|
||||
"@types/splitpanes": "2.2.6",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/yargs-parser": "21.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.13.2",
|
||||
"@typescript-eslint/parser": "6.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "7.3.1",
|
||||
"@typescript-eslint/parser": "7.3.1",
|
||||
"@vitejs/plugin-vue": "4.5.1",
|
||||
"@vue/compiler-sfc": "3.3.10",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
@@ -136,9 +136,9 @@
|
||||
"autoprefixer": "10.4.16",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.3.1",
|
||||
"eslint": "8.55.0",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"eslint-plugin-vue": "9.19.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-vue": "9.24.0",
|
||||
"glob": "10.3.10",
|
||||
"npm-run-all": "4.1.5",
|
||||
"openapi-types": "12.1.3",
|
||||
@@ -164,4 +164,4 @@
|
||||
"vitest": "0.34.6",
|
||||
"vue-tsc": "1.8.24"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import "@vue/runtime-core"
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
@@ -111,6 +111,7 @@ import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/
|
||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
||||
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
||||
import { TeamsSpotlightSearcherService } from "~/services/spotlight/searchers/teamRequest.searcher"
|
||||
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
||||
import {
|
||||
SwitchWorkspaceSpotlightSearcherService,
|
||||
@@ -144,6 +145,7 @@ useService(SwitchEnvSpotlightSearcherService)
|
||||
useService(WorkspaceSpotlightSearcherService)
|
||||
useService(SwitchWorkspaceSpotlightSearcherService)
|
||||
useService(InterceptorSpotlightSearcherService)
|
||||
useService(TeamsSpotlightSearcherService)
|
||||
|
||||
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
|
||||
useService(searcher)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
v-model="activeTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
|
||||
render-inactive-tabs
|
||||
>
|
||||
@@ -16,7 +16,6 @@
|
||||
<HttpHeaders
|
||||
v-model="editableCollection"
|
||||
:is-collection-property="true"
|
||||
@change-tab="changeOptionTab"
|
||||
/>
|
||||
<div
|
||||
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
|
||||
@@ -34,6 +33,7 @@
|
||||
:is-collection-property="true"
|
||||
:is-root-collection="editingProperties?.isRootCollection"
|
||||
:inherited-properties="editingProperties?.inheritedProperties"
|
||||
:source="source"
|
||||
/>
|
||||
<div
|
||||
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
|
||||
@@ -64,27 +64,42 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppCollection, HoppRESTAuth, HoppRESTHeaders } from "@hoppscotch/data"
|
||||
import { RESTOptionTabs } from "../http/RequestOptions.vue"
|
||||
import {
|
||||
GQLHeader,
|
||||
HoppCollection,
|
||||
HoppGQLAuth,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeaders,
|
||||
} from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { clone } from "lodash-es"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { PersistenceService } from "~/services/persistence"
|
||||
|
||||
const persistenceService = useService(PersistenceService)
|
||||
const t = useI18n()
|
||||
|
||||
type EditingProperties = {
|
||||
export type EditingProperties = {
|
||||
collection: Partial<HoppCollection> | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}
|
||||
|
||||
type HoppCollectionAuth = HoppRESTAuth | HoppGQLAuth
|
||||
type HoppCollectionHeaders = HoppRESTHeaders | GQLHeader[]
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
loadingState: boolean
|
||||
editingProperties: EditingProperties | null
|
||||
source: "REST" | "GraphQL"
|
||||
modelValue: string
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
@@ -99,11 +114,12 @@ const emit = defineEmits<{
|
||||
newCollection: Omit<EditingProperties, "inheritedProperties">
|
||||
): void
|
||||
(e: "hide-modal"): void
|
||||
(e: "update:modelValue"): void
|
||||
}>()
|
||||
|
||||
const editableCollection = ref<{
|
||||
headers: HoppRESTHeaders
|
||||
auth: HoppRESTAuth
|
||||
headers: HoppCollectionHeaders
|
||||
auth: HoppCollectionAuth
|
||||
}>({
|
||||
headers: [],
|
||||
auth: {
|
||||
@@ -112,21 +128,38 @@ const editableCollection = ref<{
|
||||
},
|
||||
})
|
||||
|
||||
const selectedOptionTab = ref("headers")
|
||||
watch(
|
||||
editableCollection,
|
||||
(updatedEditableCollection) => {
|
||||
if (props.show && props.editingProperties) {
|
||||
const unsavedCollectionProperties: EditingProperties = {
|
||||
collection: updatedEditableCollection,
|
||||
isRootCollection: props.editingProperties?.isRootCollection ?? false,
|
||||
path: props.editingProperties?.path,
|
||||
inheritedProperties: props.editingProperties?.inheritedProperties,
|
||||
}
|
||||
persistenceService.setLocalConfig(
|
||||
"unsaved_collection_properties",
|
||||
JSON.stringify(unsavedCollectionProperties)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
)
|
||||
|
||||
const changeOptionTab = (tab: RESTOptionTabs) => {
|
||||
selectedOptionTab.value = tab
|
||||
}
|
||||
const activeTab = useVModel(props, "modelValue", emit)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show && props.editingProperties?.collection) {
|
||||
editableCollection.value.auth = clone(
|
||||
props.editingProperties.collection.auth as HoppRESTAuth
|
||||
props.editingProperties.collection.auth as HoppCollectionAuth
|
||||
)
|
||||
editableCollection.value.headers = clone(
|
||||
props.editingProperties.collection.headers as HoppRESTHeaders
|
||||
props.editingProperties.collection.headers as HoppCollectionHeaders
|
||||
)
|
||||
} else {
|
||||
editableCollection.value = {
|
||||
@@ -136,6 +169,8 @@ watch(
|
||||
authActive: false,
|
||||
},
|
||||
}
|
||||
|
||||
persistenceService.removeLocalConfig("unsaved_collection_properties")
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -152,9 +187,11 @@ const saveEditedCollection = () => {
|
||||
isRootCollection: props.editingProperties.isRootCollection,
|
||||
}
|
||||
emit("set-collection-properties", collection as EditingProperties)
|
||||
persistenceService.removeLocalConfig("unsaved_collection_properties")
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
persistenceService.removeLocalConfig("unsaved_collection_properties")
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-if="hasNoTeamAccess || isShowingSearchResults"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
class="!rounded-none"
|
||||
@@ -36,8 +36,9 @@
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="
|
||||
collectionsType.type === 'team-collections' &&
|
||||
collectionsType.selectedTeam === undefined
|
||||
(collectionsType.type === 'team-collections' &&
|
||||
collectionsType.selectedTeam === undefined) ||
|
||||
isShowingSearchResults
|
||||
"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
@@ -58,7 +59,7 @@
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
@@ -128,6 +129,14 @@
|
||||
})
|
||||
}
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
handleCollectionClick({
|
||||
collectionID: node.id,
|
||||
isOpen,
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'folders'"
|
||||
@@ -137,7 +146,7 @@
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
@@ -209,6 +218,15 @@
|
||||
})
|
||||
}
|
||||
"
|
||||
@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
|
||||
v-if="node.data.type === 'requests'"
|
||||
@@ -218,7 +236,7 @@
|
||||
:collections-type="collectionsType.type"
|
||||
:duplicate-loading="duplicateLoading"
|
||||
:is-active="isActiveRequest(node.data.data.data.id)"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||
:request-move-loading="requestMoveLoading"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:is-selected="
|
||||
@@ -283,7 +301,15 @@
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="node === null"
|
||||
v-if="filterText.length !== 0 && teamCollectionList.length === 0"
|
||||
: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`"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
@@ -394,6 +420,11 @@ const props = defineProps({
|
||||
default: () => ({ type: "my-collections", selectedTeam: undefined }),
|
||||
required: true,
|
||||
},
|
||||
filterText: {
|
||||
type: String as PropType<string>,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
teamCollectionList: {
|
||||
type: Array as PropType<TeamCollection[]>,
|
||||
default: () => [],
|
||||
@@ -436,6 +467,8 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const isShowingSearchResults = computed(() => props.filterText.length > 0)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
event: "add-request",
|
||||
@@ -543,6 +576,14 @@ const emit = defineEmits<{
|
||||
}
|
||||
}
|
||||
): 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: "expand-team-collection", payload: string): void
|
||||
(event: "display-modal-add"): void
|
||||
@@ -555,6 +596,18 @@ const getPath = (path: string) => {
|
||||
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 hasNoTeamAccess = computed(
|
||||
|
||||
@@ -146,8 +146,10 @@
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
<CollectionsProperties
|
||||
v-model="collectionPropertiesModalActiveTab"
|
||||
:show="showModalEditProperties"
|
||||
:editing-properties="editingProperties"
|
||||
source="GraphQL"
|
||||
@hide-modal="displayModalEditProperties(false)"
|
||||
@set-collection-properties="setCollectionProperties"
|
||||
/>
|
||||
@@ -155,7 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from "vue"
|
||||
import { nextTick, onMounted, ref } from "vue"
|
||||
import { clone, cloneDeep } from "lodash-es"
|
||||
import {
|
||||
graphqlCollections$,
|
||||
@@ -186,6 +188,10 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
|
||||
import { useToast } from "~/composables/toast"
|
||||
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 toast = useToast()
|
||||
@@ -219,7 +225,7 @@ const editingRequest = ref<HoppGQLRequest | null>(null)
|
||||
const editingRequestIndex = ref<number | null>(null)
|
||||
|
||||
const editingProperties = ref<{
|
||||
collection: HoppCollection | null
|
||||
collection: Partial<HoppCollection> | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
@@ -232,6 +238,53 @@ const editingProperties = 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 = 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 collectionsClone = clone(collections.value)
|
||||
|
||||
@@ -557,7 +610,7 @@ const editProperties = ({
|
||||
if (collectionIndex === null || collection === null) return
|
||||
|
||||
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||
let inheritedProperties = {}
|
||||
let inheritedProperties = undefined
|
||||
|
||||
if (parentIndex) {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
@@ -568,7 +621,7 @@ const editProperties = ({
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
} as HoppInheritedProperty
|
||||
}
|
||||
}
|
||||
|
||||
editingProperties.value = {
|
||||
@@ -582,11 +635,15 @@ const editProperties = ({
|
||||
}
|
||||
|
||||
const setCollectionProperties = (newCollection: {
|
||||
collection: HoppCollection
|
||||
collection: Partial<HoppCollection> | null
|
||||
path: string
|
||||
isRootCollection: boolean
|
||||
}) => {
|
||||
const { collection, path, isRootCollection } = newCollection
|
||||
if (!collection) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isRootCollection) {
|
||||
editGraphqlCollection(parseInt(path), collection)
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
autocomplete="off"
|
||||
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||
:placeholder="t('action.search')"
|
||||
:disabled="collectionsType.type === 'team-collections'"
|
||||
/>
|
||||
</div>
|
||||
<CollectionsMyCollections
|
||||
@@ -58,8 +57,15 @@
|
||||
<CollectionsTeamCollections
|
||||
v-else
|
||||
:collections-type="collectionsType"
|
||||
:team-collection-list="teamCollectionList"
|
||||
:team-loading-collections="teamLoadingCollections"
|
||||
:team-collection-list="
|
||||
filterTexts.length > 0 ? teamsSearchResults : teamCollectionList
|
||||
"
|
||||
:team-loading-collections="
|
||||
filterTexts.length > 0
|
||||
? collectionsBeingLoadedFromSearch
|
||||
: teamLoadingCollections
|
||||
"
|
||||
:filter-text="filterTexts"
|
||||
:export-loading="exportLoading"
|
||||
:duplicate-loading="duplicateLoading"
|
||||
:save-request="saveRequest"
|
||||
@@ -87,6 +93,7 @@
|
||||
@expand-team-collection="expandTeamCollection"
|
||||
@display-modal-add="displayModalAdd(true)"
|
||||
@display-modal-import-export="displayModalImportExport(true)"
|
||||
@collection-click="handleCollectionClick"
|
||||
/>
|
||||
<div
|
||||
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
|
||||
@@ -154,8 +161,10 @@
|
||||
@hide-modal="displayTeamModalAdd(false)"
|
||||
/>
|
||||
<CollectionsProperties
|
||||
v-model="collectionPropertiesModalActiveTab"
|
||||
:show="showModalEditProperties"
|
||||
:editing-properties="editingProperties"
|
||||
source="REST"
|
||||
@hide-modal="displayModalEditProperties(false)"
|
||||
@set-collection-properties="setCollectionProperties"
|
||||
/>
|
||||
@@ -163,7 +172,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, PropType, ref, watch } from "vue"
|
||||
import { computed, nextTick, onMounted, PropType, ref, watch } from "vue"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
@@ -199,7 +208,7 @@ import {
|
||||
HoppRESTRequest,
|
||||
makeCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { cloneDeep, isEqual } from "lodash-es"
|
||||
import { cloneDeep, debounce, isEqual } from "lodash-es"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
createNewRootCollection,
|
||||
@@ -240,6 +249,11 @@ import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
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 toast = useToast()
|
||||
@@ -291,12 +305,7 @@ const editingRequestName = ref("")
|
||||
const editingRequestIndex = ref<number | null>(null)
|
||||
const editingRequestID = ref<string | null>(null)
|
||||
|
||||
const editingProperties = ref<{
|
||||
collection: Partial<HoppCollection> | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>({
|
||||
const editingProperties = ref<EditingProperties>({
|
||||
collection: null,
|
||||
isRootCollection: false,
|
||||
path: "",
|
||||
@@ -336,6 +345,96 @@ 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 = 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
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
@@ -364,7 +463,28 @@ const switchToMyCollections = () => {
|
||||
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) => {
|
||||
if (filterTexts.value.length > 0 && teamsSearchResults.value) {
|
||||
return
|
||||
}
|
||||
|
||||
teamCollectionAdapter.expandCollection(collectionID)
|
||||
}
|
||||
|
||||
@@ -1330,13 +1450,25 @@ const selectRequest = (selectedRequest: {
|
||||
let possibleTab = null
|
||||
|
||||
if (collectionsType.value.type === "team-collections") {
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
|
||||
let inheritedProperties: HoppInheritedProperty | undefined = undefined
|
||||
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
if (filterTexts.value.length > 0) {
|
||||
const collectionID = folderPath.split("/").at(-1)
|
||||
|
||||
if (!collectionID) return
|
||||
|
||||
inheritedProperties =
|
||||
cascadeParentCollectionForHeaderAuthForSearchResults(collectionID)
|
||||
} else {
|
||||
inheritedProperties =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
|
||||
}
|
||||
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
})
|
||||
|
||||
if (possibleTab) {
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
} else {
|
||||
@@ -1348,10 +1480,7 @@ const selectRequest = (selectedRequest: {
|
||||
requestID: requestIndex,
|
||||
collectionID: folderPath,
|
||||
},
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
inheritedProperties: inheritedProperties,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
<hr v-if="additionalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
v-for="loginItem in additionalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@@ -170,7 +170,7 @@ type AuthProviderItem = {
|
||||
}
|
||||
|
||||
let allowedAuthProviders: AuthProviderItem[] = []
|
||||
let additonalLoginItems: LoginItemDef[] = []
|
||||
const additionalLoginItems: LoginItemDef[] = []
|
||||
|
||||
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
|
||||
await item.onClick()
|
||||
@@ -199,10 +199,33 @@ onMounted(async () => {
|
||||
allowedAuthProviders = enabledAuthProviders
|
||||
|
||||
// setup the additional login items
|
||||
additonalLoginItems =
|
||||
platform.auth.additionalLoginItems?.filter((item) =>
|
||||
res.right.includes(item.id)
|
||||
) ?? []
|
||||
platform.auth.additionalLoginItems?.forEach((item) => {
|
||||
if (res.right.includes(item.id)) {
|
||||
additionalLoginItems.push(item)
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
@@ -311,6 +334,14 @@ const authProvidersAvailable: AuthProviderItem[] = [
|
||||
action: signInWithGithub,
|
||||
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",
|
||||
icon: IconGoogle,
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'oauth-2'
|
||||
selectOAuth2AuthType()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -189,12 +189,12 @@
|
||||
<div v-if="auth.authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.token"
|
||||
v-model="auth.grantTypeInfo.token"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization v-model="auth" />
|
||||
<HttpOAuth2Authorization v-model="auth" source="GraphQL" />
|
||||
</div>
|
||||
<div v-if="auth.authType === 'api-key'">
|
||||
<HttpAuthorizationApiKey v-model="auth" />
|
||||
@@ -220,19 +220,22 @@
|
||||
</template>
|
||||
|
||||
<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 IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
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"
|
||||
|
||||
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -280,6 +283,30 @@ const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
|
||||
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 = {
|
||||
...auth.value,
|
||||
authType: "oauth-2",
|
||||
addTo: "HEADERS",
|
||||
grantTypeInfo: grantTypeInfo,
|
||||
}
|
||||
}
|
||||
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
|
||||
const clearContent = () => {
|
||||
|
||||
@@ -579,12 +579,18 @@ const getComputedAuthHeaders = (
|
||||
})
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS")
|
||||
) {
|
||||
const requestAuth = request.auth
|
||||
|
||||
const isOAuth2 = requestAuth.authType === "oauth-2"
|
||||
|
||||
const token = isOAuth2 ? requestAuth.grantTypeInfo.token : requestAuth.token
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${request.auth.token}`,
|
||||
value: `Bearer ${token}`,
|
||||
})
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
const { key, addTo } = request.auth
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'oauth-2'
|
||||
selectOAuth2AuthType()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -177,15 +177,24 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'oauth-2'">
|
||||
<div v-if="auth.authType === 'oauth-2'" class="w-full">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<!-- Ensure a new object is assigned here to avoid reactivity issues -->
|
||||
<SmartEnvInput
|
||||
v-model="auth.token"
|
||||
:model-value="auth.grantTypeInfo.token"
|
||||
placeholder="Token"
|
||||
:envs="envs"
|
||||
@update:model-value="
|
||||
auth.grantTypeInfo = { ...auth.grantTypeInfo, token: $event }
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
|
||||
<HttpOAuth2Authorization
|
||||
v-model="auth"
|
||||
:is-collection-property="isCollectionProperty"
|
||||
:envs="envs"
|
||||
:source="source"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'api-key'">
|
||||
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
|
||||
@@ -217,7 +226,7 @@ import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppRESTAuth } from "@hoppscotch/data"
|
||||
import { HoppRESTAuth, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
@@ -226,17 +235,27 @@ import { onMounted } from "vue"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { AggregateEnvironment } from "~/newstore/environments"
|
||||
|
||||
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppRESTAuth
|
||||
isCollectionProperty?: boolean
|
||||
isRootCollection?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
envs?: AggregateEnvironment[]
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppRESTAuth
|
||||
isCollectionProperty?: boolean
|
||||
isRootCollection?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
envs?: AggregateEnvironment[]
|
||||
source?: "REST" | "GraphQL"
|
||||
}>(),
|
||||
{
|
||||
source: "REST",
|
||||
envs: undefined,
|
||||
inheritedProperties: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTAuth): void
|
||||
@@ -272,6 +291,30 @@ const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
|
||||
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 = {
|
||||
...auth.value,
|
||||
authType: "oauth-2",
|
||||
addTo: "HEADERS",
|
||||
grantTypeInfo: grantTypeInfo,
|
||||
}
|
||||
}
|
||||
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
|
||||
const clearContent = () => {
|
||||
|
||||
@@ -98,6 +98,7 @@ import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -124,6 +125,7 @@ useCodemirror(
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
onInit: (view: EditorView) => view.focus(),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -273,6 +273,10 @@ const loading = computed(
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
if (adapter.isInitialized()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 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
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="p-4">
|
||||
<div class="p-4 truncate">
|
||||
<label
|
||||
class="font-semibold text-secondaryDark"
|
||||
:class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }"
|
||||
@@ -131,6 +131,7 @@
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
:loading-state="loading"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam()"
|
||||
/>
|
||||
@@ -161,6 +162,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconUserX from "~icons/lucide/user-x"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -173,6 +176,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(e: "edit-team"): void
|
||||
(e: "invite-team"): void
|
||||
(e: "refetch-teams"): void
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
@@ -180,7 +184,12 @@ const toast = useToast()
|
||||
const confirmRemove = ref(false)
|
||||
const confirmExit = ref(false)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
|
||||
const deleteTeam = () => {
|
||||
loading.value = true
|
||||
pipe(
|
||||
backendDeleteTeam(props.teamID),
|
||||
TE.match(
|
||||
@@ -188,9 +197,25 @@ const deleteTeam = () => {
|
||||
// TODO: Better errors ? We know the possible errors now
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(err)
|
||||
loading.value = false
|
||||
confirmRemove.value = false
|
||||
},
|
||||
() => {
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('team.create_new')}`"
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center">
|
||||
@@ -16,13 +17,6 @@
|
||||
:alt="`${t('empty.teams')}`"
|
||||
:text="`${t('empty.teams')}`"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('team.create_new')}`"
|
||||
filled
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-else-if="!loading"
|
||||
@@ -39,6 +33,7 @@
|
||||
:compact="modal"
|
||||
@edit-team="editTeam(team, team.id)"
|
||||
@invite-team="inviteTeam(team, team.id)"
|
||||
@refetch-teams="refetchTeams"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!loading && adapterError" class="flex flex-col items-center">
|
||||
@@ -76,6 +71,7 @@ import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
|
||||
@@ -68,6 +68,9 @@ type CodeMirrorOptions = {
|
||||
|
||||
// callback on editor update
|
||||
onUpdate?: (view: ViewUpdate) => void
|
||||
|
||||
// callback on view initialization
|
||||
onInit?: (view: EditorView) => void
|
||||
}
|
||||
|
||||
const hoppCompleterExt = (completer: Completer): Extension => {
|
||||
@@ -208,7 +211,9 @@ export function useCodemirror(
|
||||
el: Ref<any | null>,
|
||||
value: Ref<string | undefined>,
|
||||
options: CodeMirrorOptions
|
||||
): { cursor: Ref<{ line: number; ch: number }> } {
|
||||
): {
|
||||
cursor: Ref<{ line: number; ch: number }>
|
||||
} {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
// Set default value for contextMenuEnabled if not provided
|
||||
@@ -383,6 +388,8 @@ export function useCodemirror(
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
|
||||
options.onInit?.(view.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { customRef, onBeforeUnmount, Ref, watch } from "vue"
|
||||
import { customRef, onBeforeUnmount, ref, Ref, UnwrapRef, watch } from "vue"
|
||||
|
||||
export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
|
||||
return customRef((track, trigger) => {
|
||||
@@ -31,3 +31,16 @@ export function pluckMultipleFromRef<T, K extends Array<keyof T>>(
|
||||
): { [key in K[number]]: Ref<T[key]> } {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -66,12 +66,20 @@ export function getRequestsByPath(
|
||||
let currentCollection = collections[pathArray[0]]
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.requests
|
||||
const latestVersionedRequests = currentCollection.requests.filter(
|
||||
(req): req is HoppRESTRequest => req.v === "3"
|
||||
)
|
||||
|
||||
return latestVersionedRequests
|
||||
}
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
}
|
||||
|
||||
return currentCollection.requests
|
||||
const latestVersionedRequests = currentCollection.requests.filter(
|
||||
(req): req is HoppRESTRequest => req.v === "3"
|
||||
)
|
||||
|
||||
return latestVersionedRequests
|
||||
}
|
||||
|
||||
@@ -868,6 +868,38 @@ const samples = [
|
||||
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", () => {
|
||||
|
||||
@@ -33,7 +33,27 @@ export const parseCurlCommand = (curlCommand: string) => {
|
||||
// const compressed = !!parsedArguments.compressed
|
||||
|
||||
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 { headers } = headerObject
|
||||
|
||||
@@ -269,8 +269,16 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
|
||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||
} else if (auth.authType === "bearer") {
|
||||
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") {
|
||||
const { key, value, addTo } = auth
|
||||
if (addTo === "Headers") {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
getDefaultGQLRequest,
|
||||
getDefaultRESTRequest,
|
||||
translateToNewRESTCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { HoppGQLRequest, translateToNewGQLCollection } from "@hoppscotch/data"
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
import { translateToNewGQLCollection } from "@hoppscotch/data"
|
||||
import { entityReference } from "verzod"
|
||||
import { z } from "zod"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
export const hoppRESTImporter = (content: string[]) =>
|
||||
pipe(
|
||||
@@ -32,8 +36,24 @@ export const hoppRESTImporter = (content: string[]) =>
|
||||
* else translate it into one.
|
||||
*/
|
||||
const validateCollection = (collection: unknown) => {
|
||||
const result = entityReference(HoppCollection).safeParse(collection)
|
||||
if (result.success) return O.some(result.data)
|
||||
const collectionSchemaParsedResult = HoppCollection.safeParse(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))
|
||||
}
|
||||
@@ -64,9 +84,24 @@ export const hoppGQLImporter = (content: string) =>
|
||||
* @returns the collection if it is valid, else a translated version of the collection
|
||||
*/
|
||||
export const validateGQLCollection = (collection: unknown) => {
|
||||
const result = z.array(entityReference(HoppCollection)).safeParse(collection)
|
||||
const collectionSchemaParsedResult = HoppCollection.safeParse(collection)
|
||||
|
||||
if (result.success) return O.some(result.data)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -111,12 +111,16 @@ const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: !(auth.disabled ?? false),
|
||||
accessTokenURL: replaceVarTemplating(auth.accessTokenUrl ?? ""),
|
||||
authURL: replaceVarTemplating(auth.authorizationUrl ?? ""),
|
||||
clientID: replaceVarTemplating(auth.clientId ?? ""),
|
||||
oidcDiscoveryURL: "",
|
||||
scope: replaceVarTemplating(auth.scope ?? ""),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
authEndpoint: replaceVarTemplating(auth.authorizationUrl ?? ""),
|
||||
clientID: replaceVarTemplating(auth.clientId ?? ""),
|
||||
clientSecret: "",
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
scopes: replaceVarTemplating(auth.scope ?? ""),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: replaceVarTemplating(auth.accessTokenUrl ?? ""),
|
||||
},
|
||||
}
|
||||
else if (auth.type === "bearer")
|
||||
return {
|
||||
|
||||
@@ -279,67 +279,92 @@ const resolveOpenAPIV3SecurityObj = (
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.authorizationCode.tokenUrl ?? "",
|
||||
authURL: scheme.flows.authorizationCode.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
authEndpoint: scheme.flows.authorizationCode.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: scheme.flows.authorizationCode.tokenUrl ?? "",
|
||||
clientSecret: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flows.implicit) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: scheme.flows.implicit.authorizationUrl ?? "",
|
||||
accessTokenURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "IMPLICIT",
|
||||
authEndpoint: scheme.flows.implicit.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
token: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flows.password) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
authURL: "",
|
||||
accessTokenURL: scheme.flows.password.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "PASSWORD",
|
||||
clientID: "",
|
||||
authEndpoint: scheme.flows.password.tokenUrl,
|
||||
clientSecret: "",
|
||||
password: "",
|
||||
username: "",
|
||||
token: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flows.clientCredentials) {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.flows.clientCredentials.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "CLIENT_CREDENTIALS",
|
||||
authEndpoint: scheme.flows.clientCredentials.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
authEndpoint: "",
|
||||
clientID: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: "",
|
||||
clientSecret: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.type === "openIdConnect") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: scheme.openIdConnectUrl ?? "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
authEndpoint: "",
|
||||
clientID: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: "",
|
||||
clientSecret: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,56 +441,76 @@ const resolveOpenAPIV2SecurityScheme = (
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
authEndpoint: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: scheme.tokenUrl ?? "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flow === "implicit") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
authEndpoint: scheme.authorizationUrl ?? "",
|
||||
clientID: "",
|
||||
grantType: "IMPLICIT",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flow === "application") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
authEndpoint: scheme.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
grantType: "CLIENT_CREDENTIALS",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
} else if (scheme.flow === "password") {
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: scheme.tokenUrl ?? "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "PASSWORD",
|
||||
authEndpoint: scheme.tokenUrl ?? "",
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
password: "",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
username: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
}
|
||||
return {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: "",
|
||||
authURL: "",
|
||||
clientID: "",
|
||||
oidcDiscoveryURL: "",
|
||||
scope: _schemeData.join(" "),
|
||||
token: "",
|
||||
grantTypeInfo: {
|
||||
authEndpoint: "",
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
scopes: _schemeData.join(" "),
|
||||
token: "",
|
||||
isPKCE: false,
|
||||
tokenEndpoint: "",
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,25 +162,36 @@ const getHoppReqAuth = (item: Item): HoppRESTAuth => {
|
||||
),
|
||||
}
|
||||
} 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 {
|
||||
authType: "oauth-2",
|
||||
authActive: true,
|
||||
accessTokenURL: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "accessTokenUrl") ?? ""
|
||||
),
|
||||
authURL: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "authUrl") ?? ""
|
||||
),
|
||||
clientID: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "clientId") ?? ""
|
||||
),
|
||||
scope: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "scope") ?? ""
|
||||
),
|
||||
token: replacePMVarTemplating(
|
||||
getVariableValue(auth.oauth2, "accessToken") ?? ""
|
||||
),
|
||||
oidcDiscoveryURL: "",
|
||||
grantTypeInfo: {
|
||||
grantType: "AUTHORIZATION_CODE",
|
||||
authEndpoint: authURL,
|
||||
clientID: clientId,
|
||||
scopes: scope,
|
||||
token: token,
|
||||
tokenEndpoint: accessTokenURL,
|
||||
clientSecret: "",
|
||||
isPKCE: false,
|
||||
},
|
||||
addTo: "HEADERS",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -82,16 +82,17 @@ export const getComputedAuthHeaders = (
|
||||
})
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS")
|
||||
) {
|
||||
const token =
|
||||
request.auth.authType === "bearer"
|
||||
? request.auth.token
|
||||
: request.auth.grantTypeInfo.token
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${
|
||||
parse
|
||||
? parseTemplateString(request.auth.token, envVars)
|
||||
: request.auth.token
|
||||
}`,
|
||||
value: `Bearer ${parse ? parseTemplateString(token, envVars) : token}`,
|
||||
})
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
const { key, addTo } = request.auth
|
||||
@@ -196,17 +197,40 @@ export const getComputedParams = (
|
||||
): ComputedParam[] => {
|
||||
// When this gets complex, its best to split this function off (like with getComputedHeaders)
|
||||
// API-key auth can be added to query params
|
||||
if (!req.auth || !req.auth.authActive) return []
|
||||
if (req.auth.authType !== "api-key") return []
|
||||
if (req.auth.addTo !== "Query params") return []
|
||||
if (!req.auth || !req.auth.authActive) {
|
||||
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 [
|
||||
{
|
||||
source: "auth",
|
||||
param: {
|
||||
active: true,
|
||||
key: parseTemplateString(req.auth.key, envVars),
|
||||
value: parseTemplateString(req.auth.value, envVars),
|
||||
key: "access_token",
|
||||
value: parseTemplateString(grantTypeInfo.token, envVars),
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -250,7 +274,7 @@ function getFinalBodyFromRequest(
|
||||
|
||||
if (request.body.contentType === "application/x-www-form-urlencoded") {
|
||||
const parsedBodyRecord = pipe(
|
||||
request.body.body,
|
||||
request.body.body ?? "",
|
||||
parseRawKeyValueEntriesE,
|
||||
E.map(
|
||||
flow(
|
||||
@@ -287,7 +311,7 @@ function getFinalBodyFromRequest(
|
||||
|
||||
if (request.body.contentType === "multipart/form-data") {
|
||||
return pipe(
|
||||
request.body.body,
|
||||
request.body.body ?? [],
|
||||
A.filter((x) => (x.key !== "" || x.isFile) && x.active), // Remove empty keys
|
||||
|
||||
// Sort files down
|
||||
|
||||
@@ -79,7 +79,7 @@ const importCollections = (url: unknown, type: unknown) =>
|
||||
content.data,
|
||||
TO.fromPredicate(isOfType("string")),
|
||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||
TE.chain((data) => importer.importer(data))
|
||||
TE.chain((data) => importer.importer([data]))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,23 +5,31 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { handleOAuthRedirect } from "~/helpers/oauth"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { onMounted } from "vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
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 router = useRouter()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
const gqlTabs = useService(GQLTabService)
|
||||
const persistenceService = useService(PersistenceService)
|
||||
const restTabs = useService(RESTTabService)
|
||||
|
||||
function translateOAuthRedirectError(error: string) {
|
||||
switch (error) {
|
||||
@@ -60,22 +68,59 @@ function translateOAuthRedirectError(error: string) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const tokenInfo = await handleOAuthRedirect()
|
||||
const localOAuthTempConfig =
|
||||
persistenceService.getLocalConfig("oauth_temp_config")
|
||||
|
||||
if (!localOAuthTempConfig) {
|
||||
toast.error(t("authorization.oauth.something_went_wrong_on_oauth_redirect"))
|
||||
router.push("/")
|
||||
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("/")
|
||||
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
|
||||
const authConfig: PersistedOAuthConfig = {
|
||||
...persistedOAuthConfig,
|
||||
token: tokenInfo.right.access_token,
|
||||
}
|
||||
persistenceService.setLocalConfig(
|
||||
"oauth_temp_config",
|
||||
JSON.stringify(authConfig)
|
||||
)
|
||||
|
||||
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 (
|
||||
tabs.currentActiveTab.value.document.request.auth.authType === "oauth-2"
|
||||
tabService.currentActiveTab.value.document.request.auth.authType ===
|
||||
"oauth-2"
|
||||
) {
|
||||
tabs.currentActiveTab.value.document.request.auth.token =
|
||||
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
|
||||
tokenInfo.right.access_token
|
||||
|
||||
router.push("/")
|
||||
return
|
||||
toast.success(t("authorization.oauth.token_fetched_successfully"))
|
||||
}
|
||||
|
||||
router.push(routeToRedirect)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -210,6 +210,8 @@ import { toggleSetting } from "~/newstore/settings"
|
||||
import IconVerified from "~icons/lucide/verified"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
type ProfileTabs = "sync" | "teams"
|
||||
|
||||
const selectedProfileTab = ref<ProfileTabs>("sync")
|
||||
@@ -244,19 +246,28 @@ const displayName = ref(currentUser.value?.displayName || "")
|
||||
const updatingDisplayName = ref(false)
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName || ""))
|
||||
|
||||
const updateDisplayName = () => {
|
||||
const updateDisplayName = async () => {
|
||||
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
|
||||
platform.auth
|
||||
.setDisplayName(displayName.value as string)
|
||||
.then(() => {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
})
|
||||
.finally(() => {
|
||||
updatingDisplayName.value = false
|
||||
})
|
||||
|
||||
const res = await platform.auth.setDisplayName(displayName.value)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
} else if (E.isRight(res)) {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
}
|
||||
|
||||
updatingDisplayName.value = false
|
||||
}
|
||||
|
||||
const emailAddress = ref(currentUser.value?.email || "")
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Observable } from "rxjs"
|
||||
import { Component } from "vue"
|
||||
import { getI18n } from "~/modules/i18n"
|
||||
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.
|
||||
@@ -135,6 +137,15 @@ export type AuthPlatformDef = {
|
||||
*/
|
||||
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
|
||||
* copy auth token from Developer Options.
|
||||
@@ -219,9 +230,11 @@ export type AuthPlatformDef = {
|
||||
/**
|
||||
* Updates the display name of the user
|
||||
* @param name The new name to set this to.
|
||||
* @returns An empty promise that is resolved when the operation is complete
|
||||
* @returns A promise that resolves with the display name update status when the operation is complete
|
||||
*/
|
||||
setDisplayName: (name: string) => Promise<void>
|
||||
setDisplayName: (
|
||||
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 )
|
||||
|
||||
@@ -231,6 +231,7 @@ export class ExtensionInterceptorService
|
||||
try {
|
||||
const result = await extensionHook.sendRequest({
|
||||
...req,
|
||||
headers: req.headers ?? {},
|
||||
wantsBinary: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ export class HeaderInspectorService extends Service implements Inspector {
|
||||
index: index,
|
||||
},
|
||||
doc: {
|
||||
text: this.t("action.learn_more"),
|
||||
link: "https://docs.hoppscotch.io/documentation/features/inspections",
|
||||
text: this.t("action.download_here"),
|
||||
link: "https://hoppscotch.com/download",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
293
packages/hoppscotch-common/src/services/oauth/flows/authCode.ts
Normal file
293
packages/hoppscotch-common/src/services/oauth/flows/authCode.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
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
|
||||
)
|
||||
135
packages/hoppscotch-common/src/services/oauth/flows/implicit.ts
Normal file
135
packages/hoppscotch-common/src/services/oauth/flows/implicit.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
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
|
||||
)
|
||||
189
packages/hoppscotch-common/src/services/oauth/flows/password.ts
Normal file
189
packages/hoppscotch-common/src/services/oauth/flows/password.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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
|
||||
)
|
||||
124
packages/hoppscotch-common/src/services/oauth/oauth.service.ts
Normal file
124
packages/hoppscotch-common/src/services/oauth/oauth.service.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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], "")
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
folders: [],
|
||||
requests: [
|
||||
{
|
||||
v: "2",
|
||||
v: "3",
|
||||
endpoint: "https://echo.hoppscotch.io",
|
||||
name: "Echo test",
|
||||
params: [],
|
||||
@@ -50,7 +50,7 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
folders: [],
|
||||
requests: [
|
||||
{
|
||||
v: 2,
|
||||
v: 3,
|
||||
name: "Echo test",
|
||||
url: "https://echo.hoppscotch.io/graphql",
|
||||
headers: [],
|
||||
@@ -138,7 +138,7 @@ export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
requestVariables: [],
|
||||
v: "2",
|
||||
v: "3",
|
||||
},
|
||||
responseMeta: { duration: 807, statusCode: 200 },
|
||||
star: false,
|
||||
@@ -150,7 +150,7 @@ export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [
|
||||
{
|
||||
v: 1,
|
||||
request: {
|
||||
v: 2,
|
||||
v: 3,
|
||||
name: "Untitled",
|
||||
url: "https://echo.hoppscotch.io/graphql",
|
||||
query: "query Request { url }",
|
||||
@@ -171,7 +171,7 @@ export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
|
||||
tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
|
||||
doc: {
|
||||
request: {
|
||||
v: 2,
|
||||
v: 3,
|
||||
name: "Untitled",
|
||||
url: "https://echo.hoppscotch.io/graphql",
|
||||
headers: [],
|
||||
@@ -194,7 +194,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
|
||||
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
|
||||
doc: {
|
||||
request: {
|
||||
v: "2",
|
||||
v: "3",
|
||||
endpoint: "https://echo.hoppscotch.io",
|
||||
name: "Echo test",
|
||||
params: [],
|
||||
|
||||
@@ -169,7 +169,16 @@ export class CollectionsSpotlightSearcherService
|
||||
}
|
||||
|
||||
scopeHandle.run(() => {
|
||||
const isPersonalWorkspace = computed(
|
||||
() => this.workspaceService.currentWorkspace.value.type === "personal"
|
||||
)
|
||||
|
||||
watch(query, (query) => {
|
||||
if (!isPersonalWorkspace.value) {
|
||||
results.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (pageCategory === "other") {
|
||||
results.value = []
|
||||
return
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"dist/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build:code": "vite build",
|
||||
"build:decl": "tsc --project tsconfig.decl.json",
|
||||
"build": "pnpm run build:code && pnpm run build:decl",
|
||||
@@ -46,4 +47,4 @@
|
||||
"verzod": "0.2.2",
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import { translateToNewRequest } from "../rest"
|
||||
import { translateToGQLRequest } from "../graphql"
|
||||
|
||||
const versionedObject = z.object({
|
||||
// v is a stringified number
|
||||
v: z.string().regex(/^\d+$/).transform(Number),
|
||||
v: z.number(),
|
||||
})
|
||||
|
||||
export const HoppCollection = createVersionedEntity({
|
||||
@@ -26,7 +25,7 @@ export const HoppCollection = createVersionedEntity({
|
||||
// For V1 we have to check the schema
|
||||
const result = V1_VERSION.schema.safeParse(data)
|
||||
|
||||
return result.success ? 0 : null
|
||||
return result.success ? 1 : null
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,29 +2,32 @@ import { InferredEntity, createVersionedEntity } from "verzod"
|
||||
import { z } from "zod"
|
||||
import V1_VERSION from "./v/1"
|
||||
import V2_VERSION from "./v/2"
|
||||
import V3_VERSION from "./v/3"
|
||||
|
||||
export { GQLHeader } from "./v/1"
|
||||
export {
|
||||
HoppGQLAuth,
|
||||
HoppGQLAuthAPIKey,
|
||||
HoppGQLAuthBasic,
|
||||
HoppGQLAuthBearer,
|
||||
HoppGQLAuthNone,
|
||||
HoppGQLAuthOAuth2,
|
||||
HoppGQLAuthInherit,
|
||||
} from "./v/2"
|
||||
|
||||
export const GQL_REQ_SCHEMA_VERSION = 2
|
||||
export { HoppGQLAuth } from "./v/3"
|
||||
export { HoppGQLAuthOAuth2 } from "./v/3"
|
||||
|
||||
export const GQL_REQ_SCHEMA_VERSION = 3
|
||||
|
||||
const versionedObject = z.object({
|
||||
v: z.number(),
|
||||
})
|
||||
|
||||
export const HoppGQLRequest = createVersionedEntity({
|
||||
latestVersion: 2,
|
||||
latestVersion: 3,
|
||||
versionMap: {
|
||||
1: V1_VERSION,
|
||||
2: V2_VERSION,
|
||||
3: V3_VERSION,
|
||||
},
|
||||
getVersion(x) {
|
||||
const result = versionedObject.safeParse(x)
|
||||
|
||||
@@ -71,7 +71,7 @@ export const HoppGQLAuth = z
|
||||
|
||||
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
|
||||
|
||||
const V2_SCHEMA = z.object({
|
||||
export const V2_SCHEMA = z.object({
|
||||
id: z.optional(z.string()),
|
||||
v: z.literal(2),
|
||||
|
||||
|
||||
77
packages/hoppscotch-data/src/graphql/v/3.ts
Normal file
77
packages/hoppscotch-data/src/graphql/v/3.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -4,32 +4,39 @@ import cloneDeep from "lodash/cloneDeep"
|
||||
import V0_VERSION from "./v/0"
|
||||
import V1_VERSION from "./v/1"
|
||||
import V2_VERSION from "./v/2"
|
||||
import V3_VERSION from "./v/3"
|
||||
import { createVersionedEntity, InferredEntity } from "verzod"
|
||||
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "../utils/eq"
|
||||
import {
|
||||
HoppRESTAuth,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTHeaders,
|
||||
HoppRESTParams,
|
||||
} from "./v/1"
|
||||
|
||||
import { HoppRESTReqBody, HoppRESTHeaders, HoppRESTParams } from "./v/1"
|
||||
|
||||
import { HoppRESTAuth } from "./v/3"
|
||||
import { HoppRESTRequestVariables } from "./v/2"
|
||||
import { z } from "zod"
|
||||
|
||||
export * from "./content-types"
|
||||
|
||||
export {
|
||||
FormDataKeyValue,
|
||||
HoppRESTReqBodyFormData,
|
||||
HoppRESTAuth,
|
||||
HoppRESTAuthAPIKey,
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthInherit,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthNone,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTHeaders,
|
||||
} from "./v/1"
|
||||
|
||||
export {
|
||||
HoppRESTAuth,
|
||||
HoppRESTAuthOAuth2,
|
||||
AuthCodeGrantTypeParams,
|
||||
ClientCredentialsGrantTypeParams,
|
||||
ImplicitOauthFlowParams,
|
||||
PasswordGrantTypeParams,
|
||||
} from "./v/3"
|
||||
|
||||
export { HoppRESTRequestVariables } from "./v/2"
|
||||
|
||||
const versionedObject = z.object({
|
||||
@@ -38,11 +45,12 @@ const versionedObject = z.object({
|
||||
})
|
||||
|
||||
export const HoppRESTRequest = createVersionedEntity({
|
||||
latestVersion: 2,
|
||||
latestVersion: 3,
|
||||
versionMap: {
|
||||
0: V0_VERSION,
|
||||
1: V1_VERSION,
|
||||
2: V2_VERSION,
|
||||
3: V3_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
// For V1 onwards we have the v string storing the number
|
||||
@@ -84,7 +92,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
|
||||
),
|
||||
})
|
||||
|
||||
export const RESTReqSchemaVersion = "2"
|
||||
export const RESTReqSchemaVersion = "3"
|
||||
|
||||
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
||||
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
||||
@@ -179,7 +187,7 @@ export function makeRESTRequest(
|
||||
|
||||
export function getDefaultRESTRequest(): HoppRESTRequest {
|
||||
return {
|
||||
v: "2",
|
||||
v: "3",
|
||||
endpoint: "https://echo.hoppscotch.io",
|
||||
name: "Untitled",
|
||||
params: [],
|
||||
|
||||
@@ -18,7 +18,7 @@ export const HoppRESTRequestVariables = z.array(
|
||||
|
||||
export type HoppRESTRequestVariables = z.infer<typeof HoppRESTRequestVariables>
|
||||
|
||||
const V2_SCHEMA = V1_SCHEMA.extend({
|
||||
export const V2_SCHEMA = V1_SCHEMA.extend({
|
||||
v: z.literal("2"),
|
||||
requestVariables: HoppRESTRequestVariables,
|
||||
})
|
||||
|
||||
127
packages/hoppscotch-data/src/rest/v/3.ts
Normal file
127
packages/hoppscotch-data/src/rest/v/3.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { z } from "zod"
|
||||
import {
|
||||
HoppRESTAuthAPIKey,
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthInherit,
|
||||
HoppRESTAuthNone,
|
||||
} from "./1"
|
||||
import { V2_SCHEMA } from "./2"
|
||||
|
||||
import { defineVersion } from "verzod"
|
||||
|
||||
export const AuthCodeGrantTypeParams = z.object({
|
||||
grantType: z.literal("AUTHORIZATION_CODE"),
|
||||
authEndpoint: z.string().trim(),
|
||||
tokenEndpoint: z.string().trim(),
|
||||
clientID: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
scopes: z.string().trim().optional(),
|
||||
token: z.string().catch(""),
|
||||
isPKCE: z.boolean(),
|
||||
codeVerifierMethod: z
|
||||
.union([z.literal("plain"), z.literal("S256")])
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const ClientCredentialsGrantTypeParams = z.object({
|
||||
grantType: z.literal("CLIENT_CREDENTIALS"),
|
||||
authEndpoint: z.string().trim(),
|
||||
clientID: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
scopes: z.string().trim().optional(),
|
||||
token: z.string().catch(""),
|
||||
})
|
||||
|
||||
export const PasswordGrantTypeParams = z.object({
|
||||
grantType: z.literal("PASSWORD"),
|
||||
authEndpoint: z.string().trim(),
|
||||
clientID: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
scopes: z.string().trim().optional(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
token: z.string().catch(""),
|
||||
})
|
||||
|
||||
export const ImplicitOauthFlowParams = z.object({
|
||||
grantType: z.literal("IMPLICIT"),
|
||||
authEndpoint: z.string().trim(),
|
||||
clientID: z.string().trim(),
|
||||
scopes: z.string().trim().optional(),
|
||||
token: z.string().catch(""),
|
||||
})
|
||||
|
||||
export const HoppRESTAuthOAuth2 = z.object({
|
||||
authType: z.literal("oauth-2"),
|
||||
grantTypeInfo: z.discriminatedUnion("grantType", [
|
||||
AuthCodeGrantTypeParams,
|
||||
ClientCredentialsGrantTypeParams,
|
||||
PasswordGrantTypeParams,
|
||||
ImplicitOauthFlowParams,
|
||||
]),
|
||||
addTo: z.enum(["HEADERS", "QUERY_PARAMS"]).catch("HEADERS"),
|
||||
})
|
||||
|
||||
export type HoppRESTAuthOAuth2 = z.infer<typeof HoppRESTAuthOAuth2>
|
||||
|
||||
export const HoppRESTAuth = z
|
||||
.discriminatedUnion("authType", [
|
||||
HoppRESTAuthNone,
|
||||
HoppRESTAuthInherit,
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTAuthAPIKey,
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
authActive: z.boolean(),
|
||||
})
|
||||
)
|
||||
|
||||
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
|
||||
|
||||
// V2_SCHEMA has one change in HoppRESTAuthOAuth2, we'll add the grant_type field
|
||||
export const V3_SCHEMA = V2_SCHEMA.extend({
|
||||
v: z.literal("3"),
|
||||
auth: HoppRESTAuth,
|
||||
})
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -11,6 +11,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
ecmaFeatures: {
|
||||
jsx: false,
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
"@vue/typescript/recommended",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation UpdateUserDisplayName($updatedDisplayName: String!) {
|
||||
updateDisplayName(updatedDisplayName: $updatedDisplayName) {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import { runMutation } from "@hoppscotch/common/helpers/backend/GQLClient"
|
||||
import axios from "axios"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { z } from "zod"
|
||||
import {
|
||||
UpdateUserDisplayNameDocument,
|
||||
UpdateUserDisplayNameMutation,
|
||||
UpdateUserDisplayNameMutationVariables,
|
||||
} from "../../api/generated/graphql"
|
||||
|
||||
const expectedAllowedProvidersSchema = z.object({
|
||||
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
|
||||
@@ -28,3 +34,12 @@ export const getAllowedAuthProviders = async () => {
|
||||
return E.left("SOMETHING_WENT_WRONG")
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUserDisplayName = (updatedDisplayName: string) =>
|
||||
runMutation<
|
||||
UpdateUserDisplayNameMutation,
|
||||
UpdateUserDisplayNameMutationVariables,
|
||||
""
|
||||
>(UpdateUserDisplayNameDocument, {
|
||||
updatedDisplayName,
|
||||
})()
|
||||
|
||||
@@ -8,7 +8,8 @@ import { PersistenceService } from "@hoppscotch/common/services/persistence"
|
||||
import axios from "axios"
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { Ref, ref, watch } from "vue"
|
||||
import { getAllowedAuthProviders } from "./auth.api"
|
||||
import { getAllowedAuthProviders, updateUserDisplayName } from "./auth.api"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
@@ -211,6 +212,13 @@ export const def: AuthPlatformDef = {
|
||||
}
|
||||
},
|
||||
|
||||
axiosPlatformConfig() {
|
||||
return {
|
||||
// for including cookies in the request
|
||||
withCredentials: true,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* it is not possible for us to know if the current cookie is expired because we cannot access http-only cookies from js
|
||||
* hence just returning if the currentUser$ has a value associated with it
|
||||
@@ -310,9 +318,22 @@ export const def: AuthPlatformDef = {
|
||||
async setEmailAddress(_email: string) {
|
||||
return
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
async setDisplayName(name: string) {
|
||||
return
|
||||
if (!name) return E.left("USER_NAME_CANNOT_BE_EMPTY")
|
||||
if (!currentUser$.value) return E.left("NO_USER_LOGGED_IN")
|
||||
|
||||
const res = await updateUserDisplayName(name)
|
||||
|
||||
if (E.isRight(res)) {
|
||||
setUser({
|
||||
...currentUser$.value,
|
||||
displayName: res.right.updateDisplayName.displayName ?? null,
|
||||
})
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
return E.left(res.left)
|
||||
},
|
||||
|
||||
async signOutUser() {
|
||||
|
||||
@@ -164,11 +164,14 @@
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"reenter_email": "Re-enter email",
|
||||
"remove_admin_failure": "Failed to remove admin status!!",
|
||||
"remove_admin_failure_only_one_admin": "Failed to remove admin status. There should be at least one admin!!",
|
||||
"remove_admin_success": "Admin status removed!!",
|
||||
"remove_admin_from_users_failure": "Failed to remove admin status from selected users!!",
|
||||
"remove_admin_from_users_success": "Admin status removed from selected users!!",
|
||||
"remove_admin_to_delete_user": "Remove admin privilege to delete the user!!",
|
||||
"remove_owner_to_delete_user": "Remove team ownership status to delete the user!!",
|
||||
"remove_admin_for_deletion": "Remove admin status before attempting deletion!!",
|
||||
"remove_owner_for_deletion": "One or more users are team owners. Update ownership before deletion!!",
|
||||
"remove_invitee_failure": "Removal of invitee failed!!",
|
||||
"remove_invitee_success": "Removal of invitee is successfull!!",
|
||||
"remove_member_failure": "Member couldn't be removed!!",
|
||||
@@ -253,7 +256,6 @@
|
||||
},
|
||||
"users": {
|
||||
"admin": "Admin",
|
||||
"admin_email": "Admin Email",
|
||||
"admin_id": "Admin ID",
|
||||
"cancel": "Cancel",
|
||||
"created_on": "Created On",
|
||||
@@ -270,6 +272,7 @@
|
||||
"invalid_user": "Invalid User",
|
||||
"invite_load_list_error": "Unable to Load Invited Users List",
|
||||
"invite_user": "Invite User",
|
||||
"invited_by": "Invited By",
|
||||
"invited_on": "Invited On",
|
||||
"invited_users": "Invited Users",
|
||||
"invitee_email": "Invitee Email",
|
||||
|
||||
@@ -51,7 +51,6 @@ declare module '@vue/runtime-core' {
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
||||
UsersTable: typeof import('./components/users/Table.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export const UNAUTHORIZED = 'Unauthorized' as const;
|
||||
// Sometimes the backend returns Unauthorized error message as follows:
|
||||
export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const;
|
||||
|
||||
export const DELETE_USER_FAILED_ONLY_ONE_ADMIN =
|
||||
'admin/only_one_admin_account_found' as const;
|
||||
export const ONLY_ONE_ADMIN_ACCOUNT_FOUND =
|
||||
'[GraphQL] admin/only_one_admin_account_found' as const;
|
||||
|
||||
export const ADMIN_CANNOT_BE_DELETED =
|
||||
'admin/admin_can_not_be_deleted' as const;
|
||||
@@ -17,3 +17,6 @@ export const ADMIN_CANNOT_BE_DELETED =
|
||||
// When trying to invite a user that is already invited
|
||||
export const USER_ALREADY_INVITED =
|
||||
'[GraphQL] admin/user_already_invited' as const;
|
||||
|
||||
// When attempting to delete a user who is an owner of a team
|
||||
export const USER_IS_OWNER = 'user/is_owner' as const;
|
||||
|
||||
113
packages/hoppscotch-sh-admin/src/helpers/userManagement.ts
Normal file
113
packages/hoppscotch-sh-admin/src/helpers/userManagement.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { getI18n } from '~/modules/i18n';
|
||||
import { UserDeletionResult } from './backend/graphql';
|
||||
import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from './errors';
|
||||
|
||||
type ToastMessage = {
|
||||
message: string;
|
||||
state: 'success' | 'error';
|
||||
};
|
||||
|
||||
const t = getI18n();
|
||||
const toast = useToast();
|
||||
|
||||
const displayToastMessages = (
|
||||
toastMessages: ToastMessage[],
|
||||
currentIndex: number
|
||||
) => {
|
||||
const { message, state } = toastMessages[currentIndex];
|
||||
|
||||
toast[state](message, {
|
||||
duration: 2000,
|
||||
onComplete: () => {
|
||||
if (currentIndex < toastMessages.length - 1) {
|
||||
displayToastMessages(toastMessages, currentIndex + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const handleUserDeletion = (deletedUsersList: UserDeletionResult[]) => {
|
||||
const uniqueErrorMessages = new Set(
|
||||
deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean)
|
||||
) as Set<string>;
|
||||
|
||||
const isBulkAction = deletedUsersList.length > 1;
|
||||
|
||||
const deletedUserIDs = deletedUsersList
|
||||
.filter((user) => user.isDeleted)
|
||||
.map((user) => user.userUID);
|
||||
|
||||
// Show the success toast based on the action type if there are no errors
|
||||
if (uniqueErrorMessages.size === 0) {
|
||||
toast.success(
|
||||
isBulkAction
|
||||
? t('state.delete_users_success')
|
||||
: t('state.delete_user_success')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const errMsgMap = {
|
||||
[ADMIN_CANNOT_BE_DELETED]: isBulkAction
|
||||
? t('state.remove_admin_for_deletion')
|
||||
: t('state.remove_admin_to_delete_user'),
|
||||
|
||||
[USER_IS_OWNER]: isBulkAction
|
||||
? t('state.remove_owner_for_deletion')
|
||||
: t('state.remove_owner_to_delete_user'),
|
||||
};
|
||||
const errMsgMapKeys = Object.keys(errMsgMap);
|
||||
|
||||
const toastMessages: ToastMessage[] = [];
|
||||
|
||||
if (isBulkAction) {
|
||||
// Indicates the actual count of users deleted (filtered via the `isDeleted` field)
|
||||
const deletedUsersCount = deletedUserIDs.length;
|
||||
|
||||
if (isBulkAction && deletedUsersCount > 0) {
|
||||
toastMessages.push({
|
||||
message: t('state.delete_some_users_success', {
|
||||
count: deletedUsersCount,
|
||||
}),
|
||||
state: 'success',
|
||||
});
|
||||
}
|
||||
const remainingDeletionsCount = deletedUsersList.length - deletedUsersCount;
|
||||
if (remainingDeletionsCount > 0) {
|
||||
toastMessages.push({
|
||||
message: t('state.delete_some_users_failure', {
|
||||
count: remainingDeletionsCount,
|
||||
}),
|
||||
state: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uniqueErrorMessages.forEach((errorMessage) => {
|
||||
if (errMsgMapKeys.includes(errorMessage)) {
|
||||
toastMessages.push({
|
||||
message: errMsgMap[errorMessage as keyof typeof errMsgMap],
|
||||
state: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback for the case where the error message is not in the compiled list
|
||||
if (
|
||||
Array.from(uniqueErrorMessages).some(
|
||||
(key) => !((key as string) in errMsgMap)
|
||||
)
|
||||
) {
|
||||
const fallbackErrMsg = isBulkAction
|
||||
? t('state.delete_users_failure')
|
||||
: t('state.delete_user_failure');
|
||||
|
||||
toastMessages.push({
|
||||
message: fallbackErrMsg,
|
||||
state: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
displayToastMessages(toastMessages, 0);
|
||||
};
|
||||
@@ -1,7 +1,23 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { I18n, createI18n } from 'vue-i18n';
|
||||
import { HoppModule } from '.';
|
||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||
|
||||
// A reference to the i18n instance
|
||||
let i18nInstance: I18n<
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
string,
|
||||
false
|
||||
> | null = null;
|
||||
|
||||
/**
|
||||
* Returns the i18n instance
|
||||
*/
|
||||
export function getI18n() {
|
||||
return i18nInstance!.global.t;
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
const i18n = createI18n({
|
||||
@@ -11,6 +27,9 @@ export default <HoppModule>{
|
||||
legacy: false,
|
||||
allowComposition: true,
|
||||
});
|
||||
|
||||
app.use(i18n);
|
||||
|
||||
i18nInstance = i18n;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
RemoveUsersByAdminDocument,
|
||||
UserInfoDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { ADMIN_CANNOT_BE_DELETED } from '~/helpers/errors';
|
||||
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
@@ -210,16 +210,11 @@ const deleteUserMutation = async (id: string | null) => {
|
||||
} else {
|
||||
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
||||
|
||||
const isAdminError = deletedUsers.some(
|
||||
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
|
||||
);
|
||||
|
||||
isAdminError
|
||||
? toast.error(t('state.delete_user_failed_only_one_admin'))
|
||||
: toast.success(t('state.delete_user_success'));
|
||||
handleUserDeletion(deletedUsers);
|
||||
}
|
||||
confirmDeletion.value = false;
|
||||
deleteUserUID.value = null;
|
||||
router.push('/users');
|
||||
|
||||
!result.error && router.push('/users');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto mb-5">
|
||||
<div class="mb-3 flex items-center justify-end">
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
@@ -210,7 +210,7 @@
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmUsersToAdmin"
|
||||
:title="
|
||||
AreMultipleUsersSelected
|
||||
areMultipleUsersSelected
|
||||
? t('state.confirm_users_to_admin')
|
||||
: t('state.confirm_user_to_admin')
|
||||
"
|
||||
@@ -220,7 +220,7 @@
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmAdminsToUsers"
|
||||
:title="
|
||||
AreMultipleUsersSelectedToAdmin
|
||||
areMultipleUsersSelectedToAdmin
|
||||
? t('state.confirm_admins_to_users')
|
||||
: t('state.confirm_admin_to_user')
|
||||
"
|
||||
@@ -230,7 +230,7 @@
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmUsersDeletion"
|
||||
:title="
|
||||
AreMultipleUsersSelectedForDeletion
|
||||
areMultipleUsersSelectedForDeletion
|
||||
? t('state.confirm_users_deletion')
|
||||
: t('state.confirm_user_deletion')
|
||||
"
|
||||
@@ -259,10 +259,10 @@ import {
|
||||
UsersListV2Document,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import {
|
||||
ADMIN_CANNOT_BE_DELETED,
|
||||
DELETE_USER_FAILED_ONLY_ONE_ADMIN,
|
||||
ONLY_ONE_ADMIN_ACCOUNT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
} from '~/helpers/errors';
|
||||
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||
import IconCheck from '~icons/lucide/check';
|
||||
import IconLeft from '~icons/lucide/chevron-left';
|
||||
import IconRight from '~icons/lucide/chevron-right';
|
||||
@@ -309,16 +309,10 @@ const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
|
||||
// Ensure this variable is declared outside the debounce function
|
||||
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
let toastTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
if (debounceTimeout) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
// Debounce Function
|
||||
@@ -462,7 +456,7 @@ const confirmUsersToAdmin = ref(false);
|
||||
const usersToAdminUID = ref<string | null>(null);
|
||||
const usersToAdmin = useMutation(MakeUsersAdminDocument);
|
||||
|
||||
const AreMultipleUsersSelected = computed(() => selectedRows.value.length > 1);
|
||||
const areMultipleUsersSelected = computed(() => selectedRows.value.length > 1);
|
||||
|
||||
const confirmUserToAdmin = (id: string | null) => {
|
||||
confirmUsersToAdmin.value = true;
|
||||
@@ -482,11 +476,15 @@ const makeUsersToAdmin = async (id: string | null) => {
|
||||
|
||||
if (result.error) {
|
||||
toast.error(
|
||||
id ? t('state.admin_failure') : t('state.users_to_admin_failure')
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.users_to_admin_failure')
|
||||
: t('state.admin_failure')
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
id ? t('state.admin_success') : t('state.users_to_admin_success')
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.users_to_admin_success')
|
||||
: t('state.admin_success')
|
||||
);
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
@@ -514,7 +512,7 @@ const resetConfirmAdminToUser = () => {
|
||||
adminsToUserUID.value = null;
|
||||
};
|
||||
|
||||
const AreMultipleUsersSelectedToAdmin = computed(
|
||||
const areMultipleUsersSelectedToAdmin = computed(
|
||||
() => selectedRows.value.length > 1
|
||||
);
|
||||
|
||||
@@ -524,16 +522,20 @@ const makeAdminsToUsers = async (id: string | null) => {
|
||||
const variables = { userUIDs };
|
||||
const result = await adminsToUser.executeMutation(variables);
|
||||
if (result.error) {
|
||||
if (result.error.message === ONLY_ONE_ADMIN_ACCOUNT_FOUND) {
|
||||
return toast.error(t('state.remove_admin_failure_only_one_admin'));
|
||||
}
|
||||
|
||||
toast.error(
|
||||
id
|
||||
? t('state.remove_admin_failure')
|
||||
: t('state.remove_admin_from_users_failure')
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.remove_admin_from_users_failure')
|
||||
: t('state.remove_admin_failure')
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
id
|
||||
? t('state.remove_admin_success')
|
||||
: t('state.remove_admin_from_users_success')
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.remove_admin_from_users_success')
|
||||
: t('state.remove_admin_success')
|
||||
);
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
@@ -562,7 +564,7 @@ const resetConfirmUserDeletion = () => {
|
||||
deleteUserUID.value = null;
|
||||
};
|
||||
|
||||
const AreMultipleUsersSelectedForDeletion = computed(
|
||||
const areMultipleUsersSelectedForDeletion = computed(
|
||||
() => selectedRows.value.length > 1
|
||||
);
|
||||
|
||||
@@ -572,45 +574,22 @@ const deleteUsers = async (id: string | null) => {
|
||||
const result = await usersDeletion.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
const errorMessage =
|
||||
result.error.message === DELETE_USER_FAILED_ONLY_ONE_ADMIN
|
||||
? t('state.delete_user_failed_only_one_admin')
|
||||
: id
|
||||
? t('state.delete_user_failure')
|
||||
: t('state.delete_users_failure');
|
||||
const errorMessage = areMultipleUsersSelected.value
|
||||
? t('state.delete_users_failure')
|
||||
: t('state.delete_user_failure');
|
||||
toast.error(errorMessage);
|
||||
} else {
|
||||
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
||||
const deletedIDs = deletedUsers
|
||||
const deletedUserIDs = deletedUsers
|
||||
.filter((user) => user.isDeleted)
|
||||
.map((user) => user.userUID);
|
||||
|
||||
const isAdminError = deletedUsers.some(
|
||||
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
|
||||
);
|
||||
handleUserDeletion(deletedUsers);
|
||||
|
||||
usersList.value = usersList.value.filter(
|
||||
(user) => !deletedIDs.includes(user.uid)
|
||||
(user) => !deletedUserIDs.includes(user.uid)
|
||||
);
|
||||
|
||||
if (isAdminError) {
|
||||
toast.success(
|
||||
t('state.delete_some_users_success', { count: deletedIDs.length })
|
||||
);
|
||||
toast.error(
|
||||
t('state.delete_some_users_failure', {
|
||||
count: deletedUsers.length - deletedIDs.length,
|
||||
})
|
||||
);
|
||||
toastTimeout = setTimeout(() => {
|
||||
toast.error(t('state.remove_admin_for_deletion'));
|
||||
}, 2000);
|
||||
} else {
|
||||
toast.success(
|
||||
id ? t('state.delete_user_success') : t('state.delete_users_success')
|
||||
);
|
||||
}
|
||||
|
||||
selectedRows.value.splice(0, selectedRows.value.length);
|
||||
}
|
||||
confirmUsersDeletion.value = false;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<template #action="{ item }">
|
||||
<div v-if="item" class="my-1 mr-2">
|
||||
<HoppButtonSecondary
|
||||
v-if="xlAndLarger"
|
||||
v-if="lgAndLarger"
|
||||
:icon="IconTrash"
|
||||
:label="t('users.revoke_invitation')"
|
||||
class="text-secondaryDark bg-red-500 hover:bg-red-600"
|
||||
@@ -119,7 +119,7 @@ const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const xlAndLarger = breakpoints.greater('xl');
|
||||
const lgAndLarger = breakpoints.greater('lg');
|
||||
|
||||
// Get Proper Date Formats
|
||||
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MM-yyyy');
|
||||
@@ -130,9 +130,8 @@ const { fetching, error, data } = useQuery({ query: InvitedUsersDocument });
|
||||
|
||||
// Table Headings
|
||||
const headings = [
|
||||
{ key: 'adminUid', label: t('users.admin_id') },
|
||||
{ key: 'adminEmail', label: t('users.admin_email') },
|
||||
{ key: 'inviteeEmail', label: t('users.invitee_email') },
|
||||
{ key: 'adminEmail', label: t('users.invited_by') },
|
||||
{ key: 'invitedOn', label: t('users.invited_on') },
|
||||
{ key: 'action', label: 'Action' },
|
||||
];
|
||||
|
||||
582
pnpm-lock.yaml
generated
582
pnpm-lock.yaml
generated
@@ -21,7 +21,7 @@ importers:
|
||||
version: 16.2.4
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.1.0
|
||||
version: 0.1.0(eslint@8.55.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9)
|
||||
version: 0.1.0(eslint@8.57.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9)
|
||||
'@types/node':
|
||||
specifier: 17.0.27
|
||||
version: 17.0.27
|
||||
@@ -411,7 +411,7 @@ importers:
|
||||
version: link:../hoppscotch-js-sandbox
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.1.0
|
||||
version: 0.1.0(eslint@8.55.0)(terser@5.27.0)(vite@4.5.0)(vue@3.3.9)
|
||||
version: 0.1.0(eslint@8.57.0)(terser@5.27.0)(vite@4.5.0)(vue@3.3.9)
|
||||
'@hoppscotch/vue-toasted':
|
||||
specifier: 0.1.0
|
||||
version: 0.1.0(vue@3.3.9)
|
||||
@@ -678,11 +678,11 @@ importers:
|
||||
specifier: 21.0.3
|
||||
version: 21.0.3
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 6.13.2
|
||||
version: 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.2)
|
||||
specifier: 7.3.1
|
||||
version: 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 6.13.2
|
||||
version: 6.13.2(eslint@8.55.0)(typescript@5.3.2)
|
||||
specifier: 7.3.1
|
||||
version: 7.3.1(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 4.5.1
|
||||
version: 4.5.1(vite@4.5.0)(vue@3.3.9)
|
||||
@@ -691,7 +691,7 @@ importers:
|
||||
version: 3.3.10
|
||||
'@vue/eslint-config-typescript':
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0(eslint-plugin-vue@9.19.2)(eslint@8.55.0)(typescript@5.3.2)
|
||||
version: 12.0.0(eslint-plugin-vue@9.24.0)(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@vue/runtime-core':
|
||||
specifier: 3.3.10
|
||||
version: 3.3.10
|
||||
@@ -705,14 +705,14 @@ importers:
|
||||
specifier: 16.3.1
|
||||
version: 16.3.1
|
||||
eslint:
|
||||
specifier: 8.55.0
|
||||
version: 8.55.0
|
||||
specifier: 8.57.0
|
||||
version: 8.57.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 5.0.1
|
||||
version: 5.0.1(eslint@8.55.0)(prettier@3.1.0)
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(eslint@8.57.0)(prettier@3.1.0)
|
||||
eslint-plugin-vue:
|
||||
specifier: 9.19.2
|
||||
version: 9.19.2(eslint@8.55.0)
|
||||
specifier: 9.24.0
|
||||
version: 9.24.0(eslint@8.57.0)
|
||||
glob:
|
||||
specifier: 10.3.10
|
||||
version: 10.3.10
|
||||
@@ -757,7 +757,7 @@ importers:
|
||||
version: 4.5.0(@types/node@17.0.27)(sass@1.69.5)(terser@5.27.0)
|
||||
vite-plugin-checker:
|
||||
specifier: 0.6.2
|
||||
version: 0.6.2(eslint@8.55.0)(typescript@5.3.2)(vite@4.5.0)(vue-tsc@1.8.24)
|
||||
version: 0.6.2(eslint@8.57.0)(typescript@5.3.2)(vite@4.5.0)(vue-tsc@1.8.24)
|
||||
vite-plugin-fonts:
|
||||
specifier: 0.7.0
|
||||
version: 0.7.0(vite@4.5.0)
|
||||
@@ -1259,7 +1259,7 @@ importers:
|
||||
version: 3.1.1(graphql@16.6.0)
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.1.3
|
||||
version: 0.1.3(eslint@8.55.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9)
|
||||
version: 0.1.3(eslint@8.57.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9)
|
||||
'@hoppscotch/vue-toasted':
|
||||
specifier: 0.1.0
|
||||
version: 0.1.0(vue@3.3.9)
|
||||
@@ -3997,6 +3997,15 @@ packages:
|
||||
eslint: 8.55.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
/@eslint-community/regexpp@4.10.0:
|
||||
resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
@@ -4043,6 +4052,10 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@eslint/js@8.57.0:
|
||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
/@faker-js/faker@5.5.3:
|
||||
resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==}
|
||||
dev: false
|
||||
@@ -5998,32 +6011,6 @@ packages:
|
||||
dependencies:
|
||||
graphql: 16.8.1
|
||||
|
||||
/@hoppscotch/ui@0.1.0(eslint@8.55.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-+4iHdfO7gRn7l3vpnPcQZbgdA+uE/K1KQX0/eUFcCWvja/C3eSM0db31MRX2cz1KYGwiezzhhVe21mIT4a0CZQ==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
vue: 3.3.9
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.3.9)
|
||||
'@fontsource-variable/inter': 5.0.15
|
||||
'@fontsource-variable/material-symbols-rounded': 5.0.16
|
||||
'@fontsource-variable/roboto-mono': 5.0.16
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.3.9)
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.27.0)(vite@3.2.4)
|
||||
'@vueuse/core': 8.7.5(vue@3.3.9)
|
||||
fp-ts: 2.16.2
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.55.0)(vite@3.2.4)
|
||||
vue: 3.3.9(typescript@4.9.5)
|
||||
vuedraggable-es: 4.1.1(vue@3.3.9)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- vite
|
||||
dev: true
|
||||
|
||||
/@hoppscotch/ui@0.1.0(eslint@8.55.0)(terser@5.27.0)(vite@4.5.0)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-+4iHdfO7gRn7l3vpnPcQZbgdA+uE/K1KQX0/eUFcCWvja/C3eSM0db31MRX2cz1KYGwiezzhhVe21mIT4a0CZQ==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -6050,7 +6037,59 @@ packages:
|
||||
- vite
|
||||
dev: false
|
||||
|
||||
/@hoppscotch/ui@0.1.3(eslint@8.55.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9):
|
||||
/@hoppscotch/ui@0.1.0(eslint@8.57.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-+4iHdfO7gRn7l3vpnPcQZbgdA+uE/K1KQX0/eUFcCWvja/C3eSM0db31MRX2cz1KYGwiezzhhVe21mIT4a0CZQ==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
vue: 3.3.9
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.3.9)
|
||||
'@fontsource-variable/inter': 5.0.15
|
||||
'@fontsource-variable/material-symbols-rounded': 5.0.16
|
||||
'@fontsource-variable/roboto-mono': 5.0.16
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.3.9)
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.27.0)(vite@3.2.4)
|
||||
'@vueuse/core': 8.7.5(vue@3.3.9)
|
||||
fp-ts: 2.16.2
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@3.2.4)
|
||||
vue: 3.3.9(typescript@4.9.5)
|
||||
vuedraggable-es: 4.1.1(vue@3.3.9)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- vite
|
||||
dev: true
|
||||
|
||||
/@hoppscotch/ui@0.1.0(eslint@8.57.0)(terser@5.27.0)(vite@4.5.0)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-+4iHdfO7gRn7l3vpnPcQZbgdA+uE/K1KQX0/eUFcCWvja/C3eSM0db31MRX2cz1KYGwiezzhhVe21mIT4a0CZQ==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
vue: 3.3.9
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.3.9)
|
||||
'@fontsource-variable/inter': 5.0.15
|
||||
'@fontsource-variable/material-symbols-rounded': 5.0.16
|
||||
'@fontsource-variable/roboto-mono': 5.0.16
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.3.9)
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.27.0)(vite@4.5.0)
|
||||
'@vueuse/core': 8.7.5(vue@3.3.9)
|
||||
fp-ts: 2.16.2
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@4.5.0)
|
||||
vue: 3.3.9(typescript@5.3.2)
|
||||
vuedraggable-es: 4.1.1(vue@3.3.9)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- vite
|
||||
dev: false
|
||||
|
||||
/@hoppscotch/ui@0.1.3(eslint@8.57.0)(terser@5.27.0)(vite@3.2.4)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-a1dmqqL+zS2P6cxkCBLdBtd+mD+MnCDSN63TrCPldW5W92rtqpeZ0bmGgiQlzfA2457JRktYpVCBR0Oc0J1jbA==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
@@ -6066,7 +6105,7 @@ packages:
|
||||
fp-ts: 2.16.2
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.55.0)(vite@3.2.4)
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@3.2.4)
|
||||
vue: 3.3.9(typescript@4.9.3)
|
||||
vuedraggable-es: 4.1.1(vue@3.3.9)
|
||||
transitivePeerDependencies:
|
||||
@@ -7494,6 +7533,11 @@ packages:
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@pkgr/core@0.1.1:
|
||||
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@pkgr/utils@2.4.2:
|
||||
resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
@@ -8806,6 +8850,64 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 6.13.2(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/scope-manager': 6.13.2
|
||||
'@typescript-eslint/type-utils': 6.13.2(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/utils': 6.13.2(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/visitor-keys': 6.13.2
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.0
|
||||
natural-compare: 1.4.0
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^7.0.0
|
||||
eslint: ^8.56.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/scope-manager': 7.3.1
|
||||
'@typescript-eslint/type-utils': 7.3.1(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/visitor-keys': 7.3.1
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.0
|
||||
natural-compare: 1.4.0
|
||||
semver: 7.6.0
|
||||
ts-api-utils: 1.0.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@5.30.6(eslint@8.19.0)(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -8887,6 +8989,48 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@6.13.2(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 6.13.2
|
||||
'@typescript-eslint/types': 6.13.2
|
||||
'@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.2)
|
||||
'@typescript-eslint/visitor-keys': 6.13.2
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.3.1
|
||||
'@typescript-eslint/types': 7.3.1
|
||||
'@typescript-eslint/typescript-estree': 7.3.1(typescript@5.3.2)
|
||||
'@typescript-eslint/visitor-keys': 7.3.1
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/scope-manager@5.30.6:
|
||||
resolution: {integrity: sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -8919,6 +9063,14 @@ packages:
|
||||
'@typescript-eslint/visitor-keys': 6.13.2
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/scope-manager@7.3.1:
|
||||
resolution: {integrity: sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.3.1
|
||||
'@typescript-eslint/visitor-keys': 7.3.1
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@5.30.6(eslint@8.19.0)(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -8998,6 +9150,46 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@6.13.2(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.2)
|
||||
'@typescript-eslint/utils': 6.13.2(eslint@8.57.0)(typescript@5.3.2)
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
ts-api-utils: 1.0.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@7.3.1(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.3.1(typescript@5.3.2)
|
||||
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.3.2)
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
ts-api-utils: 1.0.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types@5.30.6:
|
||||
resolution: {integrity: sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -9018,6 +9210,11 @@ packages:
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types@7.3.1:
|
||||
resolution: {integrity: sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@5.30.6(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -9102,6 +9299,28 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@7.3.1(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.3.1
|
||||
'@typescript-eslint/visitor-keys': 7.3.1
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.3
|
||||
semver: 7.6.0
|
||||
ts-api-utils: 1.0.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@5.30.6(eslint@8.19.0)(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -9179,6 +9398,44 @@ packages:
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@6.13.2(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.0
|
||||
'@typescript-eslint/scope-manager': 6.13.2
|
||||
'@typescript-eslint/types': 6.13.2
|
||||
'@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.2)
|
||||
eslint: 8.57.0
|
||||
semver: 7.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@7.3.1(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.0
|
||||
'@typescript-eslint/scope-manager': 7.3.1
|
||||
'@typescript-eslint/types': 7.3.1
|
||||
'@typescript-eslint/typescript-estree': 7.3.1(typescript@5.3.2)
|
||||
eslint: 8.57.0
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys@5.30.6:
|
||||
resolution: {integrity: sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -9211,6 +9468,14 @@ packages:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys@7.3.1:
|
||||
resolution: {integrity: sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.3.1
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@ungap/promise-all-settled@1.1.2:
|
||||
resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
|
||||
dev: true
|
||||
@@ -9695,6 +9960,27 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vue/eslint-config-typescript@12.0.0(eslint-plugin-vue@9.24.0)(eslint@8.57.0)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
|
||||
eslint-plugin-vue: ^9.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.57.0)(typescript@5.3.2)
|
||||
'@typescript-eslint/parser': 6.13.2(eslint@8.57.0)(typescript@5.3.2)
|
||||
eslint: 8.57.0
|
||||
eslint-plugin-vue: 9.24.0(eslint@8.57.0)
|
||||
typescript: 5.3.2
|
||||
vue-eslint-parser: 9.3.1(eslint@8.57.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vue/language-core@1.8.24(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-2ClHvij0WlsDWryPzXJCSpPc6rusZFNoVtRZGgGGkKCmKuIREDDKmH8j+1tYyxPYyH0qL6pZ6+IHD8KIm5nWAw==}
|
||||
peerDependencies:
|
||||
@@ -13154,26 +13440,6 @@ packages:
|
||||
prettier-linter-helpers: 1.0.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.0.1(eslint@8.55.0)(prettier@3.1.0):
|
||||
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.55.0
|
||||
prettier: 3.1.0
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.0.1(eslint@8.55.0)(prettier@3.2.5):
|
||||
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -13194,6 +13460,26 @@ packages:
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.1.3(eslint@8.57.0)(prettier@3.1.0):
|
||||
resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
prettier: 3.1.0
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.8.8
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-vue@9.17.0(eslint@8.47.0):
|
||||
resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
@@ -13230,6 +13516,25 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-vue@9.24.0(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
eslint: 8.57.0
|
||||
globals: 13.24.0
|
||||
natural-compare: 1.4.0
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 6.0.16
|
||||
semver: 7.6.0
|
||||
vue-eslint-parser: 9.4.2(eslint@8.57.0)
|
||||
xml-name-validator: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-scope@5.1.1:
|
||||
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -13470,6 +13775,52 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/eslint@8.57.0:
|
||||
resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@eslint/eslintrc': 2.1.4
|
||||
'@eslint/js': 8.57.0
|
||||
'@humanwhocodes/config-array': 0.11.14
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
'@ungap/structured-clone': 1.2.0
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
file-entry-cache: 6.0.1
|
||||
find-up: 5.0.0
|
||||
glob-parent: 6.0.2
|
||||
globals: 13.24.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.0
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
is-path-inside: 3.0.3
|
||||
js-yaml: 4.1.0
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
levn: 0.4.1
|
||||
lodash.merge: 4.6.2
|
||||
minimatch: 3.1.2
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.3
|
||||
strip-ansi: 6.0.1
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/espree@6.2.1:
|
||||
resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -19045,6 +19396,14 @@ packages:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
/postcss-selector-parser@6.0.16:
|
||||
resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
@@ -20212,6 +20571,14 @@ packages:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
/semver@7.6.0:
|
||||
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/send@0.18.0:
|
||||
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -21018,6 +21385,14 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/synckit@0.8.8:
|
||||
resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@pkgr/core': 0.1.1
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/systemjs@6.14.2:
|
||||
resolution: {integrity: sha512-1TlOwvKWdXxAY9vba+huLu99zrQURDWA8pUTYsRIYDZYQbGyK+pyEP4h4dlySsqo7ozyJBmYD20F+iUHhAltEg==}
|
||||
|
||||
@@ -22579,7 +22954,7 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-plugin-checker@0.6.2(eslint@8.55.0)(typescript@5.3.2)(vite@4.5.0)(vue-tsc@1.8.24):
|
||||
/vite-plugin-checker@0.6.2(eslint@8.57.0)(typescript@5.3.2)(vite@4.5.0)(vue-tsc@1.8.24):
|
||||
resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
peerDependencies:
|
||||
@@ -22615,7 +22990,7 @@ packages:
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.5.3
|
||||
commander: 8.3.0
|
||||
eslint: 8.55.0
|
||||
eslint: 8.57.0
|
||||
fast-glob: 3.3.2
|
||||
fs-extra: 11.1.1
|
||||
lodash.debounce: 4.0.8
|
||||
@@ -22633,18 +23008,6 @@ packages:
|
||||
vue-tsc: 1.8.24(typescript@5.3.2)
|
||||
dev: true
|
||||
|
||||
/vite-plugin-eslint@1.8.1(eslint@8.55.0)(vite@3.2.4):
|
||||
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
|
||||
peerDependencies:
|
||||
eslint: '>=7'
|
||||
vite: '>=2'
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
'@types/eslint': 8.56.2
|
||||
eslint: 8.55.0
|
||||
rollup: 2.79.1
|
||||
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.27.0)
|
||||
|
||||
/vite-plugin-eslint@1.8.1(eslint@8.55.0)(vite@4.5.0):
|
||||
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
|
||||
peerDependencies:
|
||||
@@ -22658,6 +23021,31 @@ packages:
|
||||
vite: 4.5.0(@types/node@17.0.27)(sass@1.69.5)(terser@5.27.0)
|
||||
dev: false
|
||||
|
||||
/vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@3.2.4):
|
||||
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
|
||||
peerDependencies:
|
||||
eslint: '>=7'
|
||||
vite: '>=2'
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
'@types/eslint': 8.56.2
|
||||
eslint: 8.57.0
|
||||
rollup: 2.79.1
|
||||
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.27.0)
|
||||
|
||||
/vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@4.5.0):
|
||||
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
|
||||
peerDependencies:
|
||||
eslint: '>=7'
|
||||
vite: '>=2'
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
'@types/eslint': 8.56.2
|
||||
eslint: 8.57.0
|
||||
rollup: 2.79.1
|
||||
vite: 4.5.0(@types/node@17.0.27)(sass@1.69.5)(terser@5.27.0)
|
||||
dev: false
|
||||
|
||||
/vite-plugin-fonts@0.7.0(vite@4.5.0):
|
||||
resolution: {integrity: sha512-fisKirkQrA2RFwcyI96SENLu1FyRYNIiC/l5DGdD8oV3OsAWGrYKs0e7/VZF6l0rm0QiYA2sOVTzYfrLAzP9cw==}
|
||||
deprecated: renamed to `unplugin-fonts`, see https://github.com/cssninjaStudio/unplugin-fonts/releases/tag/v1.0.0
|
||||
@@ -23373,7 +23761,7 @@ packages:
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
semver: 7.5.4
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -23391,7 +23779,43 @@ packages:
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
semver: 7.5.4
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/vue-eslint-parser@9.3.1(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/vue-eslint-parser@9.4.2(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@9.2.2)
|
||||
eslint: 8.57.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user