Compare commits
10 Commits
feat/reque
...
fix/safari
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a870725abf | ||
|
|
cd7429dd24 | ||
|
|
e6cc235532 | ||
|
|
6a65eb03e7 | ||
|
|
c66ea66689 | ||
|
|
8b0f337280 | ||
|
|
2e8a04fbd6 | ||
|
|
7e1ebd5b43 | ||
|
|
1eb5e02100 | ||
|
|
6198a25bc6 |
@@ -112,7 +112,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: dev
|
target: prod
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
restart: always
|
restart: always
|
||||||
@@ -122,7 +122,7 @@ services:
|
|||||||
- PORT=3000
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
- ./packages/hoppscotch-backend/:/usr/src/app
|
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.12.6",
|
"version": "2023.12.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -34,14 +34,12 @@
|
|||||||
"@nestjs/jwt": "^10.1.1",
|
"@nestjs/jwt": "^10.1.1",
|
||||||
"@nestjs/passport": "^10.0.2",
|
"@nestjs/passport": "^10.0.2",
|
||||||
"@nestjs/platform-express": "^10.2.6",
|
"@nestjs/platform-express": "^10.2.6",
|
||||||
"@nestjs/schedule": "^4.0.1",
|
|
||||||
"@nestjs/throttler": "^5.0.0",
|
"@nestjs/throttler": "^5.0.0",
|
||||||
"@prisma/client": "^5.8.0",
|
"@prisma/client": "^5.8.0",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cron": "^3.1.6",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"fp-ts": "^2.13.1",
|
"fp-ts": "^2.13.1",
|
||||||
@@ -59,7 +57,6 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
"posthog-node": "^3.6.3",
|
|
||||||
"prisma": "^5.8.0",
|
"prisma": "^5.8.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE
|
|
||||||
"TeamCollection"
|
|
||||||
ADD
|
|
||||||
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE
|
|
||||||
"TeamRequest"
|
|
||||||
ADD
|
|
||||||
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "TeamCollection_textSearch_idx" ON "TeamCollection" USING GIN (titleSearch);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "TeamRequest_textSearch_idx" ON "TeamRequest" USING GIN (titleSearch);
|
|
||||||
@@ -27,7 +27,9 @@ import {
|
|||||||
} from './input-types.args';
|
} from './input-types.args';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { UserDeletionResult } from 'src/user/user.model';
|
import { User } from 'src/user/user.model';
|
||||||
|
import { PaginationArgs } from 'src/types/input-types.args';
|
||||||
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Admin)
|
@Resolver(() => Admin)
|
||||||
@@ -47,6 +49,203 @@ export class AdminResolver {
|
|||||||
return admin;
|
return admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [User], {
|
||||||
|
description: 'Returns a list of all admin users in infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async admins() {
|
||||||
|
const admins = await this.adminService.fetchAdmins();
|
||||||
|
return admins;
|
||||||
|
}
|
||||||
|
@ResolveField(() => User, {
|
||||||
|
description: 'Returns a user info by UID',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async userInfo(
|
||||||
|
@Args({
|
||||||
|
name: 'userUid',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'The user UID',
|
||||||
|
})
|
||||||
|
userUid: string,
|
||||||
|
): Promise<AuthUser> {
|
||||||
|
const user = await this.adminService.fetchUserInfo(userUid);
|
||||||
|
if (E.isLeft(user)) throwErr(user.left);
|
||||||
|
return user.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [User], {
|
||||||
|
description: 'Returns a list of all the users in infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async allUsers(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args() args: PaginationArgs,
|
||||||
|
): Promise<AuthUser[]> {
|
||||||
|
const users = await this.adminService.fetchUsers(args.cursor, args.take);
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [InvitedUser], {
|
||||||
|
description: 'Returns a list of all the invited users',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||||
|
const users = await this.adminService.fetchInvitedUsers();
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [Team], {
|
||||||
|
description: 'Returns a list of all the teams in the infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async allTeams(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args() args: PaginationArgs,
|
||||||
|
): Promise<Team[]> {
|
||||||
|
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
@ResolveField(() => Team, {
|
||||||
|
description: 'Returns a team info by ID when requested by Admin',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async teamInfo(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which info to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<Team> {
|
||||||
|
const team = await this.adminService.getTeamInfo(teamID);
|
||||||
|
if (E.isLeft(team)) throwErr(team.left);
|
||||||
|
return team.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the members in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async membersCountInTeam(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
|
||||||
|
return teamMembersCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored collections in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async collectionCountInTeam(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
|
||||||
|
return teamCollCount;
|
||||||
|
}
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored requests in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async requestCountInTeam(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
|
||||||
|
return teamReqCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored environments in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async environmentCountInTeam(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const envsCount = await this.adminService.environmentCountInTeam(teamID);
|
||||||
|
return envsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [TeamInvitation], {
|
||||||
|
description: 'Return all the pending invitations in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async pendingInvitationCountInTeam(
|
||||||
|
@Parent() admin: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
) {
|
||||||
|
const invitations = await this.adminService.pendingInvitationCountInTeam(
|
||||||
|
teamID,
|
||||||
|
);
|
||||||
|
return invitations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Users in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async usersCount() {
|
||||||
|
return this.adminService.getUsersCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Teams in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async teamsCount() {
|
||||||
|
return this.adminService.getTeamsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Team Collections in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async teamCollectionsCount() {
|
||||||
|
return this.adminService.getTeamCollectionsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Team Requests in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
|
})
|
||||||
|
async teamRequestsCount() {
|
||||||
|
return this.adminService.getTeamRequestsCount();
|
||||||
|
}
|
||||||
|
|
||||||
/* Mutations */
|
/* Mutations */
|
||||||
|
|
||||||
@Mutation(() => InvitedUser, {
|
@Mutation(() => InvitedUser, {
|
||||||
@@ -70,26 +269,8 @@ export class AdminResolver {
|
|||||||
return invitedUser.right;
|
return invitedUser.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Revoke a user invites by invitee emails',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async revokeUserInvitationsByAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeEmails',
|
|
||||||
description: 'Invitee Emails',
|
|
||||||
type: () => [String],
|
|
||||||
})
|
|
||||||
inviteeEmails: string[],
|
|
||||||
): Promise<boolean> {
|
|
||||||
const invite = await this.adminService.revokeUserInvitations(inviteeEmails);
|
|
||||||
if (E.isLeft(invite)) throwErr(invite.left);
|
|
||||||
return invite.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Delete an user account from infra',
|
description: 'Delete an user account from infra',
|
||||||
deprecationReason: 'Use removeUsersByAdmin instead',
|
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async removeUserByAdmin(
|
async removeUserByAdmin(
|
||||||
@@ -100,33 +281,12 @@ export class AdminResolver {
|
|||||||
})
|
})
|
||||||
userUID: string,
|
userUID: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const removedUser = await this.adminService.removeUserAccount(userUID);
|
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
||||||
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||||
return removedUser.right;
|
return invitedUser.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => [UserDeletionResult], {
|
|
||||||
description: 'Delete user accounts from infra',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async removeUsersByAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'userUIDs',
|
|
||||||
description: 'users UID',
|
|
||||||
type: () => [ID],
|
|
||||||
})
|
|
||||||
userUIDs: string[],
|
|
||||||
): Promise<UserDeletionResult[]> {
|
|
||||||
const deletionResults = await this.adminService.removeUserAccounts(
|
|
||||||
userUIDs,
|
|
||||||
);
|
|
||||||
if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
|
|
||||||
return deletionResults.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Make user an admin',
|
description: 'Make user an admin',
|
||||||
deprecationReason: 'Use makeUsersAdmin instead',
|
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async makeUserAdmin(
|
async makeUserAdmin(
|
||||||
@@ -142,51 +302,8 @@ export class AdminResolver {
|
|||||||
return admin.right;
|
return admin.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Make users an admin',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async makeUsersAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'userUIDs',
|
|
||||||
description: 'users UID',
|
|
||||||
type: () => [ID],
|
|
||||||
})
|
|
||||||
userUIDs: string[],
|
|
||||||
): Promise<boolean> {
|
|
||||||
const isUpdated = await this.adminService.makeUsersAdmin(userUIDs);
|
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
|
||||||
return isUpdated.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Update user display name',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async updateUserDisplayNameByAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'userUID',
|
|
||||||
description: 'users UID',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
userUID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'displayName',
|
|
||||||
description: 'users display name',
|
|
||||||
})
|
|
||||||
displayName: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const isUpdated = await this.adminService.updateUserDisplayName(
|
|
||||||
userUID,
|
|
||||||
displayName,
|
|
||||||
);
|
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
|
||||||
return isUpdated.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Remove user as admin',
|
description: 'Remove user as admin',
|
||||||
deprecationReason: 'Use demoteUsersByAdmin instead',
|
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async removeUserAsAdmin(
|
async removeUserAsAdmin(
|
||||||
@@ -202,23 +319,6 @@ export class AdminResolver {
|
|||||||
return admin.right;
|
return admin.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Remove users as admin',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async demoteUsersByAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'userUIDs',
|
|
||||||
description: 'users UID',
|
|
||||||
type: () => [ID],
|
|
||||||
})
|
|
||||||
userUIDs: string[],
|
|
||||||
): Promise<boolean> {
|
|
||||||
const isUpdated = await this.adminService.demoteUsersByAdmin(userUIDs);
|
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
|
||||||
return isUpdated.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Team, {
|
@Mutation(() => Team, {
|
||||||
description:
|
description:
|
||||||
'Create a new team by providing the user uid to nominate as Team owner',
|
'Create a new team by providing the user uid to nominate as Team owner',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { mockDeep } from 'jest-mock-extended';
|
import { mockDeep } from 'jest-mock-extended';
|
||||||
import { InvitedUsers, User as DbUser } from '@prisma/client';
|
import { InvitedUsers } from '@prisma/client';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { TeamService } from '../team/team.service';
|
import { TeamService } from '../team/team.service';
|
||||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||||
@@ -13,15 +13,10 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
import {
|
import {
|
||||||
DUPLICATE_EMAIL,
|
DUPLICATE_EMAIL,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_INVITATION_DELETION_FAILED,
|
|
||||||
USER_NOT_FOUND,
|
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -63,87 +58,20 @@ const invitedUsers: InvitedUsers[] = [
|
|||||||
invitedOn: new Date(),
|
invitedOn: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dbAdminUsers: DbUser[] = [
|
|
||||||
{
|
|
||||||
uid: 'uid 1',
|
|
||||||
displayName: 'displayName',
|
|
||||||
email: 'email@email.com',
|
|
||||||
photoURL: 'photoURL',
|
|
||||||
isAdmin: true,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: '',
|
|
||||||
currentGQLSession: '',
|
|
||||||
createdOn: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uid: 'uid 2',
|
|
||||||
displayName: 'displayName',
|
|
||||||
email: 'email@email.com',
|
|
||||||
photoURL: 'photoURL',
|
|
||||||
isAdmin: true,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: '',
|
|
||||||
currentGQLSession: '',
|
|
||||||
createdOn: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const dbNonAminUser: DbUser = {
|
|
||||||
uid: 'uid 3',
|
|
||||||
displayName: 'displayName',
|
|
||||||
email: 'email@email.com',
|
|
||||||
photoURL: 'photoURL',
|
|
||||||
isAdmin: false,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: '',
|
|
||||||
currentGQLSession: '',
|
|
||||||
createdOn: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AdminService', () => {
|
describe('AdminService', () => {
|
||||||
describe('fetchInvitedUsers', () => {
|
describe('fetchInvitedUsers', () => {
|
||||||
test('should resolve right and apply pagination correctly', async () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
|
||||||
// @ts-ignore
|
|
||||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
|
||||||
|
|
||||||
const paginationArgs: OffsetPaginationArgs = { take: 5, skip: 2 };
|
|
||||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
|
||||||
|
|
||||||
expect(mockPrisma.invitedUsers.findMany).toHaveBeenCalledWith({
|
|
||||||
...paginationArgs,
|
|
||||||
orderBy: {
|
|
||||||
invitedOn: 'desc',
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
NOT: {
|
|
||||||
inviteeEmail: {
|
|
||||||
in: [dbAdminUsers[0].email],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('should resolve right and return an array of invited users', async () => {
|
test('should resolve right and return an array of invited users', async () => {
|
||||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
|
||||||
// @ts-ignore
|
|
||||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||||
|
|
||||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
const results = await adminService.fetchInvitedUsers();
|
||||||
expect(results).toEqual(invitedUsers);
|
expect(results).toEqual(invitedUsers);
|
||||||
});
|
});
|
||||||
test('should resolve left and return an empty array if invited users not found', async () => {
|
test('should resolve left and return an empty array if invited users not found', async () => {
|
||||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
|
||||||
|
|
||||||
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
const results = await adminService.fetchInvitedUsers();
|
||||||
expect(results).toEqual([]);
|
expect(results).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -206,58 +134,6 @@ describe('AdminService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('revokeUserInvitations', () => {
|
|
||||||
test('should resolve left and return error if email not invited', async () => {
|
|
||||||
mockPrisma.invitedUsers.deleteMany.mockRejectedValueOnce(
|
|
||||||
'RecordNotFound',
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.revokeUserInvitations([
|
|
||||||
'test@gmail.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(USER_INVITATION_DELETION_FAILED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve right and return deleted invitee email', async () => {
|
|
||||||
const adminUid = 'adminUid';
|
|
||||||
mockPrisma.invitedUsers.deleteMany.mockResolvedValueOnce({ count: 1 });
|
|
||||||
|
|
||||||
const result = await adminService.revokeUserInvitations([
|
|
||||||
invitedUsers[0].inviteeEmail,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
|
||||||
where: {
|
|
||||||
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(result).toEqualRight(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeUsersAsAdmin', () => {
|
|
||||||
test('should resolve right and make admins to users', async () => {
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
|
||||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
|
||||||
|
|
||||||
return expect(
|
|
||||||
await adminService.demoteUsersByAdmin([dbAdminUsers[0].uid]),
|
|
||||||
).toEqualRight(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve left and return error if only one admin in the infra', async () => {
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
|
||||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
|
||||||
|
|
||||||
return expect(
|
|
||||||
await adminService.demoteUsersByAdmin(
|
|
||||||
dbAdminUsers.map((user) => user.uid),
|
|
||||||
),
|
|
||||||
).toEqualLeft(ONLY_ONE_ADMIN_ACCOUNT);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUsersCount', () => {
|
describe('getUsersCount', () => {
|
||||||
test('should return count of all users in the organization', async () => {
|
test('should return count of all users in the organization', async () => {
|
||||||
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ import * as E from 'fp-ts/Either';
|
|||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { validateEmail } from '../utils';
|
import { validateEmail } from '../utils';
|
||||||
import {
|
import {
|
||||||
ADMIN_CAN_NOT_BE_DELETED,
|
|
||||||
DUPLICATE_EMAIL,
|
DUPLICATE_EMAIL,
|
||||||
EMAIL_FAILED,
|
EMAIL_FAILED,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
USERS_NOT_FOUND,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_INVITATION_DELETION_FAILED,
|
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
@@ -29,8 +26,6 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
|||||||
import { TeamMemberRole } from '../team/team.model';
|
import { TeamMemberRole } from '../team/team.model';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
|
||||||
import { UserDeletionResult } from 'src/user/user.model';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -53,30 +48,12 @@ export class AdminService {
|
|||||||
* @param cursorID Users uid
|
* @param cursorID Users uid
|
||||||
* @param take number of users to fetch
|
* @param take number of users to fetch
|
||||||
* @returns an Either of array of user or error
|
* @returns an Either of array of user or error
|
||||||
* @deprecated use fetchUsersV2 instead
|
|
||||||
*/
|
*/
|
||||||
async fetchUsers(cursorID: string, take: number) {
|
async fetchUsers(cursorID: string, take: number) {
|
||||||
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
||||||
return allUsers;
|
return allUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all the users in the infra.
|
|
||||||
* @param searchString search on users displayName or email
|
|
||||||
* @param paginationOption pagination options
|
|
||||||
* @returns an Either of array of user or error
|
|
||||||
*/
|
|
||||||
async fetchUsersV2(
|
|
||||||
searchString: string,
|
|
||||||
paginationOption: OffsetPaginationArgs,
|
|
||||||
) {
|
|
||||||
const allUsers = await this.userService.fetchAllUsersV2(
|
|
||||||
searchString,
|
|
||||||
paginationOption,
|
|
||||||
);
|
|
||||||
return allUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite a user to join the infra.
|
* Invite a user to join the infra.
|
||||||
* @param adminUID Admin's UID
|
* @param adminUID Admin's UID
|
||||||
@@ -133,68 +110,14 @@ export class AdminService {
|
|||||||
return E.right(invitedUser);
|
return E.right(invitedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the display name of a user
|
|
||||||
* @param userUid Who's display name is being updated
|
|
||||||
* @param displayName New display name of the user
|
|
||||||
* @returns an Either of boolean or error
|
|
||||||
*/
|
|
||||||
async updateUserDisplayName(userUid: string, displayName: string) {
|
|
||||||
const updatedUser = await this.userService.updateUserDisplayName(
|
|
||||||
userUid,
|
|
||||||
displayName,
|
|
||||||
);
|
|
||||||
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke infra level user invitations
|
|
||||||
* @param inviteeEmails Invitee's emails
|
|
||||||
* @param adminUid Admin Uid
|
|
||||||
* @returns an Either of boolean or error string
|
|
||||||
*/
|
|
||||||
async revokeUserInvitations(inviteeEmails: string[]) {
|
|
||||||
try {
|
|
||||||
await this.prisma.invitedUsers.deleteMany({
|
|
||||||
where: {
|
|
||||||
inviteeEmail: { in: inviteeEmails },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return E.right(true);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(USER_INVITATION_DELETION_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the list of invited users by the admin.
|
* Fetch the list of invited users by the admin.
|
||||||
* @returns an Either of array of `InvitedUser` object or error
|
* @returns an Either of array of `InvitedUser` object or error
|
||||||
*/
|
*/
|
||||||
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
async fetchInvitedUsers() {
|
||||||
const userEmailObjs = await this.prisma.user.findMany({
|
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
|
const users: InvitedUser[] = invitedUsers.map(
|
||||||
take: paginationOption.take,
|
|
||||||
skip: paginationOption.skip,
|
|
||||||
orderBy: {
|
|
||||||
invitedOn: 'desc',
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
NOT: {
|
|
||||||
inviteeEmail: {
|
|
||||||
in: userEmailObjs.map((user) => user.email),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const users: InvitedUser[] = pendingInvitedUsers.map(
|
|
||||||
(user) => <InvitedUser>{ ...user },
|
(user) => <InvitedUser>{ ...user },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -414,7 +337,6 @@ export class AdminService {
|
|||||||
* Remove a user account by UID
|
* Remove a user account by UID
|
||||||
* @param userUid User UID
|
* @param userUid User UID
|
||||||
* @returns an Either of boolean or error
|
* @returns an Either of boolean or error
|
||||||
* @deprecated use removeUserAccounts instead
|
|
||||||
*/
|
*/
|
||||||
async removeUserAccount(userUid: string) {
|
async removeUserAccount(userUid: string) {
|
||||||
const user = await this.userService.findUserById(userUid);
|
const user = await this.userService.findUserById(userUid);
|
||||||
@@ -427,73 +349,10 @@ export class AdminService {
|
|||||||
return E.right(delUser.right);
|
return E.right(delUser.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove user (not Admin) accounts by UIDs
|
|
||||||
* @param userUIDs User UIDs
|
|
||||||
* @returns an Either of boolean or error
|
|
||||||
*/
|
|
||||||
async removeUserAccounts(userUIDs: string[]) {
|
|
||||||
const userDeleteResult: UserDeletionResult[] = [];
|
|
||||||
|
|
||||||
// step 1: fetch all users
|
|
||||||
const allUsersList = await this.userService.findUsersByIds(userUIDs);
|
|
||||||
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
|
|
||||||
|
|
||||||
// step 2: admin user can not be deleted without removing admin status/role
|
|
||||||
allUsersList.forEach((user) => {
|
|
||||||
if (user.isAdmin) {
|
|
||||||
userDeleteResult.push({
|
|
||||||
userUID: user.uid,
|
|
||||||
isDeleted: false,
|
|
||||||
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
|
|
||||||
let deletedUserEmails: string[] = [];
|
|
||||||
|
|
||||||
// step 3: delete non-admin users
|
|
||||||
const deletionPromises = nonAdminUsers.map((user) => {
|
|
||||||
return this.userService
|
|
||||||
.deleteUserByUID(user)()
|
|
||||||
.then((res) => {
|
|
||||||
if (E.isLeft(res)) {
|
|
||||||
return {
|
|
||||||
userUID: user.uid,
|
|
||||||
isDeleted: false,
|
|
||||||
errorMessage: res.left,
|
|
||||||
} as UserDeletionResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedUserEmails.push(user.email);
|
|
||||||
return {
|
|
||||||
userUID: user.uid,
|
|
||||||
isDeleted: true,
|
|
||||||
errorMessage: null,
|
|
||||||
} as UserDeletionResult;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const promiseResult = await Promise.allSettled(deletionPromises);
|
|
||||||
|
|
||||||
// step 4: revoke all the invites sent to the deleted users
|
|
||||||
await this.revokeUserInvitations(deletedUserEmails);
|
|
||||||
|
|
||||||
// step 5: return the result
|
|
||||||
promiseResult.forEach((result) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
userDeleteResult.push(result.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return E.right(userDeleteResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a user an admin
|
* Make a user an admin
|
||||||
* @param userUid User UID
|
* @param userUid User UID
|
||||||
* @returns an Either of boolean or error
|
* @returns an Either of boolean or error
|
||||||
* @deprecated use makeUsersAdmin instead
|
|
||||||
*/
|
*/
|
||||||
async makeUserAdmin(userUID: string) {
|
async makeUserAdmin(userUID: string) {
|
||||||
const admin = await this.userService.makeAdmin(userUID);
|
const admin = await this.userService.makeAdmin(userUID);
|
||||||
@@ -501,22 +360,10 @@ export class AdminService {
|
|||||||
return E.right(true);
|
return E.right(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make users to admin
|
|
||||||
* @param userUid User UIDs
|
|
||||||
* @returns an Either of boolean or error
|
|
||||||
*/
|
|
||||||
async makeUsersAdmin(userUIDs: string[]) {
|
|
||||||
const isUpdated = await this.userService.makeAdmins(userUIDs);
|
|
||||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove user as admin
|
* Remove user as admin
|
||||||
* @param userUid User UID
|
* @param userUid User UID
|
||||||
* @returns an Either of boolean or error
|
* @returns an Either of boolean or error
|
||||||
* @deprecated use demoteUsersByAdmin instead
|
|
||||||
*/
|
*/
|
||||||
async removeUserAsAdmin(userUID: string) {
|
async removeUserAsAdmin(userUID: string) {
|
||||||
const adminUsers = await this.userService.fetchAdminUsers();
|
const adminUsers = await this.userService.fetchAdminUsers();
|
||||||
@@ -527,26 +374,6 @@ export class AdminService {
|
|||||||
return E.right(true);
|
return E.right(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove users as admin
|
|
||||||
* @param userUIDs User UIDs
|
|
||||||
* @returns an Either of boolean or error
|
|
||||||
*/
|
|
||||||
async demoteUsersByAdmin(userUIDs: string[]) {
|
|
||||||
const adminUsers = await this.userService.fetchAdminUsers();
|
|
||||||
|
|
||||||
const remainingAdmins = adminUsers.filter(
|
|
||||||
(adminUser) => !userUIDs.includes(adminUser.uid),
|
|
||||||
);
|
|
||||||
if (remainingAdmins.length < 1) {
|
|
||||||
return E.left(ONLY_ONE_ADMIN_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdated = await this.userService.removeUsersAsAdmin(userUIDs);
|
|
||||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
|
||||||
return E.right(isUpdated.right);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch list of all the Users in org
|
* Fetch list of all the Users in org
|
||||||
* @returns number of users in the org
|
* @returns number of users in the org
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RESTAdminGuard implements CanActivate {
|
|
||||||
canActivate(context: ExecutionContext): boolean {
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
const user = request.user;
|
|
||||||
|
|
||||||
return user.isAdmin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,7 @@ import { AuthUser } from 'src/types/AuthUser';
|
|||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { Admin } from './admin.model';
|
import { Admin } from './admin.model';
|
||||||
import {
|
import { PaginationArgs } from 'src/types/input-types.args';
|
||||||
OffsetPaginationArgs,
|
|
||||||
PaginationArgs,
|
|
||||||
} from 'src/types/input-types.args';
|
|
||||||
import { InvitedUser } from './invited-user.model';
|
import { InvitedUser } from './invited-user.model';
|
||||||
import { Team } from 'src/team/team.model';
|
import { Team } from 'src/team/team.model';
|
||||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
@@ -32,8 +29,7 @@ import {
|
|||||||
EnableAndDisableSSOArgs,
|
EnableAndDisableSSOArgs,
|
||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from 'src/infra-config/helper';
|
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Infra)
|
@Resolver(() => Infra)
|
||||||
@@ -80,7 +76,6 @@ export class InfraResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [User], {
|
@ResolveField(() => [User], {
|
||||||
description: 'Returns a list of all the users in infra',
|
description: 'Returns a list of all the users in infra',
|
||||||
deprecationReason: 'Use allUsersV2 instead',
|
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
||||||
@@ -88,33 +83,11 @@ export class InfraResolver {
|
|||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => [User], {
|
|
||||||
description: 'Returns a list of all the users in infra',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async allUsersV2(
|
|
||||||
@Args({
|
|
||||||
name: 'searchString',
|
|
||||||
nullable: true,
|
|
||||||
description: 'Search on users displayName or email',
|
|
||||||
})
|
|
||||||
searchString: string,
|
|
||||||
@Args() paginationOption: OffsetPaginationArgs,
|
|
||||||
): Promise<AuthUser[]> {
|
|
||||||
const users = await this.adminService.fetchUsersV2(
|
|
||||||
searchString,
|
|
||||||
paginationOption,
|
|
||||||
);
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ResolveField(() => [InvitedUser], {
|
@ResolveField(() => [InvitedUser], {
|
||||||
description: 'Returns a list of all the invited users',
|
description: 'Returns a list of all the invited users',
|
||||||
})
|
})
|
||||||
async invitedUsers(
|
async invitedUsers(): Promise<InvitedUser[]> {
|
||||||
@Args() args: OffsetPaginationArgs,
|
const users = await this.adminService.fetchInvitedUsers();
|
||||||
): Promise<InvitedUser[]> {
|
|
||||||
const users = await this.adminService.fetchInvitedUsers(args);
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +247,10 @@ export class InfraResolver {
|
|||||||
async infraConfigs(
|
async infraConfigs(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'configNames',
|
name: 'configNames',
|
||||||
type: () => [InfraConfigEnum],
|
type: () => [InfraConfigEnumForClient],
|
||||||
description: 'Configs to fetch',
|
description: 'Configs to fetch',
|
||||||
})
|
})
|
||||||
names: InfraConfigEnum[],
|
names: InfraConfigEnumForClient[],
|
||||||
) {
|
) {
|
||||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||||
@@ -311,25 +284,6 @@ export class InfraResolver {
|
|||||||
return updatedRes.right;
|
return updatedRes.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Enable or disable analytics collection',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async toggleAnalyticsCollection(
|
|
||||||
@Args({
|
|
||||||
name: 'status',
|
|
||||||
type: () => ServiceStatus,
|
|
||||||
description: 'Toggle analytics collection',
|
|
||||||
})
|
|
||||||
analyticsCollectionStatus: ServiceStatus,
|
|
||||||
) {
|
|
||||||
const res = await this.infraConfigService.toggleAnalyticsCollection(
|
|
||||||
analyticsCollectionStatus,
|
|
||||||
);
|
|
||||||
if (E.isLeft(res)) throwErr(res.left);
|
|
||||||
return res.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Reset Infra Configs with default values (.env)',
|
description: 'Reset Infra Configs with default values (.env)',
|
||||||
})
|
})
|
||||||
@@ -352,9 +306,7 @@ export class InfraResolver {
|
|||||||
})
|
})
|
||||||
providerInfo: EnableAndDisableSSOArgs[],
|
providerInfo: EnableAndDisableSSOArgs[],
|
||||||
) {
|
) {
|
||||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
|
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
|
||||||
providerInfo,
|
|
||||||
);
|
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
|||||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||||
import { loadInfraConfiguration } from './infra-config/helper';
|
import { loadInfraConfiguration } from './infra-config/helper';
|
||||||
import { MailerModule } from './mailer/mailer.module';
|
import { MailerModule } from './mailer/mailer.module';
|
||||||
import { PosthogModule } from './posthog/posthog.module';
|
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -98,8 +96,6 @@ import { ScheduleModule } from '@nestjs/schedule';
|
|||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
PosthogModule,
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
|
import {
|
||||||
|
AuthProvider,
|
||||||
|
authCookieHandler,
|
||||||
|
authProviderCheck,
|
||||||
|
throwHTTPErr,
|
||||||
|
} from './helper';
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
@@ -26,7 +31,6 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
|
|||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import { GithubStrategy } from './strategies/github.strategy';
|
|||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import {
|
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||||
isInfraConfigTablePopulated,
|
|
||||||
loadInfraConfiguration,
|
|
||||||
} from 'src/infra-config/helper';
|
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -37,11 +34,6 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
|
|
||||||
if (!isInfraConfigPopulated) {
|
|
||||||
return { module: AuthModule };
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from 'src/types/AuthTokens';
|
} from 'src/types/AuthTokens';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { RESTError } from 'src/types/RESTError';
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
@@ -117,7 +117,7 @@ export class AuthService {
|
|||||||
userUid,
|
userUid,
|
||||||
);
|
);
|
||||||
if (E.isLeft(updatedUser))
|
if (E.isLeft(updatedUser))
|
||||||
return E.left(<RESTError>{
|
return E.left(<AuthError>{
|
||||||
message: updatedUser.left,
|
message: updatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -255,7 +255,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async verifyMagicLinkTokens(
|
async verifyMagicLinkTokens(
|
||||||
magicLinkIDTokens: VerifyMagicDto,
|
magicLinkIDTokens: VerifyMagicDto,
|
||||||
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
|
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
|
||||||
const passwordlessTokens = await this.validatePasswordlessTokens(
|
const passwordlessTokens = await this.validatePasswordlessTokens(
|
||||||
magicLinkIDTokens,
|
magicLinkIDTokens,
|
||||||
);
|
);
|
||||||
@@ -373,7 +373,7 @@ export class AuthService {
|
|||||||
if (usersCount === 1) {
|
if (usersCount === 1) {
|
||||||
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
||||||
if (E.isLeft(elevatedUser))
|
if (E.isLeft(elevatedUser))
|
||||||
return E.left(<RESTError>{
|
return E.left(<AuthError>{
|
||||||
message: elevatedUser.left,
|
message: elevatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
@@ -24,6 +25,15 @@ export enum AuthProvider {
|
|||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows throw to be used as an expression
|
||||||
|
* @param errMessage Message present in the error message
|
||||||
|
*/
|
||||||
|
export function throwHTTPErr(errorData: AuthError): never {
|
||||||
|
const { message, statusCode } = errorData;
|
||||||
|
throw new HttpException(message, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets and returns the cookies in the response object on successful authentication
|
* Sets and returns the cookies in the response object on successful authentication
|
||||||
* @param res Express Response Object
|
* @param res Express Response Object
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
|
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
||||||
scope: [configService.get('INFRA.GITHUB_SCOPE')],
|
scope: [configService.get('GITHUB_SCOPE')],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
|
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
||||||
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
|
scope: configService.get('GOOGLE_SCOPE').split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
|
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
||||||
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
|
scope: [configService.get('MICROSOFT_SCOPE')],
|
||||||
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
|
tenant: configService.get('MICROSOFT_TENANT'),
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ export const DUPLICATE_EMAIL = 'email/both_emails_cannot_be_same' as const;
|
|||||||
export const ONLY_ONE_ADMIN_ACCOUNT =
|
export const ONLY_ONE_ADMIN_ACCOUNT =
|
||||||
'admin/only_one_admin_account_found' as const;
|
'admin/only_one_admin_account_found' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin user can not be deleted
|
|
||||||
* To delete the admin user, first make the Admin user a normal user
|
|
||||||
* (AdminService)
|
|
||||||
*/
|
|
||||||
export const ADMIN_CAN_NOT_BE_DELETED =
|
|
||||||
'admin/admin_can_not_be_deleted' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token Authorization failed (Check 'Authorization' Header)
|
* Token Authorization failed (Check 'Authorization' Header)
|
||||||
* (GqlAuthGuard)
|
* (GqlAuthGuard)
|
||||||
@@ -107,13 +99,6 @@ export const USER_IS_OWNER = 'user/is_owner' as const;
|
|||||||
*/
|
*/
|
||||||
export const USER_IS_ADMIN = 'user/is_admin' as const;
|
export const USER_IS_ADMIN = 'user/is_admin' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* User invite deletion failure error due to invitation not found
|
|
||||||
* (AdminService)
|
|
||||||
*/
|
|
||||||
export const USER_INVITATION_DELETION_FAILED =
|
|
||||||
'user/invitation_deletion_failed' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Teams not found
|
* Teams not found
|
||||||
* (TeamsService)
|
* (TeamsService)
|
||||||
@@ -228,12 +213,6 @@ export const TEAM_COL_NOT_SAME_PARENT =
|
|||||||
export const TEAM_COL_SAME_NEXT_COLL =
|
export const TEAM_COL_SAME_NEXT_COLL =
|
||||||
'team_coll/collection_and_next_collection_are_same';
|
'team_coll/collection_and_next_collection_are_same';
|
||||||
|
|
||||||
/**
|
|
||||||
* Team Collection search failed
|
|
||||||
* (TeamCollectionService)
|
|
||||||
*/
|
|
||||||
export const TEAM_COL_SEARCH_FAILED = 'team_coll/team_collection_search_failed';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Team Collection Re-Ordering Failed
|
* Team Collection Re-Ordering Failed
|
||||||
* (TeamCollectionService)
|
* (TeamCollectionService)
|
||||||
@@ -289,13 +268,6 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
|||||||
export const TEAM_COLL_DATA_INVALID =
|
export const TEAM_COLL_DATA_INVALID =
|
||||||
'team_coll/team_coll_data_invalid' as const;
|
'team_coll/team_coll_data_invalid' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Team Collection parent tree generation failed
|
|
||||||
* (TeamCollectionService)
|
|
||||||
*/
|
|
||||||
export const TEAM_COLL_PARENT_TREE_GEN_FAILED =
|
|
||||||
'team_coll/team_coll_parent_tree_generation_failed';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -321,19 +293,6 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
|
|||||||
*/
|
*/
|
||||||
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Team Request search failed
|
|
||||||
* (TeamRequestService)
|
|
||||||
*/
|
|
||||||
export const TEAM_REQ_SEARCH_FAILED = 'team_req/team_request_search_failed';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Team Request parent tree generation failed
|
|
||||||
* (TeamRequestService)
|
|
||||||
*/
|
|
||||||
export const TEAM_REQ_PARENT_TREE_GEN_FAILED =
|
|
||||||
'team_req/team_req_parent_tree_generation_failed';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No Postmark Sender Email defined
|
* No Postmark Sender Email defined
|
||||||
* (AuthService)
|
* (AuthService)
|
||||||
@@ -731,22 +690,9 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
|||||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||||
'infra_config/service_not_configured' as const;
|
'infra_config/service_not_configured' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Infra Config update/fetch operation not allowed
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
|
|
||||||
'infra_config/operation_not_allowed';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message for when the database table does not exist
|
* Error message for when the database table does not exist
|
||||||
* (InfraConfigService)
|
* (InfraConfigService)
|
||||||
*/
|
*/
|
||||||
export const DATABASE_TABLE_NOT_EXIST =
|
export const DATABASE_TABLE_NOT_EXIST =
|
||||||
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
||||||
|
|
||||||
/**
|
|
||||||
* PostHog client is not initialized
|
|
||||||
* (InfraConfigService)
|
|
||||||
*/
|
|
||||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import {
|
import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
||||||
AUTH_PROVIDER_NOT_CONFIGURED,
|
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
|
|
||||||
export enum ServiceStatus {
|
export enum ServiceStatus {
|
||||||
ENABLE = 'ENABLE',
|
ENABLE = 'ENABLE',
|
||||||
@@ -17,21 +13,14 @@ const AuthProviderConfigurations = {
|
|||||||
[AuthProvider.GOOGLE]: [
|
[AuthProvider.GOOGLE]: [
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
|
||||||
InfraConfigEnum.GOOGLE_SCOPE,
|
|
||||||
],
|
],
|
||||||
[AuthProvider.GITHUB]: [
|
[AuthProvider.GITHUB]: [
|
||||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
|
||||||
InfraConfigEnum.GITHUB_SCOPE,
|
|
||||||
],
|
],
|
||||||
[AuthProvider.MICROSOFT]: [
|
[AuthProvider.MICROSOFT]: [
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
|
||||||
InfraConfigEnum.MICROSOFT_SCOPE,
|
|
||||||
InfraConfigEnum.MICROSOFT_TENANT,
|
|
||||||
],
|
],
|
||||||
[AuthProvider.EMAIL]: [
|
[AuthProvider.EMAIL]: [
|
||||||
InfraConfigEnum.MAILER_SMTP_URL,
|
InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
@@ -64,125 +53,6 @@ export async function loadInfraConfiguration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the default values from .env file and return them as an array
|
|
||||||
* @returns Array of default infra configs
|
|
||||||
*/
|
|
||||||
export async function getDefaultInfraConfigs(): Promise<
|
|
||||||
{ name: InfraConfigEnum; value: string }[]
|
|
||||||
> {
|
|
||||||
const prisma = new PrismaService();
|
|
||||||
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
|
||||||
value: process.env.MAILER_SMTP_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
|
||||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
|
||||||
value: process.env.GOOGLE_CALLBACK_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_SCOPE,
|
|
||||||
value: process.env.GOOGLE_SCOPE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
|
||||||
value: process.env.GITHUB_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
|
||||||
value: process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
|
|
||||||
value: process.env.GITHUB_CALLBACK_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_SCOPE,
|
|
||||||
value: process.env.GITHUB_SCOPE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
|
||||||
value: process.env.MICROSOFT_CALLBACK_URL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_SCOPE,
|
|
||||||
value: process.env.MICROSOFT_SCOPE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_TENANT,
|
|
||||||
value: process.env.MICROSOFT_TENANT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: getConfiguredSSOProviders(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
|
||||||
value: false.toString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
|
||||||
value: generateAnalyticsUserId(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify if 'infra_config' table is loaded with all entries
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
export async function isInfraConfigTablePopulated(): Promise<boolean> {
|
|
||||||
const prisma = new PrismaService();
|
|
||||||
try {
|
|
||||||
const dbInfraConfigs = await prisma.infraConfig.findMany();
|
|
||||||
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
|
||||||
|
|
||||||
const propsRemainingToInsert = infraConfigDefaultObjs.filter(
|
|
||||||
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (propsRemainingToInsert.length > 0) {
|
|
||||||
console.log(
|
|
||||||
'Infra Config table is not populated with all entries. Populating now...',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the app after 5 seconds
|
* Stop the app after 5 seconds
|
||||||
* (Docker will re-start the app)
|
* (Docker will re-start the app)
|
||||||
@@ -234,12 +104,3 @@ export function getConfiguredSSOProviders() {
|
|||||||
|
|
||||||
return configuredAuthProviders.join(',');
|
return configuredAuthProviders.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a hashed valued for analytics
|
|
||||||
* @returns Generated hashed value
|
|
||||||
*/
|
|
||||||
export function generateAnalyticsUserId() {
|
|
||||||
const hashedUserID = randomBytes(20).toString('hex');
|
|
||||||
return hashedUserID;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Controller, Get, HttpStatus, Put, UseGuards } from '@nestjs/common';
|
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
|
||||||
import { InfraConfigService } from './infra-config.service';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
|
||||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
|
||||||
import { RESTError } from 'src/types/RESTError';
|
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
|
||||||
@Controller({ path: 'site', version: '1' })
|
|
||||||
export class SiteController {
|
|
||||||
constructor(private infraConfigService: InfraConfigService) {}
|
|
||||||
|
|
||||||
@Get('setup')
|
|
||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
|
||||||
async fetchSetupInfo() {
|
|
||||||
const status = await this.infraConfigService.get(
|
|
||||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(status))
|
|
||||||
throwHTTPErr(<RESTError>{
|
|
||||||
message: status.left,
|
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
|
||||||
});
|
|
||||||
return status.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('setup')
|
|
||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
|
||||||
async setSetupAsComplete() {
|
|
||||||
const res = await this.infraConfigService.update(
|
|
||||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
false.toString(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(res))
|
|
||||||
throwHTTPErr(<RESTError>{
|
|
||||||
message: res.left,
|
|
||||||
statusCode: HttpStatus.FORBIDDEN,
|
|
||||||
});
|
|
||||||
return res.right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -8,7 +8,7 @@ export class InfraConfig {
|
|||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnum;
|
name: InfraConfigEnumForClient;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
@@ -16,7 +16,7 @@ export class InfraConfig {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(InfraConfigEnum, {
|
registerEnumType(InfraConfigEnumForClient, {
|
||||||
name: 'InfraConfigEnum',
|
name: 'InfraConfigEnum',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { SiteController } from './infra-config.controller';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
providers: [InfraConfigService],
|
providers: [InfraConfigService],
|
||||||
exports: [InfraConfigService],
|
exports: [InfraConfigService],
|
||||||
controllers: [SiteController],
|
|
||||||
})
|
})
|
||||||
export class InfraConfigModule {}
|
export class InfraConfigModule {}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
|
||||||
import {
|
import {
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
InfraConfigEnum,
|
||||||
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
InfraConfigEnumForClient,
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
} from 'src/types/InfraConfig';
|
||||||
} from 'src/errors';
|
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as helper from './helper';
|
import * as helper from './helper';
|
||||||
import { InfraConfig as dbInfraConfig } from '@prisma/client';
|
|
||||||
import { InfraConfig } from './infra-config.model';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
@@ -22,82 +19,12 @@ const infraConfigService = new InfraConfigService(
|
|||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const INITIALIZED_DATE_CONST = new Date();
|
|
||||||
const dbInfraConfigs: dbInfraConfig[] = [
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: 'abcdefghijkl',
|
|
||||||
active: true,
|
|
||||||
createdOn: INITIALIZED_DATE_CONST,
|
|
||||||
updatedOn: INITIALIZED_DATE_CONST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: 'google',
|
|
||||||
active: true,
|
|
||||||
createdOn: INITIALIZED_DATE_CONST,
|
|
||||||
updatedOn: INITIALIZED_DATE_CONST,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const infraConfigs: InfraConfig[] = [
|
|
||||||
{
|
|
||||||
name: dbInfraConfigs[0].name as InfraConfigEnum,
|
|
||||||
value: dbInfraConfigs[0].value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: dbInfraConfigs[1].name as InfraConfigEnum,
|
|
||||||
value: dbInfraConfigs[1].value,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('InfraConfigService', () => {
|
describe('InfraConfigService', () => {
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('should update the infra config without backend server restart', async () => {
|
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
|
||||||
id: '',
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
active: true,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
|
||||||
const result = await infraConfigService.update(name, value);
|
|
||||||
|
|
||||||
expect(helper.stopApp).not.toHaveBeenCalled();
|
|
||||||
expect(result).toEqualRight({ name, value });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update the infra config with backend server restart', async () => {
|
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
|
||||||
const value = 'true';
|
|
||||||
|
|
||||||
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
|
||||||
id: '',
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
active: true,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
});
|
|
||||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
|
||||||
|
|
||||||
const result = await infraConfigService.update(name, value, true);
|
|
||||||
|
|
||||||
expect(helper.stopApp).toHaveBeenCalledTimes(1);
|
|
||||||
expect(result).toEqualRight({ name, value });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update the infra config', async () => {
|
it('should update the infra config', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
@@ -144,7 +71,7 @@ describe('InfraConfigService', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should get the infra config', async () => {
|
it('should get the infra config', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
@@ -160,7 +87,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass correct params to prisma findUnique', async () => {
|
it('should pass correct params to prisma findUnique', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
await infraConfigService.get(name);
|
await infraConfigService.get(name);
|
||||||
|
|
||||||
@@ -171,7 +98,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the infra config does not exist', async () => {
|
it('should throw an error if the infra config does not exist', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
||||||
|
|
||||||
@@ -179,45 +106,4 @@ describe('InfraConfigService', () => {
|
|||||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMany', () => {
|
|
||||||
it('should throw error if any disallowed names are provided', async () => {
|
|
||||||
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
|
||||||
const result = await infraConfigService.getMany(disallowedNames);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
|
||||||
});
|
|
||||||
it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => {
|
|
||||||
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
|
||||||
|
|
||||||
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
|
||||||
disallowedNames.includes(dbConfig.name as InfraConfigEnum),
|
|
||||||
);
|
|
||||||
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
|
||||||
dbInfraConfigResponses,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await infraConfigService.getMany(disallowedNames, false);
|
|
||||||
|
|
||||||
expect(result).toEqualRight(
|
|
||||||
infraConfigs.filter((i) => disallowedNames.includes(i.name)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return right with infraConfigs if Prisma query succeeds', async () => {
|
|
||||||
const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID];
|
|
||||||
|
|
||||||
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
|
||||||
allowedNames.includes(dbConfig.name as InfraConfigEnum),
|
|
||||||
);
|
|
||||||
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
|
||||||
dbInfraConfigResponses,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await infraConfigService.getMany(allowedNames);
|
|
||||||
expect(result).toEqualRight(
|
|
||||||
infraConfigs.filter((i) => allowedNames.includes(i.name)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,25 +3,23 @@ import { InfraConfig } from './infra-config.model';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import {
|
||||||
|
InfraConfigEnum,
|
||||||
|
InfraConfigEnumForClient,
|
||||||
|
} from 'src/types/InfraConfig';
|
||||||
import {
|
import {
|
||||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
INFRA_CONFIG_INVALID_INPUT,
|
INFRA_CONFIG_INVALID_INPUT,
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
|
INFRA_CONFIG_NOT_LISTED,
|
||||||
INFRA_CONFIG_RESET_FAILED,
|
INFRA_CONFIG_RESET_FAILED,
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||||
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import {
|
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||||
throwErr,
|
|
||||||
validateSMTPEmail,
|
|
||||||
validateSMTPUrl,
|
|
||||||
validateUrl,
|
|
||||||
} from 'src/utils';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper';
|
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@@ -32,32 +30,70 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
|
||||||
EXCLUDE_FROM_UPDATE_CONFIGS = [
|
|
||||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
|
||||||
InfraConfigEnum.ANALYTICS_USER_ID,
|
|
||||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
];
|
|
||||||
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
|
||||||
EXCLUDE_FROM_FETCH_CONFIGS = [
|
|
||||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
InfraConfigEnum.ANALYTICS_USER_ID,
|
|
||||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
];
|
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.initializeInfraConfigTable();
|
await this.initializeInfraConfigTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
|
||||||
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
|
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
|
value: process.env.MAILER_SMTP_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
value: process.env.MAILER_ADDRESS_FROM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
value: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
|
value: process.env.GITHUB_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
value: process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: getConfiguredSSOProviders(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return infraConfigDefaultObjs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the 'infra_config' table with values from .env
|
* Initialize the 'infra_config' table with values from .env
|
||||||
* @description This function create rows 'infra_config' in very first time (only once)
|
* @description This function create rows 'infra_config' in very first time (only once)
|
||||||
*/
|
*/
|
||||||
async initializeInfraConfigTable() {
|
async initializeInfraConfigTable() {
|
||||||
try {
|
try {
|
||||||
|
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
||||||
|
const enumValues = Object.values(InfraConfigEnum);
|
||||||
|
|
||||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
||||||
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
||||||
|
|
||||||
|
// Check if all the 'names' are listed in the default values
|
||||||
|
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
||||||
|
throw new Error(INFRA_CONFIG_NOT_LISTED);
|
||||||
|
}
|
||||||
|
|
||||||
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
||||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
||||||
@@ -111,10 +147,12 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* Update InfraConfig by name
|
* Update InfraConfig by name
|
||||||
* @param name Name of the InfraConfig
|
* @param name Name of the InfraConfig
|
||||||
* @param value Value of the InfraConfig
|
* @param value Value of the InfraConfig
|
||||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
|
async update(
|
||||||
|
name: InfraConfigEnumForClient | InfraConfigEnum,
|
||||||
|
value: string,
|
||||||
|
) {
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -124,7 +162,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
data: { value },
|
data: { value },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (restartEnabled) stopApp();
|
stopApp();
|
||||||
|
|
||||||
return E.right(this.cast(infraConfig));
|
return E.right(this.cast(infraConfig));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -138,11 +176,6 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
|
||||||
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
|
||||||
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
const isValidate = this.validateEnvValues(infraConfigs);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -176,26 +209,12 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case AuthProvider.GOOGLE:
|
case AuthProvider.GOOGLE:
|
||||||
return (
|
return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET;
|
||||||
configMap.GOOGLE_CLIENT_ID &&
|
|
||||||
configMap.GOOGLE_CLIENT_SECRET &&
|
|
||||||
configMap.GOOGLE_CALLBACK_URL &&
|
|
||||||
configMap.GOOGLE_SCOPE
|
|
||||||
);
|
|
||||||
case AuthProvider.GITHUB:
|
case AuthProvider.GITHUB:
|
||||||
return (
|
return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET;
|
||||||
configMap.GITHUB_CLIENT_ID &&
|
|
||||||
configMap.GITHUB_CLIENT_SECRET &&
|
|
||||||
configMap.GITHUB_CALLBACK_URL &&
|
|
||||||
configMap.GITHUB_SCOPE
|
|
||||||
);
|
|
||||||
case AuthProvider.MICROSOFT:
|
case AuthProvider.MICROSOFT:
|
||||||
return (
|
return (
|
||||||
configMap.MICROSOFT_CLIENT_ID &&
|
configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET
|
||||||
configMap.MICROSOFT_CLIENT_SECRET &&
|
|
||||||
configMap.MICROSOFT_CALLBACK_URL &&
|
|
||||||
configMap.MICROSOFT_SCOPE &&
|
|
||||||
configMap.MICROSOFT_TENANT
|
|
||||||
);
|
);
|
||||||
case AuthProvider.EMAIL:
|
case AuthProvider.EMAIL:
|
||||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||||
@@ -204,22 +223,6 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or Disable Analytics Collection
|
|
||||||
*
|
|
||||||
* @param status Status to enable or disable
|
|
||||||
* @returns Boolean of status of analytics collection
|
|
||||||
*/
|
|
||||||
async toggleAnalyticsCollection(status: ServiceStatus) {
|
|
||||||
const isUpdated = await this.update(
|
|
||||||
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
|
||||||
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
|
||||||
return E.right(isUpdated.right.value === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or Disable SSO for login/signup
|
* Enable or Disable SSO for login/signup
|
||||||
* @param provider Auth Provider to enable or disable
|
* @param provider Auth Provider to enable or disable
|
||||||
@@ -258,7 +261,6 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
const isUpdated = await this.update(
|
const isUpdated = await this.update(
|
||||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
updatedAuthProviders.join(','),
|
updatedAuthProviders.join(','),
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
|
||||||
@@ -270,7 +272,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param name Name of the InfraConfig
|
* @param name Name of the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async get(name: InfraConfigEnum) {
|
async get(name: InfraConfigEnumForClient) {
|
||||||
try {
|
try {
|
||||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||||
where: { name },
|
where: { name },
|
||||||
@@ -287,15 +289,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param names Names of the InfraConfigs
|
* @param names Names of the InfraConfigs
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) {
|
async getMany(names: InfraConfigEnumForClient[]) {
|
||||||
if (checkDisallowedKeys) {
|
|
||||||
// Check if the names are allowed to fetch by client
|
|
||||||
for (let i = 0; i < names.length; i++) {
|
|
||||||
if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i]))
|
|
||||||
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||||
where: { name: { in: names } },
|
where: { name: { in: names } },
|
||||||
@@ -322,24 +316,13 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
||||||
|
|
||||||
await this.prisma.infraConfig.deleteMany({
|
await this.prisma.infraConfig.deleteMany({
|
||||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hardcode t
|
|
||||||
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
|
|
||||||
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
);
|
|
||||||
await this.prisma.infraConfig.createMany({
|
await this.prisma.infraConfig.createMany({
|
||||||
data: [
|
data: infraConfigDefaultObjs,
|
||||||
...updatedInfraConfigDefaultObjs,
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: 'true',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stopApp();
|
stopApp();
|
||||||
@@ -355,60 +338,36 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
validateEnvValues(
|
validateEnvValues(
|
||||||
infraConfigs: {
|
infraConfigs: {
|
||||||
name: InfraConfigEnum;
|
name: InfraConfigEnumForClient | InfraConfigEnum;
|
||||||
value: string;
|
value: string;
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
switch (infraConfigs[i].name) {
|
switch (infraConfigs[i].name) {
|
||||||
case InfraConfigEnum.MAILER_SMTP_URL:
|
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GOOGLE_SCOPE:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GITHUB_SCOPE:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_SCOPE:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_TENANT:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class InfraConfigArgs {
|
export class InfraConfigArgs {
|
||||||
@Field(() => InfraConfigEnum, {
|
@Field(() => InfraConfigEnumForClient, {
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnum;
|
name: InfraConfigEnumForClient;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class MailerService {
|
|||||||
): string {
|
): string {
|
||||||
switch (mailDesc.template) {
|
switch (mailDesc.template) {
|
||||||
case 'team-invitation':
|
case 'team-invitation':
|
||||||
return `A user has invited you to join a team workspace in Hoppscotch`;
|
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
||||||
|
|
||||||
case 'user-invitation':
|
case 'user-invitation':
|
||||||
return 'Sign in to Hoppscotch';
|
return 'Sign in to Hoppscotch';
|
||||||
|
|||||||
@@ -27,12 +27,6 @@
|
|||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nohighlight {
|
|
||||||
color: inherit !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -464,7 +458,7 @@
|
|||||||
<td class="content-cell">
|
<td class="content-cell">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
<h1>Hi there,</h1>
|
<h1>Hi there,</h1>
|
||||||
<p><a class="nohighlight" name="invitee" href="#">{{invitee}}</a> with <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a> has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
<p>{{invitee}} with {{invite_team_name}} has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
||||||
<!-- Action -->
|
<!-- Action -->
|
||||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -490,7 +484,7 @@
|
|||||||
Welcome aboard, <br />
|
Welcome aboard, <br />
|
||||||
Your friends at Hoppscotch
|
Your friends at Hoppscotch
|
||||||
</p>
|
</p>
|
||||||
<p><strong>P.S.</strong> If you don't associate with <a class="nohighlight" name="invitee" href="#">{{invitee}}</a> or <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a>, just ignore this email.</p>
|
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, just ignore this email.</p>
|
||||||
<!-- Sub copy -->
|
<!-- Sub copy -->
|
||||||
<table class="body-sub">
|
<table class="body-sub">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -27,12 +27,6 @@
|
|||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nohighlight {
|
|
||||||
color: inherit !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PosthogService } from './posthog.service';
|
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule],
|
|
||||||
providers: [PosthogService],
|
|
||||||
})
|
|
||||||
export class PosthogModule {}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { PostHog } from 'posthog-node';
|
|
||||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
|
||||||
import { CronJob } from 'cron';
|
|
||||||
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
|
|
||||||
import { throwErr } from 'src/utils';
|
|
||||||
@Injectable()
|
|
||||||
export class PosthogService {
|
|
||||||
private postHogClient: PostHog;
|
|
||||||
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private readonly prismaService: PrismaService,
|
|
||||||
private schedulerRegistry: SchedulerRegistry,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
|
|
||||||
console.log('Initializing PostHog');
|
|
||||||
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
|
|
||||||
host: 'https://eu.posthog.com',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schedule the cron job only if analytics collection is allowed
|
|
||||||
this.scheduleCronJob();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleCronJob() {
|
|
||||||
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
|
|
||||||
await this.capture();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.schedulerRegistry.addCronJob('captureAnalytics', job);
|
|
||||||
job.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
async capture() {
|
|
||||||
if (!this.postHogClient) {
|
|
||||||
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.postHogClient.capture({
|
|
||||||
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
|
|
||||||
event: 'sh_instance',
|
|
||||||
properties: {
|
|
||||||
type: 'COMMUNITY',
|
|
||||||
total_user_count: await this.prismaService.user.count(),
|
|
||||||
total_workspace_count: await this.prismaService.team.count(),
|
|
||||||
version: this.configService.get('npm_package_version'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log('Sent event to PostHog');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Type of data returned from the query to obtain all search results
|
|
||||||
export type SearchQueryReturnType = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: 'collection' | 'request';
|
|
||||||
method?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Type of data returned from the query to obtain all parents
|
|
||||||
export type ParentTreeQueryReturnType = {
|
|
||||||
id: string;
|
|
||||||
parentID: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
|
||||||
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
|
|
||||||
import { TeamMemberRole } from '@prisma/client';
|
|
||||||
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
|
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
|
||||||
@Controller({ path: 'team-collection', version: '1' })
|
|
||||||
export class TeamCollectionController {
|
|
||||||
constructor(private readonly teamCollectionService: TeamCollectionService) {}
|
|
||||||
|
|
||||||
@Get('search/:teamID/:searchQuery')
|
|
||||||
@RequiresTeamRole(
|
|
||||||
TeamMemberRole.VIEWER,
|
|
||||||
TeamMemberRole.EDITOR,
|
|
||||||
TeamMemberRole.OWNER,
|
|
||||||
)
|
|
||||||
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
|
|
||||||
async searchByTitle(
|
|
||||||
@Param('searchQuery') searchQuery: string,
|
|
||||||
@Param('teamID') teamID: string,
|
|
||||||
@Query('take') take: string,
|
|
||||||
@Query('skip') skip: string,
|
|
||||||
) {
|
|
||||||
const res = await this.teamCollectionService.searchByTitle(
|
|
||||||
searchQuery,
|
|
||||||
teamID,
|
|
||||||
parseInt(take),
|
|
||||||
parseInt(skip),
|
|
||||||
);
|
|
||||||
if (E.isLeft(res)) throwHTTPErr(res.left);
|
|
||||||
return res.right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-membe
|
|||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
import { TeamCollectionController } from './team-collection.controller';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
||||||
@@ -16,6 +15,5 @@ import { TeamCollectionController } from './team-collection.controller';
|
|||||||
GqlCollectionTeamMemberGuard,
|
GqlCollectionTeamMemberGuard,
|
||||||
],
|
],
|
||||||
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
||||||
controllers: [TeamCollectionController],
|
|
||||||
})
|
})
|
||||||
export class TeamCollectionModule {}
|
export class TeamCollectionModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
import { TeamCollection } from './team-collection.model';
|
||||||
import {
|
import {
|
||||||
@@ -14,10 +14,6 @@ import {
|
|||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COLL_DATA_INVALID,
|
TEAM_COLL_DATA_INVALID,
|
||||||
TEAM_REQ_SEARCH_FAILED,
|
|
||||||
TEAM_COL_SEARCH_FAILED,
|
|
||||||
TEAM_REQ_PARENT_TREE_GEN_FAILED,
|
|
||||||
TEAM_COLL_PARENT_TREE_GEN_FAILED,
|
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
@@ -26,9 +22,6 @@ import * as O from 'fp-ts/Option';
|
|||||||
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
import { stringToJson } from 'src/utils';
|
import { stringToJson } from 'src/utils';
|
||||||
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
|
|
||||||
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
|
|
||||||
import { RESTError } from 'src/types/RESTError';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamCollectionService {
|
export class TeamCollectionService {
|
||||||
@@ -1063,266 +1056,4 @@ export class TeamCollectionService {
|
|||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for TeamCollections and TeamRequests by title
|
|
||||||
*
|
|
||||||
* @param searchQuery The search query
|
|
||||||
* @param teamID The Team ID
|
|
||||||
* @param take Number of items we want returned
|
|
||||||
* @param skip Number of items we want to skip
|
|
||||||
* @returns An Either of the search results
|
|
||||||
*/
|
|
||||||
async searchByTitle(
|
|
||||||
searchQuery: string,
|
|
||||||
teamID: string,
|
|
||||||
take = 10,
|
|
||||||
skip = 0,
|
|
||||||
) {
|
|
||||||
// Fetch all collections and requests that match the search query
|
|
||||||
const searchResults: SearchQueryReturnType[] = [];
|
|
||||||
|
|
||||||
const matchedCollections = await this.searchCollections(
|
|
||||||
searchQuery,
|
|
||||||
teamID,
|
|
||||||
take,
|
|
||||||
skip,
|
|
||||||
);
|
|
||||||
if (E.isLeft(matchedCollections))
|
|
||||||
return E.left(<RESTError>{
|
|
||||||
message: matchedCollections.left,
|
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
|
||||||
});
|
|
||||||
searchResults.push(...matchedCollections.right);
|
|
||||||
|
|
||||||
const matchedRequests = await this.searchRequests(
|
|
||||||
searchQuery,
|
|
||||||
teamID,
|
|
||||||
take,
|
|
||||||
skip,
|
|
||||||
);
|
|
||||||
if (E.isLeft(matchedRequests))
|
|
||||||
return E.left(<RESTError>{
|
|
||||||
message: matchedRequests.left,
|
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
|
||||||
});
|
|
||||||
searchResults.push(...matchedRequests.right);
|
|
||||||
|
|
||||||
// Generate the parent tree for searchResults
|
|
||||||
const searchResultsWithTree: CollectionSearchNode[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < searchResults.length; i++) {
|
|
||||||
const fetchedParentTree = await this.fetchParentTree(searchResults[i]);
|
|
||||||
if (E.isLeft(fetchedParentTree))
|
|
||||||
return E.left(<RESTError>{
|
|
||||||
message: fetchedParentTree.left,
|
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
|
||||||
});
|
|
||||||
searchResultsWithTree.push({
|
|
||||||
type: searchResults[i].type,
|
|
||||||
title: searchResults[i].title,
|
|
||||||
method: searchResults[i].method,
|
|
||||||
id: searchResults[i].id,
|
|
||||||
path: !fetchedParentTree
|
|
||||||
? []
|
|
||||||
: ([fetchedParentTree.right] as CollectionSearchNode[]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return E.right({ data: searchResultsWithTree });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for TeamCollections by title
|
|
||||||
*
|
|
||||||
* @param searchQuery The search query
|
|
||||||
* @param teamID The Team ID
|
|
||||||
* @param take Number of items we want returned
|
|
||||||
* @param skip Number of items we want to skip
|
|
||||||
* @returns An Either of the search results
|
|
||||||
*/
|
|
||||||
private async searchCollections(
|
|
||||||
searchQuery: string,
|
|
||||||
teamID: string,
|
|
||||||
take: number,
|
|
||||||
skip: number,
|
|
||||||
) {
|
|
||||||
const query = Prisma.sql`
|
|
||||||
select id,title,'collection' AS type
|
|
||||||
from "TeamCollection"
|
|
||||||
where "TeamCollection"."teamID"=${teamID}
|
|
||||||
and titlesearch @@ to_tsquery(${searchQuery})
|
|
||||||
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
|
|
||||||
limit ${take}
|
|
||||||
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
|
||||||
`;
|
|
||||||
try {
|
|
||||||
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
|
||||||
return E.right(res);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_COL_SEARCH_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for TeamRequests by title
|
|
||||||
*
|
|
||||||
* @param searchQuery The search query
|
|
||||||
* @param teamID The Team ID
|
|
||||||
* @param take Number of items we want returned
|
|
||||||
* @param skip Number of items we want to skip
|
|
||||||
* @returns An Either of the search results
|
|
||||||
*/
|
|
||||||
private async searchRequests(
|
|
||||||
searchQuery: string,
|
|
||||||
teamID: string,
|
|
||||||
take: number,
|
|
||||||
skip: number,
|
|
||||||
) {
|
|
||||||
const query = Prisma.sql`
|
|
||||||
select id,title,request->>'method' as method,'request' AS type
|
|
||||||
from "TeamRequest"
|
|
||||||
where "TeamRequest"."teamID"=${teamID}
|
|
||||||
and titlesearch @@ to_tsquery(${searchQuery})
|
|
||||||
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
|
|
||||||
limit ${take}
|
|
||||||
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
|
||||||
return E.right(res);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_REQ_SEARCH_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the parent tree of a search result
|
|
||||||
*
|
|
||||||
* @param searchResult The search result for which we want to generate the parent tree
|
|
||||||
* @returns The parent tree of the search result
|
|
||||||
*/
|
|
||||||
private async fetchParentTree(searchResult: SearchQueryReturnType) {
|
|
||||||
return searchResult.type === 'collection'
|
|
||||||
? await this.fetchCollectionParentTree(searchResult.id)
|
|
||||||
: await this.fetchRequestParentTree(searchResult.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the parent tree of a collection
|
|
||||||
*
|
|
||||||
* @param id The ID of the collection
|
|
||||||
* @returns The parent tree of the collection
|
|
||||||
*/
|
|
||||||
private async fetchCollectionParentTree(id: string) {
|
|
||||||
try {
|
|
||||||
const query = Prisma.sql`
|
|
||||||
WITH RECURSIVE collection_tree AS (
|
|
||||||
SELECT tc.id, tc."parentID", tc.title
|
|
||||||
FROM "TeamCollection" AS tc
|
|
||||||
JOIN "TeamCollection" AS tr ON tc.id = tr."parentID"
|
|
||||||
WHERE tr.id = ${id}
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT parent.id, parent."parentID", parent.title
|
|
||||||
FROM "TeamCollection" AS parent
|
|
||||||
JOIN collection_tree AS ct ON parent.id = ct."parentID"
|
|
||||||
)
|
|
||||||
SELECT * FROM collection_tree;
|
|
||||||
`;
|
|
||||||
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
const collectionParentTree = this.generateParentTree(res);
|
|
||||||
return E.right(collectionParentTree);
|
|
||||||
} catch (error) {
|
|
||||||
E.left(TEAM_COLL_PARENT_TREE_GEN_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the parent tree from the collections
|
|
||||||
*
|
|
||||||
* @param parentCollections The parent collections
|
|
||||||
* @returns The parent tree of the parent collections
|
|
||||||
*/
|
|
||||||
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
|
|
||||||
function findChildren(id) {
|
|
||||||
const collection = parentCollections.filter((item) => item.id === id)[0];
|
|
||||||
if (collection.parentID == null) {
|
|
||||||
return {
|
|
||||||
id: collection.id,
|
|
||||||
title: collection.title,
|
|
||||||
type: 'collection',
|
|
||||||
path: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = {
|
|
||||||
id: collection.id,
|
|
||||||
title: collection.title,
|
|
||||||
type: 'collection',
|
|
||||||
path: findChildren(collection.parentID),
|
|
||||||
};
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCollections.length > 0) {
|
|
||||||
if (parentCollections[0].parentID == null) {
|
|
||||||
return {
|
|
||||||
id: parentCollections[0].id,
|
|
||||||
title: parentCollections[0].title,
|
|
||||||
type: 'collection',
|
|
||||||
path: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: parentCollections[0].id,
|
|
||||||
title: parentCollections[0].title,
|
|
||||||
type: 'collection',
|
|
||||||
path: findChildren(parentCollections[0].parentID),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the parent tree of a request
|
|
||||||
*
|
|
||||||
* @param id The ID of the request
|
|
||||||
* @returns The parent tree of the request
|
|
||||||
*/
|
|
||||||
private async fetchRequestParentTree(id: string) {
|
|
||||||
try {
|
|
||||||
const query = Prisma.sql`
|
|
||||||
WITH RECURSIVE request_collection_tree AS (
|
|
||||||
SELECT tc.id, tc."parentID", tc.title
|
|
||||||
FROM "TeamCollection" AS tc
|
|
||||||
JOIN "TeamRequest" AS tr ON tc.id = tr."collectionID"
|
|
||||||
WHERE tr.id = ${id}
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT parent.id, parent."parentID", parent.title
|
|
||||||
FROM "TeamCollection" AS parent
|
|
||||||
JOIN request_collection_tree AS ct ON parent.id = ct."parentID"
|
|
||||||
)
|
|
||||||
SELECT * FROM request_collection_tree;
|
|
||||||
|
|
||||||
`;
|
|
||||||
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
const requestParentTree = this.generateParentTree(res);
|
|
||||||
return E.right(requestParentTree);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { TeamService } from '../../team/team.service';
|
|
||||||
import { TeamMemberRole } from '../../team/team.model';
|
|
||||||
import {
|
|
||||||
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
|
|
||||||
BUG_AUTH_NO_USER_CTX,
|
|
||||||
BUG_TEAM_NO_TEAM_ID,
|
|
||||||
TEAM_MEMBER_NOT_FOUND,
|
|
||||||
TEAM_NOT_REQUIRED_ROLE,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { throwHTTPErr } from 'src/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RESTTeamMemberGuard implements CanActivate {
|
|
||||||
constructor(
|
|
||||||
private readonly reflector: Reflector,
|
|
||||||
private readonly teamService: TeamService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
|
||||||
'requiresTeamRole',
|
|
||||||
context.getHandler(),
|
|
||||||
);
|
|
||||||
if (!requireRoles)
|
|
||||||
throwHTTPErr({ message: BUG_TEAM_NO_REQUIRE_TEAM_ROLE, statusCode: 400 });
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
|
|
||||||
const { user } = request;
|
|
||||||
if (user == undefined)
|
|
||||||
throwHTTPErr({ message: BUG_AUTH_NO_USER_CTX, statusCode: 400 });
|
|
||||||
|
|
||||||
const teamID = request.params.teamID;
|
|
||||||
if (!teamID)
|
|
||||||
throwHTTPErr({ message: BUG_TEAM_NO_TEAM_ID, statusCode: 400 });
|
|
||||||
|
|
||||||
const teamMember = await this.teamService.getTeamMember(teamID, user.uid);
|
|
||||||
if (!teamMember)
|
|
||||||
throwHTTPErr({ message: TEAM_MEMBER_NOT_FOUND, statusCode: 404 });
|
|
||||||
|
|
||||||
if (requireRoles.includes(teamMember.role)) return true;
|
|
||||||
|
|
||||||
throwHTTPErr({ message: TEAM_NOT_REQUIRED_ROLE, statusCode: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { HttpStatus } from '@nestjs/common';
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Custom interface to handle errors for REST modules such as Auth, Admin modules
|
** Custom interface to handle errors specific to Auth module
|
||||||
** Since its REST we need to return the HTTP status code along with the error message
|
** Since its REST we need to return the HTTP status code along with the error message
|
||||||
*/
|
*/
|
||||||
export type RESTError = {
|
export type AuthError = {
|
||||||
message: string;
|
message: string;
|
||||||
statusCode: HttpStatus;
|
statusCode: HttpStatus;
|
||||||
};
|
};
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Response type of results from the search query
|
|
||||||
export type CollectionSearchNode = {
|
|
||||||
/** Encodes the hierarchy of where the node is **/
|
|
||||||
path: CollectionSearchNode[];
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: 'request';
|
|
||||||
title: string;
|
|
||||||
method: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'collection';
|
|
||||||
title: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -4,23 +4,26 @@ export enum InfraConfigEnum {
|
|||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
|
||||||
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
|
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||||
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
|
|
||||||
GITHUB_SCOPE = 'GITHUB_SCOPE',
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL',
|
|
||||||
MICROSOFT_SCOPE = 'MICROSOFT_SCOPE',
|
|
||||||
MICROSOFT_TENANT = 'MICROSOFT_TENANT',
|
|
||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
}
|
||||||
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
|
||||||
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
export enum InfraConfigEnumForClient {
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
||||||
|
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||||
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||||
|
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||||
|
|
||||||
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,3 @@ export class PaginationArgs {
|
|||||||
})
|
})
|
||||||
take: number;
|
take: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
@InputType()
|
|
||||||
export class OffsetPaginationArgs {
|
|
||||||
@Field({
|
|
||||||
nullable: true,
|
|
||||||
defaultValue: 0,
|
|
||||||
description: 'Number of items to skip',
|
|
||||||
})
|
|
||||||
skip: number;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
nullable: true,
|
|
||||||
defaultValue: 10,
|
|
||||||
description: 'Number of items to fetch',
|
|
||||||
})
|
|
||||||
take: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -56,22 +56,3 @@ export enum SessionType {
|
|||||||
registerEnumType(SessionType, {
|
registerEnumType(SessionType, {
|
||||||
name: 'SessionType',
|
name: 'SessionType',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class UserDeletionResult {
|
|
||||||
@Field(() => ID, {
|
|
||||||
description: 'UID of the user',
|
|
||||||
})
|
|
||||||
userUID: string;
|
|
||||||
|
|
||||||
@Field(() => Boolean, {
|
|
||||||
description: 'Flag to determine if user deletion was successful or not',
|
|
||||||
})
|
|
||||||
isDeleted: Boolean;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
nullable: true,
|
|
||||||
description: 'Error message if user deletion was not successful',
|
|
||||||
})
|
|
||||||
errorMessage: String;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
import { JSON_INVALID, USER_NOT_FOUND } from 'src/errors';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
@@ -176,26 +176,6 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findUsersByIds', () => {
|
|
||||||
test('should successfully return users given valid user UIDs', async () => {
|
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
|
||||||
|
|
||||||
const result = await userService.findUsersByIds([
|
|
||||||
'123344',
|
|
||||||
'5555',
|
|
||||||
'6666',
|
|
||||||
]);
|
|
||||||
expect(result).toEqual(users);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return empty array of users given a invalid user UIDs', async () => {
|
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const result = await userService.findUsersByIds(['sdcvbdbr']);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createUserViaMagicLink', () => {
|
describe('createUserViaMagicLink', () => {
|
||||||
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
||||||
mockPrisma.user.create.mockResolvedValueOnce(user);
|
mockPrisma.user.create.mockResolvedValueOnce(user);
|
||||||
@@ -434,54 +414,6 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateUserDisplayName', () => {
|
|
||||||
test('should resolve right and update user display name', async () => {
|
|
||||||
const newDisplayName = 'New Name';
|
|
||||||
mockPrisma.user.update.mockResolvedValueOnce({
|
|
||||||
...user,
|
|
||||||
displayName: newDisplayName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await userService.updateUserDisplayName(
|
|
||||||
user.uid,
|
|
||||||
newDisplayName,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight({
|
|
||||||
...user,
|
|
||||||
displayName: newDisplayName,
|
|
||||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
|
||||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('should resolve right and publish user updated subscription', async () => {
|
|
||||||
const newDisplayName = 'New Name';
|
|
||||||
mockPrisma.user.update.mockResolvedValueOnce({
|
|
||||||
...user,
|
|
||||||
displayName: newDisplayName,
|
|
||||||
});
|
|
||||||
|
|
||||||
await userService.updateUserDisplayName(user.uid, user.displayName);
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
|
||||||
`user/${user.uid}/updated`,
|
|
||||||
{
|
|
||||||
...user,
|
|
||||||
displayName: newDisplayName,
|
|
||||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
|
||||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('should resolve left and error when invalid user uid is passed', async () => {
|
|
||||||
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
|
||||||
|
|
||||||
const result = await userService.updateUserDisplayName(
|
|
||||||
'invalidUserUid',
|
|
||||||
user.displayName,
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(USER_NOT_FOUND);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchAllUsers', () => {
|
describe('fetchAllUsers', () => {
|
||||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||||
@@ -503,36 +435,6 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchAllUsersV2', () => {
|
|
||||||
test('should resolve right and return first 20 users when searchString is null', async () => {
|
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
|
||||||
|
|
||||||
const result = await userService.fetchAllUsersV2(null, {
|
|
||||||
take: 20,
|
|
||||||
skip: 0,
|
|
||||||
});
|
|
||||||
expect(result).toEqual(users);
|
|
||||||
});
|
|
||||||
test('should resolve right and return next 20 users when searchString is provided', async () => {
|
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
|
||||||
|
|
||||||
const result = await userService.fetchAllUsersV2('.com', {
|
|
||||||
take: 20,
|
|
||||||
skip: 0,
|
|
||||||
});
|
|
||||||
expect(result).toEqual(users);
|
|
||||||
});
|
|
||||||
test('should resolve left and return an empty array when users not found', async () => {
|
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const result = await userService.fetchAllUsersV2('Unknown entry', {
|
|
||||||
take: 20,
|
|
||||||
skip: 0,
|
|
||||||
});
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchAdminUsers', () => {
|
describe('fetchAdminUsers', () => {
|
||||||
test('should return a list of admin users', async () => {
|
test('should return a list of admin users', async () => {
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
||||||
@@ -654,17 +556,4 @@ describe('UserService', () => {
|
|||||||
expect(result).toEqual(10);
|
expect(result).toEqual(10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeUsersAsAdmin', () => {
|
|
||||||
test('should resolve right and return true for valid user UIDs', async () => {
|
|
||||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 1 });
|
|
||||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
|
||||||
expect(result).toEqualRight(true);
|
|
||||||
});
|
|
||||||
test('should resolve right and return false for invalid user UIDs', async () => {
|
|
||||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 0 });
|
|
||||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
|
||||||
expect(result).toEqualLeft(USERS_NOT_FOUND);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import * as T from 'fp-ts/Task';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { pipe, constVoid } from 'fp-ts/function';
|
import { pipe, constVoid } from 'fp-ts/function';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
import { USER_NOT_FOUND } from 'src/errors';
|
||||||
import { SessionType, User } from './user.model';
|
import { SessionType, User } from './user.model';
|
||||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
||||||
import { UserDataHandler } from './user.data.handler';
|
import { UserDataHandler } from './user.data.handler';
|
||||||
import { User as DbUser } from '@prisma/client';
|
import { User as DbUser } from '@prisma/client';
|
||||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@@ -89,20 +88,6 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find users with given IDs
|
|
||||||
* @param userUIDs User IDs
|
|
||||||
* @returns Array of found Users
|
|
||||||
*/
|
|
||||||
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
|
||||||
const users = await this.prisma.user.findMany({
|
|
||||||
where: {
|
|
||||||
uid: { in: userUIDs },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update User with new generated hashed refresh token
|
* Update User with new generated hashed refresh token
|
||||||
*
|
*
|
||||||
@@ -284,30 +269,6 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a user's data
|
|
||||||
* @param userUID User UID
|
|
||||||
* @param displayName User's displayName
|
|
||||||
* @returns a Either of User or error
|
|
||||||
*/
|
|
||||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
|
||||||
try {
|
|
||||||
const dbUpdatedUser = await this.prisma.user.update({
|
|
||||||
where: { uid: userUID },
|
|
||||||
data: { displayName },
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
|
|
||||||
|
|
||||||
// Publish subscription for user updates
|
|
||||||
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
|
|
||||||
|
|
||||||
return E.right(updatedUser);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(USER_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate and parse currentRESTSession and currentGQLSession
|
* Validate and parse currentRESTSession and currentGQLSession
|
||||||
* @param sessionData string of the session
|
* @param sessionData string of the session
|
||||||
@@ -325,7 +286,6 @@ export class UserService {
|
|||||||
* @param cursorID string of userUID or null
|
* @param cursorID string of userUID or null
|
||||||
* @param take number of users to query
|
* @param take number of users to query
|
||||||
* @returns an array of `User` object
|
* @returns an array of `User` object
|
||||||
* @deprecated use fetchAllUsersV2 instead
|
|
||||||
*/
|
*/
|
||||||
async fetchAllUsers(cursorID: string, take: number) {
|
async fetchAllUsers(cursorID: string, take: number) {
|
||||||
const fetchedUsers = await this.prisma.user.findMany({
|
const fetchedUsers = await this.prisma.user.findMany({
|
||||||
@@ -336,43 +296,6 @@ export class UserService {
|
|||||||
return fetchedUsers;
|
return fetchedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all the users in the `User` table based on cursor
|
|
||||||
* @param searchString search on user's displayName or email
|
|
||||||
* @param paginationOption pagination options
|
|
||||||
* @returns an array of `User` object
|
|
||||||
*/
|
|
||||||
async fetchAllUsersV2(
|
|
||||||
searchString: string,
|
|
||||||
paginationOption: OffsetPaginationArgs,
|
|
||||||
) {
|
|
||||||
const fetchedUsers = await this.prisma.user.findMany({
|
|
||||||
skip: paginationOption.skip,
|
|
||||||
take: paginationOption.take,
|
|
||||||
where: searchString
|
|
||||||
? {
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
displayName: {
|
|
||||||
contains: searchString,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: {
|
|
||||||
contains: searchString,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
orderBy: [{ isAdmin: 'desc' }, { displayName: 'asc' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
return fetchedUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the number of users in db
|
* Fetch the number of users in db
|
||||||
* @returns a count (Int) of user records in DB
|
* @returns a count (Int) of user records in DB
|
||||||
@@ -403,23 +326,6 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Change users to admins by toggling isAdmin param to true
|
|
||||||
* @param userUID user UIDs
|
|
||||||
* @returns a Either of true or error
|
|
||||||
*/
|
|
||||||
async makeAdmins(userUIDs: string[]) {
|
|
||||||
try {
|
|
||||||
await this.prisma.user.updateMany({
|
|
||||||
where: { uid: { in: userUIDs } },
|
|
||||||
data: { isAdmin: true },
|
|
||||||
});
|
|
||||||
return E.right(true);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(USER_UPDATE_FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the admin users
|
* Fetch all the admin users
|
||||||
* @returns an array of admin users
|
* @returns an array of admin users
|
||||||
@@ -538,22 +444,4 @@ export class UserService {
|
|||||||
return E.left(USER_NOT_FOUND);
|
return E.left(USER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Change users from an admin by toggling isAdmin param to false
|
|
||||||
* @param userUIDs user UIDs
|
|
||||||
* @returns a Either of true or error
|
|
||||||
*/
|
|
||||||
async removeUsersAsAdmin(userUIDs: string[]) {
|
|
||||||
const data = await this.prisma.user.updateMany({
|
|
||||||
where: { uid: { in: userUIDs } },
|
|
||||||
data: { isAdmin: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.count === 0) {
|
|
||||||
return E.left(USERS_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext, HttpException } from '@nestjs/common';
|
import { ExecutionContext } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { AuthProvider } from './auth/helper';
|
||||||
import { RESTError } from './types/RESTError';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -28,15 +27,6 @@ export function throwErr(errMessage: string): never {
|
|||||||
throw new Error(errMessage);
|
throw new Error(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function allows throw to be used as an expression
|
|
||||||
* @param errMessage Message present in the error message
|
|
||||||
*/
|
|
||||||
export function throwHTTPErr(errorData: RESTError): never {
|
|
||||||
const { message, statusCode } = errorData;
|
|
||||||
throw new HttpException(message, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints the given value to log and returns the same value.
|
* Prints the given value to log and returns the same value.
|
||||||
* Used for debugging functional pipelines.
|
* Used for debugging functional pipelines.
|
||||||
@@ -183,16 +173,6 @@ export const validateSMTPUrl = (url: string) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks to see if the URL is valid or not
|
|
||||||
* @param url The URL to validate
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
export const validateUrl = (url: string) => {
|
|
||||||
const urlRegex = /^(http|https):\/\/[^ "]+$/;
|
|
||||||
return urlRegex.test(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String to JSON parser
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @param {str} str The string to parse
|
||||||
|
|||||||
3
packages/hoppscotch-cli/bin/hopp
Executable file
3
packages/hoppscotch-cli/bin/hopp
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// * The entry point of the CLI
|
||||||
|
require("../dist").cli(process.argv);
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// * The entry point of the CLI
|
|
||||||
|
|
||||||
import { cli } from "../dist/index.js";
|
|
||||||
|
|
||||||
cli(process.argv);
|
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||||
"homepage": "https://hoppscotch.io",
|
"homepage": "https://hoppscotch.io",
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"hopp": "bin/hopp.js"
|
"hopp": "bin/hopp"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -40,30 +39,27 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.6.6",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"commander": "^11.1.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"qs": "^6.11.2",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@swc/core": "^1.3.105",
|
"@swc/core": "^1.3.92",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/qs": "^6.9.11",
|
"@types/qs": "^6.9.8",
|
||||||
"fp-ts": "^2.16.2",
|
"axios": "^0.21.4",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"esm": "^3.2.25",
|
||||||
|
"fp-ts": "^2.16.1",
|
||||||
|
"io-ts": "^2.2.20",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.0.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.1",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^7.2.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.2.2",
|
||||||
"verzod": "^0.2.2",
|
"verzod": "^0.2.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestD",
|
"name": "RequestD",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -40,8 +40,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestC",
|
"name": "RequestC",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -68,8 +67,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -106,7 +104,6 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
"requestVariables": [],
|
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -119,7 +116,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -135,7 +132,6 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
"requestVariables": [],
|
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -162,7 +158,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -178,7 +174,6 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
"requestVariables": [],
|
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -191,7 +186,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -207,7 +202,6 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
"requestVariables": [],
|
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "<<URL>>",
|
"endpoint": "<<URL>>",
|
||||||
"name": "test1",
|
"name": "test1",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -16,8 +16,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,11 +23,10 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -45,8 +44,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,11 +22,10 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -44,8 +43,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,11 +23,10 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -45,8 +44,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "sample-req",
|
"name": "sample-req",
|
||||||
@@ -13,8 +13,7 @@
|
|||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
||||||
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
|
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": true },
|
"auth": { "authType": "inherit", "authActive": true },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"name": "test-request",
|
"name": "test-request",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
@@ -19,8 +19,7 @@
|
|||||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||||
},
|
},
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
|
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
@@ -17,13 +17,12 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>/headers",
|
"endpoint": "<<baseURL>>/headers",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": {
|
"body": {
|
||||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
@@ -33,13 +32,12 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>/post",
|
"endpoint": "<<baseURL>>/post",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-query-params",
|
"name": "test-secret-query-params",
|
||||||
@@ -52,13 +50,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>/get",
|
"endpoint": "<<baseURL>>/get",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
@@ -70,13 +67,12 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -89,20 +85,18 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-fallback",
|
"name": "test-secret-fallback",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"requestVariables": [],
|
|
||||||
"endpoint": "<<baseURL>>",
|
"endpoint": "<<baseURL>>",
|
||||||
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
"name": "test-secret-headers-overrides",
|
"name": "test-secret-headers-overrides",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -54,7 +52,7 @@
|
|||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -66,14 +64,13 @@
|
|||||||
"name": "test-secret-body",
|
"name": "test-secret-body",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/post",
|
"endpoint": "<<baseURL>>/post",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"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": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -91,14 +88,13 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/get",
|
"endpoint": "<<baseURL>>/get",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
@@ -112,14 +108,13 @@
|
|||||||
"name": "test-secret-basic-auth",
|
"name": "test-secret-basic-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
"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": "1",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -134,7 +129,6 @@
|
|||||||
"name": "test-secret-bearer-auth",
|
"name": "test-secret-bearer-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"requestVariables": [],
|
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "2",
|
"v": "1",
|
||||||
"endpoint": "https://httpbin.org/post",
|
"endpoint": "https://httpbin.org/post",
|
||||||
"name": "req",
|
"name": "req",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
||||||
},
|
}
|
||||||
"requestVariables": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": false },
|
"auth": { "authType": "inherit", "authActive": false },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { Command } from "commander";
|
import { program } from "commander";
|
||||||
import * as E from "fp-ts/Either";
|
import * as E from "fp-ts/Either";
|
||||||
import { version } from "../package.json";
|
import { version } from "../package.json";
|
||||||
import { test } from "./commands/test";
|
import { test } from "./commands/test";
|
||||||
@@ -20,8 +20,6 @@ const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
|||||||
"https://docs.hoppscotch.io/documentation/clients/cli"
|
"https://docs.hoppscotch.io/documentation/clients/cli"
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
const program = new Command()
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.name("hopp")
|
.name("hopp")
|
||||||
.version(version, "-v, --ver", "see the current version of hopp-cli")
|
.version(version, "-v, --ver", "see the current version of hopp-cli")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import chalk from "chalk";
|
import { bold } from "chalk";
|
||||||
import { log } from "console";
|
import { log } from "console";
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import { pipe } from "fp-ts/function";
|
import { pipe } from "fp-ts/function";
|
||||||
import { round } from "lodash-es";
|
import round from "lodash/round";
|
||||||
|
|
||||||
import { CollectionRunnerParam } from "../types/collections";
|
import { CollectionRunnerParam } from "../types/collections";
|
||||||
import {
|
import {
|
||||||
@@ -68,7 +68,7 @@ export const collectionsRunner = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Request processing initiated message.
|
// Request processing initiated message.
|
||||||
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
|
log(WARN(`\nRunning: ${bold(requestPath)}`));
|
||||||
|
|
||||||
// Processing current request.
|
// Processing current request.
|
||||||
const result = await processRequest(processRequestParams)();
|
const result = await processRequest(processRequestParams)();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import chalk from "chalk";
|
import { bold } from "chalk";
|
||||||
import { groupEnd, group, log } from "console";
|
import { groupEnd, group, log } from "console";
|
||||||
import { handleError } from "../handlers/error";
|
import { handleError } from "../handlers/error";
|
||||||
import { RequestConfig } from "../interfaces/request";
|
import { RequestConfig } from "../interfaces/request";
|
||||||
@@ -120,7 +120,7 @@ export const printErrorsReport = (
|
|||||||
errorsReport: HoppCLIError[]
|
errorsReport: HoppCLIError[]
|
||||||
) => {
|
) => {
|
||||||
if (errorsReport.length > 0) {
|
if (errorsReport.length > 0) {
|
||||||
const REPORTED_ERRORS_TITLE = FAIL(`\n${chalk.bold(path)} reported errors:`);
|
const REPORTED_ERRORS_TITLE = FAIL(`\n${bold(path)} reported errors:`);
|
||||||
|
|
||||||
group(REPORTED_ERRORS_TITLE);
|
group(REPORTED_ERRORS_TITLE);
|
||||||
for (const errorReport of errorsReport) {
|
for (const errorReport of errorsReport) {
|
||||||
@@ -143,7 +143,7 @@ export const printFailedTestsReport = (
|
|||||||
|
|
||||||
// Only printing test-reports with failed test-cases.
|
// Only printing test-reports with failed test-cases.
|
||||||
if (failedTestsReport.length > 0) {
|
if (failedTestsReport.length > 0) {
|
||||||
const FAILED_TESTS_PATH = FAIL(`\n${chalk.bold(path)} failed tests:`);
|
const FAILED_TESTS_PATH = FAIL(`\n${bold(path)} failed tests:`);
|
||||||
group(FAILED_TESTS_PATH);
|
group(FAILED_TESTS_PATH);
|
||||||
|
|
||||||
for (const failedTestReport of failedTestsReport) {
|
for (const failedTestReport of failedTestsReport) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clone } from "lodash-es";
|
import { clone } from "lodash";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts the array based on the sort func.
|
* Sorts the array based on the sort func.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as E from "fp-ts/Either";
|
|||||||
import * as S from "fp-ts/string";
|
import * as S from "fp-ts/string";
|
||||||
import * as O from "fp-ts/Option";
|
import * as O from "fp-ts/Option";
|
||||||
import { error } from "../types/errors";
|
import { error } from "../types/errors";
|
||||||
import { round } from "lodash-es";
|
import round from "lodash/round";
|
||||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ES6",
|
||||||
"module": "ESNext",
|
"module": "commonjs",
|
||||||
"outDir": ".",
|
"outDir": ".",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ import { defineConfig } from "tsup";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: [ "./src/index.ts" ],
|
entry: [ "./src/index.ts" ],
|
||||||
outDir: "./dist/",
|
outDir: "./dist/",
|
||||||
format: ["esm"],
|
format: ["cjs"],
|
||||||
platform: "node",
|
platform: "node",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
target: "esnext",
|
target: "node12",
|
||||||
skipNodeModulesBundle: false,
|
skipNodeModulesBundle: false,
|
||||||
esbuildOptions(options) {
|
esbuildOptions(options) {
|
||||||
options.bundle = true
|
options.bundle = true
|
||||||
},
|
},
|
||||||
|
noExternal: [
|
||||||
|
/\w+/
|
||||||
|
],
|
||||||
clean: true,
|
clean: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -563,22 +563,12 @@ details[open] summary .indicator {
|
|||||||
.env-highlight {
|
.env-highlight {
|
||||||
@apply text-accentContrast;
|
@apply text-accentContrast;
|
||||||
|
|
||||||
&.request-variable-highlight {
|
&.env-found {
|
||||||
@apply bg-amber-500;
|
@apply bg-accentDark;
|
||||||
@apply hover:bg-amber-600;
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.environment-variable-highlight {
|
&.env-not-found {
|
||||||
@apply bg-green-500;
|
|
||||||
@apply hover:bg-green-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.global-variable-highlight {
|
|
||||||
@apply bg-blue-500;
|
|
||||||
@apply hover:bg-blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.environment-not-found-highlight {
|
|
||||||
@apply bg-red-500;
|
@apply bg-red-500;
|
||||||
@apply hover:bg-red-600;
|
@apply hover:bg-red-600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,6 @@
|
|||||||
"pending_invites": "There are no pending invites for this team",
|
"pending_invites": "There are no pending invites for this team",
|
||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"request_variables": "This request does not have any request variables",
|
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"secret_environments": "Secrets are not synced to Hoppscotch",
|
"secret_environments": "Secrets are not synced to Hoppscotch",
|
||||||
"shared_requests": "Shared requests are empty",
|
"shared_requests": "Shared requests are empty",
|
||||||
@@ -316,8 +315,7 @@
|
|||||||
"proxy_error": "Proxy error",
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"test_script_fail": "Could not execute post-request script",
|
"test_script_fail": "Could not execute post-request script"
|
||||||
"reading_files": "Error while reading one or more files."
|
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"as_json": "Export as JSON",
|
"as_json": "Export as JSON",
|
||||||
@@ -415,10 +413,7 @@
|
|||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"postman_environment": "Postman Environment",
|
"postman_environment": "Postman Environment",
|
||||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||||
"title": "Import",
|
"title": "Import"
|
||||||
"file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of 10MB. Only the first {files} selected will be imported",
|
|
||||||
"file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of 10MB. Please select another file.",
|
|
||||||
"success": "Successfully imported"
|
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
@@ -561,7 +556,6 @@
|
|||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
"rename": "Rename Request",
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"request_variables": "Request variables",
|
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.12.6",
|
"version": "2023.12.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
|
|||||||
step: UrlSource({
|
step: UrlSource({
|
||||||
caption: "import.from_url",
|
caption: "import.from_url",
|
||||||
onImportFromURL: async (content) => {
|
onImportFromURL: async (content) => {
|
||||||
const res = await hoppOpenAPIImporter([content])()
|
const res = await hoppOpenAPIImporter(content)()
|
||||||
|
|
||||||
if (E.isRight(res)) {
|
if (E.isRight(res)) {
|
||||||
handleImportToStore(res.right)
|
handleImportToStore(res.right)
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
|||||||
let target = collections[indexPaths.shift() as number]
|
let target = collections[indexPaths.shift() as number]
|
||||||
|
|
||||||
while (indexPaths.length > 0)
|
while (indexPaths.length > 0)
|
||||||
target = target?.folders[indexPaths.shift() as number]
|
target = target.folders[indexPaths.shift() as number]
|
||||||
|
|
||||||
return target !== undefined ? target : null
|
return target !== undefined ? target : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,8 +614,8 @@ const addNewRootCollection = (name: string) => {
|
|||||||
requests: [],
|
requests: [],
|
||||||
headers: [],
|
headers: [],
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "inherit",
|
||||||
authActive: true,
|
authActive: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,13 +32,9 @@
|
|||||||
{{ tab.document.request.method }}
|
{{ tab.document.request.method }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="flex items-center flex-1 flex-shrink-0 min-w-0 truncate rounded-r"
|
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
|
||||||
>
|
>
|
||||||
<SmartEnvInput
|
{{ tab.document.request.endpoint }}
|
||||||
v-model="tab.document.request.endpoint"
|
|
||||||
:readonly="true"
|
|
||||||
:envs="tabRequestVariables"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 space-x-2 sm:mt-0">
|
<div class="flex mt-2 space-x-2 sm:mt-0">
|
||||||
@@ -73,7 +69,6 @@
|
|||||||
v-model="tab.document.request"
|
v-model="tab.document.request"
|
||||||
v-model:option-tab="selectedOptionTab"
|
v-model:option-tab="selectedOptionTab"
|
||||||
:properties="properties"
|
:properties="properties"
|
||||||
:envs="tabRequestVariables"
|
|
||||||
/>
|
/>
|
||||||
<HttpResponse
|
<HttpResponse
|
||||||
v-if="tab.document.response"
|
v-if="tab.document.response"
|
||||||
@@ -122,15 +117,6 @@ const sharedRequestURL = computed(() => {
|
|||||||
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabRequestVariables = computed(() => {
|
|
||||||
return tab.value.document.request.requestVariables.map(({ key, value }) => ({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
secret: false,
|
|
||||||
sourceEnv: "RequestVariable",
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportToStore(res.right)
|
handleImportToStore([res.right])
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -166,14 +166,19 @@ const insomniaEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalEnvs = res.right.filter(
|
const globalEnvIndex = res.right.findIndex(
|
||||||
(env) => env.name === "Base Environment"
|
(env) => env.name === "Base Environment"
|
||||||
)
|
)
|
||||||
const otherEnvs = res.right.filter(
|
|
||||||
(env) => env.name !== "Base Environment"
|
|
||||||
)
|
|
||||||
|
|
||||||
handleImportToStore(otherEnvs, globalEnvs)
|
const globalEnv =
|
||||||
|
globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
|
||||||
|
|
||||||
|
// remove the global env from the environments array to prevent it from being imported twice
|
||||||
|
if (globalEnvIndex !== -1) {
|
||||||
|
res.right.splice(globalEnvIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right, globalEnv)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -335,14 +340,14 @@ const showImportFailedError = () => {
|
|||||||
|
|
||||||
const handleImportToStore = async (
|
const handleImportToStore = async (
|
||||||
environments: Environment[],
|
environments: Environment[],
|
||||||
globalEnvs: NonSecretEnvironment[] = []
|
globalEnv?: NonSecretEnvironment
|
||||||
) => {
|
) => {
|
||||||
// Add global envs to the store
|
// if there's a global env, add them to the store
|
||||||
globalEnvs.forEach(({ variables }) => {
|
if (globalEnv) {
|
||||||
variables.forEach(({ key, value, secret }) => {
|
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||||
addGlobalEnvVariable({ key, value, secret })
|
addGlobalEnvVariable({ key, value, secret })
|
||||||
})
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
appendEnvironments(environments)
|
appendEnvironments(environments)
|
||||||
|
|||||||
@@ -31,6 +31,17 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -43,17 +54,6 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -284,7 +284,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -43,17 +54,6 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="auth.authType === 'basic'">
|
<div v-if="auth.authType === 'basic'">
|
||||||
<HttpAuthorizationBasic v-model="auth" :envs="envs" />
|
<HttpAuthorizationBasic v-model="auth" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'inherit'" class="p-4">
|
<div v-if="auth.authType === 'inherit'" class="p-4">
|
||||||
<span v-if="inheritedProperties?.auth">
|
<span v-if="inheritedProperties?.auth">
|
||||||
@@ -169,26 +169,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'bearer'">
|
<div v-if="auth.authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||||
v-model="auth.token"
|
|
||||||
placeholder="Token"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'oauth-2'">
|
<div v-if="auth.authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||||
v-model="auth.token"
|
|
||||||
placeholder="Token"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
|
<HttpOAuth2Authorization v-model="auth" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'api-key'">
|
<div v-if="auth.authType === 'api-key'">
|
||||||
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
|
<HttpAuthorizationApiKey v-model="auth" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -224,7 +215,6 @@ import { useColorMode } from "@composables/theming"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { onMounted } from "vue"
|
import { onMounted } from "vue"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -235,7 +225,6 @@ const props = defineProps<{
|
|||||||
isCollectionProperty?: boolean
|
isCollectionProperty?: boolean
|
||||||
isRootCollection?: boolean
|
isRootCollection?: boolean
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -276,7 +265,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,12 +100,10 @@
|
|||||||
<HttpBodyParameters
|
<HttpBodyParameters
|
||||||
v-if="body.contentType === 'multipart/form-data'"
|
v-if="body.contentType === 'multipart/form-data'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
<HttpURLEncodedParams
|
<HttpURLEncodedParams
|
||||||
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -143,7 +141,6 @@ import IconExternalLink from "~icons/lucide/external-link"
|
|||||||
import IconInfo from "~icons/lucide/info"
|
import IconInfo from "~icons/lucide/info"
|
||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
import { RESTOptionTabs } from "./RequestOptions.vue"
|
import { RESTOptionTabs } from "./RequestOptions.vue"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -151,7 +148,6 @@ const t = useI18n()
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
body: HoppRESTReqBody
|
body: HoppRESTReqBody
|
||||||
headers: HoppRESTHeader[]
|
headers: HoppRESTHeader[]
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -65,8 +65,6 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="entry.key"
|
v-model="entry.key"
|
||||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateBodyParam(index, {
|
updateBodyParam(index, {
|
||||||
key: $event,
|
key: $event,
|
||||||
@@ -89,8 +87,6 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="entry.value"
|
v-model="entry.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateBodyParam(index, {
|
updateBodyParam(index, {
|
||||||
key: entry.key,
|
key: entry.key,
|
||||||
@@ -194,13 +190,11 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Body
|
modelValue: Body
|
||||||
envs: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -188,25 +188,11 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
|
|
||||||
const requestCode = computed(() => {
|
const requestCode = computed(() => {
|
||||||
const aggregateEnvs = getAggregateEnvs()
|
const aggregateEnvs = getAggregateEnvs()
|
||||||
const requestVariables = request.value.requestVariables.map(
|
|
||||||
(requestVariable) => {
|
|
||||||
if (requestVariable.active)
|
|
||||||
return {
|
|
||||||
key: requestVariable.key,
|
|
||||||
value: requestVariable.value,
|
|
||||||
secret: false,
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
v: 1,
|
v: 1,
|
||||||
id: "env",
|
id: "env",
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: [
|
variables: aggregateEnvs,
|
||||||
...(requestVariables as Environment["variables"]),
|
|
||||||
...aggregateEnvs,
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
|
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
|
||||||
|
|
||||||
@@ -226,12 +212,6 @@ const requestCode = computed(() => {
|
|||||||
active: true,
|
active: true,
|
||||||
})),
|
})),
|
||||||
endpoint: effectiveRequest.effectiveFinalURL,
|
endpoint: effectiveRequest.effectiveFinalURL,
|
||||||
requestVariables: effectiveRequest.effectiveFinalRequestVariables.map(
|
|
||||||
(requestVariable) => ({
|
|
||||||
...requestVariable,
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@click="clearContent()"
|
@click="clearContent()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="bulkHeaders"
|
v-if="bulkMode"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': WRAP_LINES }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
@@ -92,8 +92,6 @@
|
|||||||
:auto-complete-source="commonHeaders"
|
:auto-complete-source="commonHeaders"
|
||||||
:env-index="index"
|
:env-index="index"
|
||||||
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -110,8 +108,6 @@
|
|||||||
getInspectorResult(headerValueResults, index)
|
getInspectorResult(headerValueResults, index)
|
||||||
"
|
"
|
||||||
:env-index="index"
|
:env-index="index"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -333,11 +329,7 @@ import {
|
|||||||
getComputedHeaders,
|
getComputedHeaders,
|
||||||
getComputedAuthHeaders,
|
getComputedAuthHeaders,
|
||||||
} from "~/helpers/utils/EffectiveURL"
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
import {
|
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||||
AggregateEnvironment,
|
|
||||||
aggregateEnvs$,
|
|
||||||
getAggregateEnvs,
|
|
||||||
} from "~/newstore/environments"
|
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||||
@@ -367,7 +359,6 @@ const props = defineProps<{
|
|||||||
modelValue: HoppRESTRequest
|
modelValue: HoppRESTRequest
|
||||||
isCollectionProperty?: boolean
|
isCollectionProperty?: boolean
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -6,52 +6,31 @@
|
|||||||
:styles="
|
:styles="
|
||||||
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
||||||
"
|
"
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="OpenID Connect Discovery URL"
|
placeholder="OpenID Connect Discovery URL"
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="authURL"
|
v-model="authURL"
|
||||||
placeholder="Authorization URL"
|
placeholder="Authorization URL"
|
||||||
:auto-complete-env="true"
|
|
||||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
:envs="envs"
|
></SmartEnvInput>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="accessTokenURL"
|
v-model="accessTokenURL"
|
||||||
placeholder="Access Token URL"
|
placeholder="Access Token URL"
|
||||||
:auto-complete-env="true"
|
|
||||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||||
v-model="clientID"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="Client ID"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="clientSecret" placeholder="Client Secret" />
|
||||||
v-model="clientSecret"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="Client Secret"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="scope" placeholder="Scope" />
|
||||||
v-model="scope"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="Scope"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
@@ -83,7 +62,6 @@ const toast = useToast()
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -87,8 +87,6 @@
|
|||||||
:inspection-results="
|
:inspection-results="
|
||||||
getInspectorResult(parameterKeyResults, index)
|
getInspectorResult(parameterKeyResults, index)
|
||||||
"
|
"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -104,8 +102,6 @@
|
|||||||
:inspection-results="
|
:inspection-results="
|
||||||
getInspectorResult(parameterValueResults, index)
|
getInspectorResult(parameterValueResults, index)
|
||||||
"
|
"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -213,7 +209,6 @@ import { InspectionService, InspectorResult } from "~/services/inspection"
|
|||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { useNestedSetting } from "~/composables/settings"
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
@@ -247,7 +242,6 @@ useCodemirror(
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTParam[]
|
modelValue: HoppRESTParam[]
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
v-model="tab.document.request.endpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url')}`"
|
:placeholder="`${t('request.url')}`"
|
||||||
:auto-complete-source="userHistories"
|
:auto-complete-source="userHistories"
|
||||||
:auto-complete-env="true"
|
|
||||||
:inspection-results="tabResults"
|
:inspection-results="tabResults"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@enter="newSendRequest"
|
@enter="newSendRequest"
|
||||||
|
|||||||
@@ -5,51 +5,48 @@
|
|||||||
render-inactive-tabs
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('params') ?? true"
|
v-if="properties ? properties.includes('params') : true"
|
||||||
:id="'params'"
|
:id="'params'"
|
||||||
:label="`${t('tab.parameters')}`"
|
:label="`${t('tab.parameters')}`"
|
||||||
:info="`${newActiveParamsCount}`"
|
:info="`${newActiveParamsCount$}`"
|
||||||
>
|
>
|
||||||
<HttpParameters v-model="request.params" :envs="envs" />
|
<HttpParameters v-model="request.params" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('bodyParams') ?? true"
|
v-if="properties ? properties.includes('bodyParams') : true"
|
||||||
:id="'bodyParams'"
|
:id="'bodyParams'"
|
||||||
:label="`${t('tab.body')}`"
|
:label="`${t('tab.body')}`"
|
||||||
>
|
>
|
||||||
<HttpBody
|
<HttpBody
|
||||||
v-model:headers="request.headers"
|
v-model:headers="request.headers"
|
||||||
v-model:body="request.body"
|
v-model:body="request.body"
|
||||||
:envs="envs"
|
|
||||||
@change-tab="changeOptionTab"
|
@change-tab="changeOptionTab"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('headers') ?? true"
|
v-if="properties ? properties.includes('headers') : true"
|
||||||
:id="'headers'"
|
:id="'headers'"
|
||||||
:label="`${t('tab.headers')}`"
|
:label="`${t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount}`"
|
:info="`${newActiveHeadersCount$}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders
|
<HttpHeaders
|
||||||
v-model="request"
|
v-model="request"
|
||||||
:inherited-properties="inheritedProperties"
|
:inherited-properties="inheritedProperties"
|
||||||
:envs="envs"
|
|
||||||
@change-tab="changeOptionTab"
|
@change-tab="changeOptionTab"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('authorization') ?? true"
|
v-if="properties ? properties.includes('authorization') : true"
|
||||||
:id="'authorization'"
|
:id="'authorization'"
|
||||||
:label="`${t('tab.authorization')}`"
|
:label="`${t('tab.authorization')}`"
|
||||||
>
|
>
|
||||||
<HttpAuthorization
|
<HttpAuthorization
|
||||||
v-model="request.auth"
|
v-model="request.auth"
|
||||||
:inherited-properties="inheritedProperties"
|
:inherited-properties="inheritedProperties"
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('preRequestScript') ?? true"
|
v-if="properties ? properties.includes('preRequestScript') : true"
|
||||||
:id="'preRequestScript'"
|
:id="'preRequestScript'"
|
||||||
:label="`${t('tab.pre_request_script')}`"
|
:label="`${t('tab.pre_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -61,7 +58,7 @@
|
|||||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties?.includes('tests') ?? true"
|
v-if="properties ? properties.includes('tests') : true"
|
||||||
:id="'tests'"
|
:id="'tests'"
|
||||||
:label="`${t('tab.tests')}`"
|
:label="`${t('tab.tests')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -70,15 +67,6 @@
|
|||||||
>
|
>
|
||||||
<HttpTests v-model="request.testScript" />
|
<HttpTests v-model="request.testScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
|
||||||
v-if="properties?.includes('requestVariables') ?? true"
|
|
||||||
:id="'requestVariables'"
|
|
||||||
:label="`${t('tab.variables')}`"
|
|
||||||
:info="`${newActiveRequestVariablesCount}`"
|
|
||||||
:align-last="true"
|
|
||||||
>
|
|
||||||
<HttpRequestVariables v-model="request.requestVariables" />
|
|
||||||
</HoppSmartTab>
|
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -89,7 +77,6 @@ import { useVModel } from "@vueuse/core"
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const VALID_OPTION_TABS = [
|
const VALID_OPTION_TABS = [
|
||||||
"params",
|
"params",
|
||||||
@@ -98,7 +85,6 @@ const VALID_OPTION_TABS = [
|
|||||||
"authorization",
|
"authorization",
|
||||||
"preRequestScript",
|
"preRequestScript",
|
||||||
"tests",
|
"tests",
|
||||||
"requestVariables",
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
|
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
|
||||||
@@ -112,7 +98,6 @@ const props = withDefaults(
|
|||||||
optionTab: RESTOptionTabs
|
optionTab: RESTOptionTabs
|
||||||
properties?: string[]
|
properties?: string[]
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
optionTab: "params",
|
optionTab: "params",
|
||||||
@@ -131,27 +116,22 @@ const changeOptionTab = (e: RESTOptionTabs) => {
|
|||||||
selectedOptionTab.value = e
|
selectedOptionTab.value = e
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActiveParamsCount = computed(() => {
|
const newActiveParamsCount$ = computed(() => {
|
||||||
const count = request.value.params.filter(
|
const e = request.value.params.filter(
|
||||||
(x) => x.active && (x.key || x.value)
|
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||||
).length
|
).length
|
||||||
|
|
||||||
return count ? count : null
|
if (e === 0) return null
|
||||||
|
return `${e}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const newActiveHeadersCount = computed(() => {
|
const newActiveHeadersCount$ = computed(() => {
|
||||||
const count = request.value.headers.filter(
|
const e = request.value.headers.filter(
|
||||||
(x) => x.active && (x.key || x.value)
|
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||||
).length
|
).length
|
||||||
|
|
||||||
return count ? count : null
|
if (e === 0) return null
|
||||||
})
|
return `${e}`
|
||||||
|
|
||||||
const newActiveRequestVariablesCount = computed(() => {
|
|
||||||
const count = request.value.requestVariables.filter(
|
|
||||||
(x) => x.active && (x.key || x.value)
|
|
||||||
).length
|
|
||||||
return count ? count : null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||||
|
|||||||
@@ -1,412 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<div
|
|
||||||
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
|
|
||||||
>
|
|
||||||
<label class="truncate font-semibold text-secondaryLight">
|
|
||||||
{{ t("request.request_variables") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing"
|
|
||||||
blank
|
|
||||||
:title="t('app.wiki')"
|
|
||||||
:icon="IconHelpCircle"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="bulkVariables"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.linewrap')"
|
|
||||||
:class="{ '!text-accent': WRAP_LINES }"
|
|
||||||
:icon="IconWrapText"
|
|
||||||
@click.prevent="
|
|
||||||
toggleNestedSetting('WRAP_LINES', 'httpRequestVariables')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('state.bulk_mode')"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:class="{ '!text-accent': bulkMode }"
|
|
||||||
@click="bulkMode = !bulkMode"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('add.new')"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:disabled="bulkMode"
|
|
||||||
@click="addVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="bulkMode" class="h-full relative">
|
|
||||||
<div ref="bulkEditor" class="absolute inset-0"></div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<draggable
|
|
||||||
v-model="workingRequestVariables"
|
|
||||||
item-key="id"
|
|
||||||
animation="250"
|
|
||||||
handle=".draggable-handle"
|
|
||||||
draggable=".draggable-content"
|
|
||||||
ghost-class="cursor-move"
|
|
||||||
chosen-class="bg-primaryLight"
|
|
||||||
drag-class="cursor-grabbing"
|
|
||||||
>
|
|
||||||
<template #item="{ element: variable, index }">
|
|
||||||
<div
|
|
||||||
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{
|
|
||||||
theme: 'tooltip',
|
|
||||||
delay: [500, 20],
|
|
||||||
content:
|
|
||||||
index !== workingRequestVariables?.length - 1
|
|
||||||
? t('action.drag_to_reorder')
|
|
||||||
: null,
|
|
||||||
}"
|
|
||||||
:icon="IconGripVertical"
|
|
||||||
class="opacity-0"
|
|
||||||
:class="{
|
|
||||||
'draggable-handle cursor-grab group-hover:opacity-100':
|
|
||||||
index !== workingRequestVariables?.length - 1,
|
|
||||||
}"
|
|
||||||
tabindex="-1"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="variable.key"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
@change="
|
|
||||||
updateVariable(index, {
|
|
||||||
id: variable.id,
|
|
||||||
key: $event,
|
|
||||||
value: variable.value,
|
|
||||||
active: variable.active,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="variable.value"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
@change="
|
|
||||||
updateVariable(index, {
|
|
||||||
id: variable.id,
|
|
||||||
key: variable.key,
|
|
||||||
value: $event,
|
|
||||||
active: variable.active,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
variable.hasOwnProperty('active')
|
|
||||||
? variable.active
|
|
||||||
? t('action.turn_off')
|
|
||||||
: t('action.turn_on')
|
|
||||||
: t('action.turn_off')
|
|
||||||
"
|
|
||||||
:icon="
|
|
||||||
variable.hasOwnProperty('active')
|
|
||||||
? variable.active
|
|
||||||
? IconCheckCircle
|
|
||||||
: IconCircle
|
|
||||||
: IconCheckCircle
|
|
||||||
"
|
|
||||||
color="green"
|
|
||||||
@click="
|
|
||||||
updateVariable(index, {
|
|
||||||
id: variable.id,
|
|
||||||
key: variable.key,
|
|
||||||
value: variable.value,
|
|
||||||
active: variable.hasOwnProperty('active')
|
|
||||||
? !variable.active
|
|
||||||
: false,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="deleteVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="workingRequestVariables.length === 0"
|
|
||||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
|
||||||
:alt="`${t('empty.request_variables')}`"
|
|
||||||
:text="t('empty.request_variables')"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="`${t('add.new')}`"
|
|
||||||
:icon="IconPlus"
|
|
||||||
filled
|
|
||||||
@click="addVariable"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { throwError } from "@functional/error"
|
|
||||||
import {
|
|
||||||
HoppRESTRequestVariable,
|
|
||||||
RawKeyValueEntry,
|
|
||||||
parseRawKeyValueEntriesE,
|
|
||||||
rawKeyValueEntriesToString,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { useVModel } from "@vueuse/core"
|
|
||||||
import * as A from "fp-ts/Array"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray"
|
|
||||||
import { flow, pipe } from "fp-ts/function"
|
|
||||||
import { cloneDeep, isEqual } from "lodash-es"
|
|
||||||
import { reactive, ref, watch } from "vue"
|
|
||||||
import draggable from "vuedraggable-es"
|
|
||||||
|
|
||||||
import { useCodemirror } from "~/composables/codemirror"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
import { useNestedSetting } from "~/composables/settings"
|
|
||||||
import { useColorMode } from "~/composables/theming"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconCircle from "~icons/lucide/circle"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconGripVertical from "~icons/lucide/grip-vertical"
|
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
|
||||||
import IconPlus from "~icons/lucide/plus"
|
|
||||||
import IconTrash from "~icons/lucide/trash"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const bulkMode = ref(false)
|
|
||||||
const bulkEditor = ref<any | null>(null)
|
|
||||||
const bulkVariables = ref("")
|
|
||||||
|
|
||||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestVariables")
|
|
||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: HoppRESTRequestVariable[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update:modelValue", value: Array<HoppRESTRequestVariable>): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// The functional requestVariable list (the requestVariable actually applied to the session)
|
|
||||||
const requestVariables = useVModel(props, "modelValue", emit)
|
|
||||||
|
|
||||||
useCodemirror(
|
|
||||||
bulkEditor,
|
|
||||||
bulkVariables,
|
|
||||||
reactive({
|
|
||||||
extendedEditorConfig: {
|
|
||||||
mode: "text/x-yaml",
|
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
|
||||||
lineWrapping: WRAP_LINES,
|
|
||||||
},
|
|
||||||
linter,
|
|
||||||
completer: null,
|
|
||||||
environmentHighlights: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const idTicker = ref(0)
|
|
||||||
|
|
||||||
const workingRequestVariables = ref<
|
|
||||||
Array<HoppRESTRequestVariable & { id: number }>
|
|
||||||
>([
|
|
||||||
{
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// Sync logic between params and working/bulk params
|
|
||||||
watch(
|
|
||||||
requestVariables,
|
|
||||||
(newRequestVariableList) => {
|
|
||||||
// Sync should overwrite working params
|
|
||||||
const filteredWorkingRequestVariables: HoppRESTRequestVariable[] = pipe(
|
|
||||||
workingRequestVariables.value,
|
|
||||||
A.filterMap(
|
|
||||||
flow(
|
|
||||||
O.fromPredicate((e) => e.key !== ""),
|
|
||||||
O.map(objRemoveKey("id"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredBulkRequestVariables = pipe(
|
|
||||||
parseRawKeyValueEntriesE(bulkVariables.value),
|
|
||||||
E.map(
|
|
||||||
flow(
|
|
||||||
RA.filter((e) => e.key !== ""),
|
|
||||||
RA.toArray
|
|
||||||
)
|
|
||||||
),
|
|
||||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(newRequestVariableList, filteredWorkingRequestVariables)) {
|
|
||||||
workingRequestVariables.value = pipe(
|
|
||||||
newRequestVariableList,
|
|
||||||
A.map((x) => ({ id: idTicker.value++, ...x }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEqual(newRequestVariableList, filteredBulkRequestVariables)) {
|
|
||||||
bulkVariables.value = rawKeyValueEntriesToString(newRequestVariableList)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(workingRequestVariables, (newWorkingRequestVariables) => {
|
|
||||||
const fixedRequestVariables = pipe(
|
|
||||||
newWorkingRequestVariables,
|
|
||||||
A.filterMap(
|
|
||||||
flow(
|
|
||||||
O.fromPredicate((e) => e.key !== ""),
|
|
||||||
O.map(objRemoveKey("id"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(requestVariables.value, fixedRequestVariables)) {
|
|
||||||
requestVariables.value = cloneDeep(fixedRequestVariables)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(bulkVariables, (newBulkParams) => {
|
|
||||||
const filteredBulkRequestVariables = pipe(
|
|
||||||
parseRawKeyValueEntriesE(newBulkParams),
|
|
||||||
E.map(
|
|
||||||
flow(
|
|
||||||
RA.filter((e) => e.key !== ""),
|
|
||||||
RA.toArray
|
|
||||||
)
|
|
||||||
),
|
|
||||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isEqual(requestVariables.value, filteredBulkRequestVariables)) {
|
|
||||||
requestVariables.value = filteredBulkRequestVariables
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Rule: Working Request variable always have last element is always an empty param
|
|
||||||
watch(workingRequestVariables, (variableList) => {
|
|
||||||
if (
|
|
||||||
variableList.length > 0 &&
|
|
||||||
variableList[variableList.length - 1].key !== ""
|
|
||||||
) {
|
|
||||||
workingRequestVariables.value.push({
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const addVariable = () => {
|
|
||||||
workingRequestVariables.value.push({
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateVariable = (index: number, variable: any & { id: number }) => {
|
|
||||||
workingRequestVariables.value = workingRequestVariables.value.map((h, i) =>
|
|
||||||
i === index ? variable : h
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteVariable = (index: number) => {
|
|
||||||
const requestVariablesBeforeDeletion = cloneDeep(
|
|
||||||
workingRequestVariables.value
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
requestVariablesBeforeDeletion.length > 0 &&
|
|
||||||
index === requestVariablesBeforeDeletion.length - 1
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (deletionToast.value) {
|
|
||||||
deletionToast.value.goAway(0)
|
|
||||||
deletionToast.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
|
||||||
action: [
|
|
||||||
{
|
|
||||||
text: `${t("action.undo")}`,
|
|
||||||
onClick: (_, toastObject) => {
|
|
||||||
workingRequestVariables.value = requestVariablesBeforeDeletion
|
|
||||||
toastObject.goAway(0)
|
|
||||||
deletionToast.value = null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
onComplete: () => {
|
|
||||||
deletionToast.value = null
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
workingRequestVariables.value = pipe(
|
|
||||||
workingRequestVariables.value,
|
|
||||||
A.deleteAt(index),
|
|
||||||
O.getOrElseW(() =>
|
|
||||||
throwError("Working Request Variable Deletion Out of Bounds")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearContent = () => {
|
|
||||||
// set params list to the initial state
|
|
||||||
workingRequestVariables.value = [
|
|
||||||
{
|
|
||||||
id: idTicker.value++,
|
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
bulkVariables.value = ""
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
@click="clearContent()"
|
@click="clearContent()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="bulkUrlEncodedParams"
|
v-if="bulkMode"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': WRAP_LINES }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
@@ -84,8 +84,6 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.key"
|
v-model="param.key"
|
||||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateUrlEncodedParam(index, {
|
updateUrlEncodedParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -98,8 +96,6 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.value"
|
v-model="param.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
@change="
|
@change="
|
||||||
updateUrlEncodedParam(index, {
|
updateUrlEncodedParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -204,7 +200,6 @@ import { throwError } from "~/helpers/functional/error"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useNestedSetting } from "~/composables/settings"
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
type Body = HoppRESTReqBody & {
|
type Body = HoppRESTReqBody & {
|
||||||
contentType: "application/x-www-form-urlencoded"
|
contentType: "application/x-www-form-urlencoded"
|
||||||
@@ -212,7 +207,6 @@ type Body = HoppRESTReqBody & {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Body
|
modelValue: Body
|
||||||
envs: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.key" placeholder="Key" />
|
||||||
v-model="auth.key"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="Key"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.value" placeholder="Value" />
|
||||||
v-model="auth.value"
|
|
||||||
:auto-complete-env="true"
|
|
||||||
placeholder="Value"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center border-b border-dividerLight">
|
<div class="flex items-center border-b border-dividerLight">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@@ -75,13 +65,11 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
|
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthAPIKey
|
modelValue: HoppRESTAuthAPIKey
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -3,16 +3,12 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.username"
|
v-model="auth.username"
|
||||||
:placeholder="t('authorization.username')"
|
:placeholder="t('authorization.username')"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.password"
|
v-model="auth.password"
|
||||||
:placeholder="t('authorization.password')"
|
:placeholder="t('authorization.password')"
|
||||||
:auto-complete-env="true"
|
|
||||||
:envs="envs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,13 +17,11 @@
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { HoppRESTAuthBasic } from "@hoppscotch/data"
|
import { HoppRESTAuthBasic } from "@hoppscotch/data"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthBasic
|
modelValue: HoppRESTAuthBasic
|
||||||
envs?: AggregateEnvironment[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
{{ t(`${caption}`) }}
|
{{ t(`${caption}`) }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
||||||
>
|
>
|
||||||
@@ -24,30 +23,15 @@
|
|||||||
type="file"
|
type="file"
|
||||||
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
||||||
:accept="acceptedFileTypes"
|
:accept="acceptedFileTypes"
|
||||||
multiple
|
|
||||||
@change="onFileChange"
|
@change="onFileChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="showFileSizeLimitExceededWarning" class="text-red-500 ml-10">
|
|
||||||
<template v-if="importFilesCount">
|
|
||||||
{{
|
|
||||||
t("import.file_size_limit_exceeded_warning_multiple_files", {
|
|
||||||
files:
|
|
||||||
importFilesCount === 1 ? "file" : `${importFilesCount} files`,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
{{ t("import.file_size_limit_exceeded_warning_single_file") }}
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
<div>
|
<div>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
:disabled="!hasFile || showFileSizeLimitExceededWarning"
|
:disabled="!hasFile"
|
||||||
@click="emit('importFromFile', fileContent)"
|
@click="emit('importFromFile', fileContent)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,30 +51,16 @@ defineProps<{
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const ALLOWED_FILE_SIZE_LIMIT = 10 // 10 MB
|
|
||||||
|
|
||||||
const importFilesCount = ref(0)
|
|
||||||
|
|
||||||
const hasFile = ref(false)
|
const hasFile = ref(false)
|
||||||
const showFileSizeLimitExceededWarning = ref(false)
|
const fileContent = ref("")
|
||||||
const fileContent = ref<string[]>([])
|
|
||||||
|
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "importFromFile", content: string[]): void
|
(e: "importFromFile", content: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const onFileChange = async () => {
|
const onFileChange = () => {
|
||||||
// Reset the state on entering the handler to avoid any stale state
|
|
||||||
if (showFileSizeLimitExceededWarning.value) {
|
|
||||||
showFileSizeLimitExceededWarning.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importFilesCount.value) {
|
|
||||||
importFilesCount.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputFileToImport = inputChooseFileToImportFrom.value
|
const inputFileToImport = inputChooseFileToImportFrom.value
|
||||||
|
|
||||||
if (!inputFileToImport) {
|
if (!inputFileToImport) {
|
||||||
@@ -99,52 +69,27 @@ const onFileChange = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||||
inputChooseFileToImportFrom.value = ""
|
inputChooseFileToImportFrom.value[0].value = ""
|
||||||
hasFile.value = false
|
hasFile.value = false
|
||||||
toast.show(t("action.choose_file").toString())
|
toast.show(t("action.choose_file").toString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const readerPromises: Promise<string | null>[] = []
|
|
||||||
|
|
||||||
let totalFileSize = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < inputFileToImport.files.length; i++) {
|
|
||||||
const file = inputFileToImport.files[i]
|
|
||||||
|
|
||||||
totalFileSize += file.size / 1024 / 1024
|
|
||||||
|
|
||||||
if (totalFileSize > ALLOWED_FILE_SIZE_LIMIT) {
|
|
||||||
showFileSizeLimitExceededWarning.value = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
readerPromises.push(
|
reader.onload = ({ target }) => {
|
||||||
new Promise((resolve, reject) => {
|
const content = target!.result as string | null
|
||||||
reader.onload = () => resolve(reader.result as string | null)
|
if (!content) {
|
||||||
reader.onerror = reject
|
hasFile.value = false
|
||||||
reader.readAsText(file)
|
toast.show(t("action.choose_file").toString())
|
||||||
})
|
return
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
importFilesCount.value = readerPromises.length
|
fileContent.value = content
|
||||||
|
|
||||||
const results = await Promise.allSettled(readerPromises)
|
hasFile.value = !!content?.length
|
||||||
|
|
||||||
const contentsArr = results
|
|
||||||
.filter((result) => result.status === "fulfilled")
|
|
||||||
.map((result) => (result as { value: string | null }).value)
|
|
||||||
.filter(Boolean) as string[]
|
|
||||||
|
|
||||||
const errors = results.filter((result) => result.status === "rejected")
|
|
||||||
if (errors.length) {
|
|
||||||
toast.error(t("error.reading_files"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileContent.value = contentsArr
|
reader.readAsText(inputFileToImport.files[0])
|
||||||
hasFile.value = contentsArr.length > 0
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2 py-4">
|
<div class="flex flex-col space-y-2 py-4">
|
||||||
|
<span>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
blank
|
blank
|
||||||
@@ -30,6 +31,8 @@
|
|||||||
:active-info-icon="hasChromeExtInstalled"
|
:active-info-icon="hasChromeExtInstalled"
|
||||||
outline
|
outline
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
blank
|
blank
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
:active-info-icon="hasFirefoxExtInstalled"
|
:active-info-icon="hasFirefoxExtInstalled"
|
||||||
outline
|
outline
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4 py-4">
|
<div class="space-y-4 py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -290,12 +290,7 @@ const widgets: Widget[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
type EmbedTabs =
|
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
||||||
| "params"
|
|
||||||
| "bodyParams"
|
|
||||||
| "headers"
|
|
||||||
| "authorization"
|
|
||||||
| "requestVariables"
|
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
|
|||||||
@@ -56,12 +56,7 @@ import { useI18n } from "~/composables/i18n"
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
type EmbedTabs =
|
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
||||||
| "params"
|
|
||||||
| "bodyParams"
|
|
||||||
| "headers"
|
|
||||||
| "authorization"
|
|
||||||
| "requestVariables"
|
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
|
|||||||
@@ -173,11 +173,6 @@ const embedOptions = ref<EmbedOption>({
|
|||||||
label: t("tab.authorization"),
|
label: t("tab.authorization"),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "requestVariables",
|
|
||||||
label: t("tab.variables"),
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
})
|
})
|
||||||
@@ -228,12 +223,7 @@ const currentUser = useReadonlyStream(
|
|||||||
|
|
||||||
const step = ref(1)
|
const step = ref(1)
|
||||||
|
|
||||||
type EmbedTabs =
|
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
||||||
| "params"
|
|
||||||
| "bodyParams"
|
|
||||||
| "headers"
|
|
||||||
| "authorization"
|
|
||||||
| "requestVariables"
|
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
@@ -379,11 +369,6 @@ const displayCustomizeRequestModal = (
|
|||||||
label: t("tab.authorization"),
|
label: t("tab.authorization"),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "requestVariables",
|
|
||||||
label: t("tab.variables"),
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex overflow-x-scroll"
|
class="flex"
|
||||||
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
|
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -57,12 +57,7 @@ import { computed } from "vue"
|
|||||||
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
type Tabs =
|
type Tabs = "params" | "bodyParams" | "headers" | "authorization"
|
||||||
| "params"
|
|
||||||
| "bodyParams"
|
|
||||||
| "headers"
|
|
||||||
| "authorization"
|
|
||||||
| "requestVariables"
|
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: Tabs
|
selectedTab: Tabs
|
||||||
|
|||||||
@@ -79,27 +79,29 @@ import { history, historyKeymap } from "@codemirror/commands"
|
|||||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import {
|
||||||
|
AggregateEnvironment,
|
||||||
|
aggregateEnvsWithSecrets$,
|
||||||
|
} from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
import { InspectorResult } from "~/services/inspection"
|
import { InspectorResult } from "~/services/inspection"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconEye from "~icons/lucide/eye"
|
import IconEye from "~icons/lucide/eye"
|
||||||
import IconEyeoff from "~icons/lucide/eye-off"
|
import IconEyeoff from "~icons/lucide/eye-off"
|
||||||
import { CompletionContext, autocompletion } from "@codemirror/autocomplete"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
|
||||||
import { syntaxTree } from "@codemirror/language"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
type Env = Environment["variables"][number] & { source: string }
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
styles?: string
|
styles?: string
|
||||||
envs?: AggregateEnvironment[] | null
|
envs?: Env[] | null
|
||||||
focus?: boolean
|
focus?: boolean
|
||||||
selectTextOnMount?: boolean
|
selectTextOnMount?: boolean
|
||||||
environmentHighlights?: boolean
|
environmentHighlights?: boolean
|
||||||
@@ -108,7 +110,6 @@ const props = withDefaults(
|
|||||||
inspectionResults?: InspectorResult[] | undefined
|
inspectionResults?: InspectorResult[] | undefined
|
||||||
contextMenuEnabled?: boolean
|
contextMenuEnabled?: boolean
|
||||||
secret?: boolean
|
secret?: boolean
|
||||||
autoCompleteEnv?: boolean
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -123,7 +124,6 @@ const props = withDefaults(
|
|||||||
inspectionResults: undefined,
|
inspectionResults: undefined,
|
||||||
contextMenuEnabled: true,
|
contextMenuEnabled: true,
|
||||||
secret: false,
|
secret: false,
|
||||||
autoCompleteEnvSource: false,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -353,62 +353,29 @@ watch(
|
|||||||
let clipboardEv: ClipboardEvent | null = null
|
let clipboardEv: ClipboardEvent | null = null
|
||||||
let pastedValue: string | null = null
|
let pastedValue: string | null = null
|
||||||
|
|
||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
||||||
AggregateEnvironment[]
|
AggregateEnvironment[]
|
||||||
>
|
>
|
||||||
|
|
||||||
const tabs = useService(RESTTabService)
|
|
||||||
|
|
||||||
const envVars = computed(() => {
|
const envVars = computed(() => {
|
||||||
if (props.envs) {
|
return props.envs
|
||||||
return props.envs.map((x) => {
|
? props.envs.map((x) => {
|
||||||
const { key, secret } = x
|
if (x.secret) {
|
||||||
const value = secret ? "********" : x.value
|
|
||||||
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
|
|
||||||
return {
|
return {
|
||||||
key,
|
key: x.key,
|
||||||
value,
|
sourceEnv: "source" in x ? x.source : null,
|
||||||
sourceEnv,
|
value: "********",
|
||||||
secret,
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: x.key,
|
||||||
|
value: x.value,
|
||||||
|
sourceEnv: "source" in x ? x.source : null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
: aggregateEnvs.value
|
||||||
return [
|
|
||||||
...tabs.currentActiveTab.value.document.request.requestVariables.map(
|
|
||||||
({ active, key, value }) =>
|
|
||||||
active
|
|
||||||
? {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
sourceEnv: "RequestVariable",
|
|
||||||
secret: false,
|
|
||||||
}
|
|
||||||
: ({} as AggregateEnvironment)
|
|
||||||
),
|
|
||||||
...aggregateEnvs.value,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function envAutoCompletion(context: CompletionContext) {
|
|
||||||
const options = (envVars.value ?? [])
|
|
||||||
.map((env) => ({
|
|
||||||
label: env?.key ? `<<${env.key}>>` : "",
|
|
||||||
info: env?.value ?? "",
|
|
||||||
apply: env?.key ? `<<${env.key}>>` : "",
|
|
||||||
}))
|
|
||||||
.filter((x) => x)
|
|
||||||
|
|
||||||
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
|
|
||||||
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
|
|
||||||
const tagBefore = /<<\w*$/.exec(textBefore)
|
|
||||||
if (!tagBefore && !context.explicit) return null
|
|
||||||
return {
|
|
||||||
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
|
||||||
options: options,
|
|
||||||
validFor: /^(<<\w*)?$/,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||||
|
|
||||||
function handleTextSelection() {
|
function handleTextSelection() {
|
||||||
@@ -502,12 +469,6 @@ const getExtensions = (readonly: boolean): Extension => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
props.autoCompleteEnv
|
|
||||||
? autocompletion({
|
|
||||||
activateOnTyping: true,
|
|
||||||
override: [envAutoCompletion],
|
|
||||||
})
|
|
||||||
: [],
|
|
||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export function useCodemirror(
|
|||||||
if (from === to) return
|
if (from === to) return
|
||||||
const text = view.value?.state.doc.sliceString(from, to)
|
const text = view.value?.state.doc.sliceString(from, to)
|
||||||
const { top, left } = view.value?.coordsAtPos(from)
|
const { top, left } = view.value?.coordsAtPos(from)
|
||||||
if (text?.trim()) {
|
if (text) {
|
||||||
invokeAction("contextmenu.open", {
|
invokeAction("contextmenu.open", {
|
||||||
position: {
|
position: {
|
||||||
top,
|
top,
|
||||||
|
|||||||
293
packages/hoppscotch-common/src/helpers/RESTRequest.ts
Normal file
293
packages/hoppscotch-common/src/helpers/RESTRequest.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import {
|
||||||
|
FormDataKeyValue,
|
||||||
|
HoppRESTAuth,
|
||||||
|
HoppRESTHeader,
|
||||||
|
HoppRESTParam,
|
||||||
|
HoppRESTReqBody,
|
||||||
|
HoppRESTRequest,
|
||||||
|
RESTReqSchemaVersion,
|
||||||
|
ValidContentTypes,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import { BehaviorSubject, combineLatest, map } from "rxjs"
|
||||||
|
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
|
||||||
|
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||||
|
|
||||||
|
export class RESTRequest {
|
||||||
|
public v$ = new BehaviorSubject<typeof RESTReqSchemaVersion>(
|
||||||
|
RESTReqSchemaVersion
|
||||||
|
)
|
||||||
|
public name$ = new BehaviorSubject("Untitled")
|
||||||
|
public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
|
||||||
|
public params$ = new BehaviorSubject<HoppRESTParam[]>([])
|
||||||
|
public headers$ = new BehaviorSubject<HoppRESTHeader[]>([])
|
||||||
|
public method$ = new BehaviorSubject("GET")
|
||||||
|
public auth$ = new BehaviorSubject<HoppRESTAuth>({
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
})
|
||||||
|
public preRequestScript$ = new BehaviorSubject("")
|
||||||
|
public testScript$ = new BehaviorSubject("")
|
||||||
|
public body$ = new BehaviorSubject<HoppRESTReqBody>({
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
public response$ = new BehaviorSubject<HoppRESTResponse | null>(null)
|
||||||
|
|
||||||
|
get request$() {
|
||||||
|
// any of above changes construct requests
|
||||||
|
return combineLatest([
|
||||||
|
this.v$,
|
||||||
|
this.name$,
|
||||||
|
this.endpoint$,
|
||||||
|
this.params$,
|
||||||
|
this.headers$,
|
||||||
|
this.method$,
|
||||||
|
this.auth$,
|
||||||
|
this.preRequestScript$,
|
||||||
|
this.testScript$,
|
||||||
|
this.body$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([
|
||||||
|
v,
|
||||||
|
name,
|
||||||
|
endpoint,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
method,
|
||||||
|
auth,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
body,
|
||||||
|
]) => ({
|
||||||
|
v,
|
||||||
|
name,
|
||||||
|
endpoint,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
method,
|
||||||
|
auth,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get contentType$() {
|
||||||
|
return this.body$.pipe(map((body) => body.contentType))
|
||||||
|
}
|
||||||
|
|
||||||
|
get bodyContent$() {
|
||||||
|
return this.body$.pipe(map((body) => body.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
get headersCount$() {
|
||||||
|
return this.headers$.pipe(
|
||||||
|
map(
|
||||||
|
(params) =>
|
||||||
|
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
||||||
|
.length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get paramsCount$() {
|
||||||
|
return this.params$.pipe(
|
||||||
|
map(
|
||||||
|
(params) =>
|
||||||
|
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
||||||
|
.length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string) {
|
||||||
|
this.name$.next(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
setEndpoint(newURL: string) {
|
||||||
|
this.endpoint$.next(newURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
setMethod(newMethod: string) {
|
||||||
|
this.method$.next(newMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
setParams(entries: HoppRESTParam[]) {
|
||||||
|
this.params$.next(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
addParam(newParam: HoppRESTParam) {
|
||||||
|
const newParams = this.params$.value.concat(newParam)
|
||||||
|
this.params$.next(newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParam(index: number, updatedParam: HoppRESTParam) {
|
||||||
|
const newParams = this.params$.value.map((param, i) =>
|
||||||
|
i === index ? updatedParam : param
|
||||||
|
)
|
||||||
|
this.params$.next(newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteParam(index: number) {
|
||||||
|
const newParams = this.params$.value.filter((_, i) => i !== index)
|
||||||
|
this.params$.next(newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllParams() {
|
||||||
|
this.params$.next([])
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeaders(entries: HoppRESTHeader[]) {
|
||||||
|
this.headers$.next(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
addHeader(newHeader: HoppRESTHeader) {
|
||||||
|
const newHeaders = this.headers$.value.concat(newHeader)
|
||||||
|
this.headers$.next(newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeader(index: number, updatedHeader: HoppRESTHeader) {
|
||||||
|
const newHeaders = this.headers$.value.map((header, i) =>
|
||||||
|
i === index ? updatedHeader : header
|
||||||
|
)
|
||||||
|
this.headers$.next(newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteHeader(index: number) {
|
||||||
|
const newHeaders = this.headers$.value.filter((_, i) => i !== index)
|
||||||
|
this.headers$.next(newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllHeaders() {
|
||||||
|
this.headers$.next([])
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentType(newContentType: ValidContentTypes | null) {
|
||||||
|
// TODO: persist body evenafter switching content typees
|
||||||
|
this.body$.next(applyBodyTransition(this.body$.value, newContentType))
|
||||||
|
}
|
||||||
|
|
||||||
|
setBody(newBody: string | FormDataKeyValue[] | null) {
|
||||||
|
const body = { ...this.body$.value }
|
||||||
|
body.body = newBody
|
||||||
|
this.body$.next({ ...body })
|
||||||
|
}
|
||||||
|
|
||||||
|
addFormDataEntry(entry: FormDataKeyValue) {
|
||||||
|
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
||||||
|
const body: HoppRESTReqBody = {
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [...this.body$.value.body, entry],
|
||||||
|
}
|
||||||
|
this.body$.next(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFormDataEntry(index: number) {
|
||||||
|
// Only perform update if the current content-type is formdata
|
||||||
|
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
||||||
|
|
||||||
|
const body: HoppRESTReqBody = {
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [...this.body$.value.body.filter((_, i) => i !== index)],
|
||||||
|
}
|
||||||
|
|
||||||
|
this.body$.next(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormDataEntry(index: number, entry: FormDataKeyValue) {
|
||||||
|
// Only perform update if the current content-type is formdata
|
||||||
|
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
||||||
|
|
||||||
|
const body: HoppRESTReqBody = {
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [
|
||||||
|
...this.body$.value.body.map((oldEntry, i) =>
|
||||||
|
i === index ? entry : oldEntry
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
this.body$.next(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllFormDataEntries() {
|
||||||
|
// Only perform update if the current content-type is formdata
|
||||||
|
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
||||||
|
|
||||||
|
const body: HoppRESTReqBody = {
|
||||||
|
contentType: "multipart/form-data",
|
||||||
|
body: [],
|
||||||
|
}
|
||||||
|
this.body$.next(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestBody(newBody: HoppRESTReqBody) {
|
||||||
|
this.body$.next(newBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuth(newAuth: HoppRESTAuth) {
|
||||||
|
this.auth$.next(newAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreRequestScript(newScript: string) {
|
||||||
|
this.preRequestScript$.next(newScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestScript(newScript: string) {
|
||||||
|
this.testScript$.next(newScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResponse(response: HoppRESTResponse | null) {
|
||||||
|
this.response$.next(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequest(request: HoppRESTRequest) {
|
||||||
|
this.v$.next(RESTReqSchemaVersion)
|
||||||
|
this.name$.next(request.name)
|
||||||
|
this.endpoint$.next(request.endpoint)
|
||||||
|
this.params$.next(request.params)
|
||||||
|
this.headers$.next(request.headers)
|
||||||
|
this.method$.next(request.method)
|
||||||
|
this.auth$.next(request.auth)
|
||||||
|
this.preRequestScript$.next(request.preRequestScript)
|
||||||
|
this.testScript$.next(request.testScript)
|
||||||
|
this.body$.next(request.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest() {
|
||||||
|
return {
|
||||||
|
v: this.v$.value,
|
||||||
|
name: this.name$.value,
|
||||||
|
endpoint: this.endpoint$.value,
|
||||||
|
params: this.params$.value,
|
||||||
|
headers: this.headers$.value,
|
||||||
|
method: this.method$.value,
|
||||||
|
auth: this.auth$.value,
|
||||||
|
preRequestScript: this.preRequestScript$.value,
|
||||||
|
testScript: this.testScript$.value,
|
||||||
|
body: this.body$.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRequest() {
|
||||||
|
this.v$.next(RESTReqSchemaVersion)
|
||||||
|
this.name$.next("")
|
||||||
|
this.endpoint$.next("")
|
||||||
|
this.params$.next([])
|
||||||
|
this.headers$.next([])
|
||||||
|
this.method$.next("GET")
|
||||||
|
this.auth$.next({
|
||||||
|
authType: "none",
|
||||||
|
authActive: false,
|
||||||
|
})
|
||||||
|
this.preRequestScript$.next("")
|
||||||
|
this.testScript$.next("")
|
||||||
|
this.body$.next({
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { Environment } from "@hoppscotch/data"
|
||||||
Environment,
|
|
||||||
HoppRESTHeaders,
|
|
||||||
HoppRESTRequestVariable,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -69,17 +65,10 @@ const getTestableBody = (
|
|||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineEnvVariables = (variables: {
|
const combineEnvVariables = (envs: {
|
||||||
environments: {
|
|
||||||
selected: Environment["variables"]
|
|
||||||
global: Environment["variables"]
|
global: Environment["variables"]
|
||||||
}
|
selected: Environment["variables"]
|
||||||
requestVariables: Environment["variables"]
|
}) => [...envs.selected, ...envs.global]
|
||||||
}) => [
|
|
||||||
...variables.requestVariables,
|
|
||||||
...variables.environments.selected,
|
|
||||||
...variables.environments.global,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const executedResponses$ = new Subject<
|
export const executedResponses$ = new Subject<
|
||||||
HoppRESTResponse & { type: "success" | "fail " }
|
HoppRESTResponse & { type: "success" | "fail " }
|
||||||
@@ -133,38 +122,6 @@ const updateEnvironmentsWithSecret = (
|
|||||||
return updatedEnv
|
return updatedEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms the environment list to a list with unique keys with value
|
|
||||||
* @param envs The environment list to be transformed
|
|
||||||
* @returns The transformed environment list with keys with value
|
|
||||||
*/
|
|
||||||
const filterNonEmptyEnvironmentVariables = (
|
|
||||||
envs: Environment["variables"]
|
|
||||||
): Environment["variables"] => {
|
|
||||||
const envsMap = new Map<string, Environment["variables"][number]>()
|
|
||||||
|
|
||||||
envs.forEach((env) => {
|
|
||||||
if (env.secret) {
|
|
||||||
envsMap.set(env.key, env)
|
|
||||||
} else if (envsMap.has(env.key)) {
|
|
||||||
const existingEnv = envsMap.get(env.key)
|
|
||||||
|
|
||||||
if (
|
|
||||||
existingEnv &&
|
|
||||||
"value" in existingEnv &&
|
|
||||||
existingEnv.value === "" &&
|
|
||||||
env.value !== ""
|
|
||||||
) {
|
|
||||||
envsMap.set(env.key, env)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
envsMap.set(env.key, env)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(envsMap.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runRESTRequest$(
|
export function runRESTRequest$(
|
||||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||||
): [
|
): [
|
||||||
@@ -218,40 +175,15 @@ export function runRESTRequest$(
|
|||||||
requestHeaders = [...tab.value.document.request.headers]
|
requestHeaders = [...tab.value.document.request.headers]
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalRequestVariables =
|
|
||||||
tab.value.document.request.requestVariables.map(
|
|
||||||
(v: HoppRESTRequestVariable) => {
|
|
||||||
if (v.active) {
|
|
||||||
return {
|
|
||||||
key: v.key,
|
|
||||||
value: v.value,
|
|
||||||
secret: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const finalRequest = {
|
const finalRequest = {
|
||||||
...tab.value.document.request,
|
...tab.value.document.request,
|
||||||
auth: requestAuth ?? { authType: "none", authActive: false },
|
auth: requestAuth ?? { authType: "none", authActive: false },
|
||||||
headers: requestHeaders as HoppRESTHeaders,
|
headers: requestHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalEnvs = {
|
|
||||||
requestVariables: finalRequestVariables as Environment["variables"],
|
|
||||||
environments: envs.right,
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
|
|
||||||
combineEnvVariables(finalEnvs)
|
|
||||||
)
|
|
||||||
|
|
||||||
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
|
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
|
||||||
id: "env-id",
|
|
||||||
v: 1,
|
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: finalEnvsWithNonEmptyValues,
|
variables: combineEnvVariables(envs.right),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
|
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const samples = [
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: rawKeyValueEntriesToString([
|
body: rawKeyValueEntriesToString([
|
||||||
@@ -38,7 +38,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -142,7 +141,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,7 +149,7 @@ const samples = [
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
body: null,
|
body: null,
|
||||||
@@ -160,7 +158,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -169,7 +166,7 @@ const samples = [
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "http://localhost:1111/hello/world/?buzz",
|
endpoint: "http://localhost:1111/hello/world/?buzz",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: `{\n "foo": "bar"\n}`,
|
body: `{\n "foo": "bar"\n}`,
|
||||||
@@ -184,7 +181,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -193,7 +189,7 @@ const samples = [
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
body: null,
|
body: null,
|
||||||
@@ -213,7 +209,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -222,7 +217,7 @@ const samples = [
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://bing.com/",
|
endpoint: "https://bing.com/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: "multipart/form-data",
|
contentType: "multipart/form-data",
|
||||||
body: [
|
body: [
|
||||||
@@ -244,7 +239,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -274,7 +268,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,7 +291,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -309,7 +301,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "http://localhost:9900/",
|
endpoint: "http://localhost:9900/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
@@ -320,7 +312,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -344,7 +335,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -355,7 +345,7 @@ const samples = [
|
|||||||
endpoint: "https://hoppscotch.io/?io",
|
endpoint: "https://hoppscotch.io/?io",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
@@ -376,7 +366,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -391,7 +380,7 @@ const samples = [
|
|||||||
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
contentType: "multipart/form-data",
|
contentType: "multipart/form-data",
|
||||||
@@ -441,7 +430,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -453,7 +441,7 @@ const samples = [
|
|||||||
endpoint: "http://localhost/",
|
endpoint: "http://localhost/",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
contentType: "multipart/form-data",
|
contentType: "multipart/form-data",
|
||||||
@@ -470,7 +458,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -486,7 +473,7 @@ const samples = [
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://hoppscotch.io/",
|
endpoint: "https://hoppscotch.io/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
body: null,
|
body: null,
|
||||||
@@ -528,7 +515,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -542,7 +528,7 @@ const samples = [
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: rawKeyValueEntriesToString([
|
body: rawKeyValueEntriesToString([
|
||||||
@@ -574,7 +560,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -588,7 +573,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -622,7 +607,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -631,13 +615,12 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://muxueqz.top/skybook.html",
|
endpoint: "https://muxueqz.top/skybook.html",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
body: { contentType: null, body: null },
|
body: { contentType: null, body: null },
|
||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -646,7 +629,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
body: {
|
body: {
|
||||||
contentType: "multipart/form-data",
|
contentType: "multipart/form-data",
|
||||||
@@ -662,7 +645,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -671,7 +653,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "http://127.0.0.1/",
|
endpoint: "http://127.0.0.1/",
|
||||||
method: "CUSTOMMETHOD",
|
method: "CUSTOMMETHOD",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
@@ -680,7 +662,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -689,7 +670,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -704,7 +685,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -713,7 +693,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
@@ -722,7 +702,6 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -731,7 +710,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://example.org/",
|
endpoint: "https://example.org/",
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
body: {
|
body: {
|
||||||
contentType: null,
|
contentType: null,
|
||||||
@@ -746,7 +725,6 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -769,7 +747,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -779,7 +756,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
@@ -790,7 +767,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -801,7 +777,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
@@ -812,7 +788,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -822,7 +797,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "http://192.168.0.24:8080/ping",
|
endpoint: "http://192.168.0.24:8080/ping",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
@@ -833,7 +808,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -843,7 +817,7 @@ const samples = [
|
|||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
@@ -865,7 +839,6 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
requestVariables: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -181,6 +181,5 @@ export const parseCurlCommand = (curlCommand: string) => {
|
|||||||
testScript: defaultRESTReq.testScript,
|
testScript: defaultRESTReq.testScript,
|
||||||
auth,
|
auth,
|
||||||
body: finalBody,
|
body: finalBody,
|
||||||
requestVariables: defaultRESTReq.requestVariables,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user