Compare commits
59 Commits
release/20
...
pr/JoelJac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7876c5cc58 | ||
|
|
e09fb32219 | ||
|
|
b5cf2fa1b4 | ||
|
|
0e2dd400e4 | ||
|
|
f99bfda88b | ||
|
|
446d29f519 | ||
|
|
d0032d6f50 | ||
|
|
fbf5d5ba71 | ||
|
|
14506cd92a | ||
|
|
f38e2b6884 | ||
|
|
ac11a804bd | ||
|
|
25d8c28e4f | ||
|
|
ae9e32b427 | ||
|
|
041eeab4d2 | ||
|
|
6d1e8c4eeb | ||
|
|
08992089f7 | ||
|
|
cc45ff595b | ||
|
|
ba58e25278 | ||
|
|
e261f75cc8 | ||
|
|
55293a0382 | ||
|
|
f7deff5448 | ||
|
|
8877dae2dd | ||
|
|
162434fad4 | ||
|
|
5095c2b76f | ||
|
|
70c4ef5699 | ||
|
|
5959f422a0 | ||
|
|
5c59e55e53 | ||
|
|
55a94bdccc | ||
|
|
faab1d20fd | ||
|
|
bd406616ec | ||
|
|
6827e97ec5 | ||
|
|
10d2048975 | ||
|
|
291f18591e | ||
|
|
342532c9b1 | ||
|
|
cf039c482a | ||
|
|
ded2725116 | ||
|
|
9c6754c70f | ||
|
|
4bd54b12cd | ||
|
|
ed6e9b6954 | ||
|
|
dfdd44b4ed | ||
|
|
fc34871dae | ||
|
|
45b532747e | ||
|
|
de4635df23 | ||
|
|
41bad1f3dc | ||
|
|
ecca3d2032 | ||
|
|
47226be6d0 | ||
|
|
6a0e73fdec | ||
|
|
672ee69b2c | ||
|
|
b359650d96 | ||
|
|
c0fae79678 | ||
|
|
5bcc38e36b | ||
|
|
00862eb192 | ||
|
|
16803acb26 | ||
|
|
3911c9cd1f | ||
|
|
0028f6e878 | ||
|
|
0ba33ec187 | ||
|
|
3482743782 | ||
|
|
d7cdeb796a | ||
|
|
3d6adcc39d |
@@ -118,7 +118,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
# - DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.12.3",
|
"version": "2023.12.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -34,12 +34,14 @@
|
|||||||
"@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",
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ 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 { User } from 'src/user/user.model';
|
import { UserDeletionResult } 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)
|
||||||
@@ -49,203 +47,6 @@ 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, {
|
||||||
@@ -269,8 +70,26 @@ 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(
|
||||||
@@ -281,12 +100,33 @@ export class AdminResolver {
|
|||||||
})
|
})
|
||||||
userUID: string,
|
userUID: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
const removedUser = await this.adminService.removeUserAccount(userUID);
|
||||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
||||||
return invitedUser.right;
|
return removedUser.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => [UserDeletionResult], {
|
||||||
|
description: 'Delete user accounts from infra',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async removeUsersByAdmin(
|
||||||
|
@Args({
|
||||||
|
name: 'userUIDs',
|
||||||
|
description: 'users UID',
|
||||||
|
type: () => [ID],
|
||||||
|
})
|
||||||
|
userUIDs: string[],
|
||||||
|
): Promise<UserDeletionResult[]> {
|
||||||
|
const deletionResults = await this.adminService.removeUserAccounts(
|
||||||
|
userUIDs,
|
||||||
|
);
|
||||||
|
if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
|
||||||
|
return deletionResults.right;
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@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(
|
||||||
@@ -302,8 +142,51 @@ 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(
|
||||||
@@ -319,6 +202,23 @@ 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 } from '@prisma/client';
|
import { InvitedUsers, User as DbUser } 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,10 +13,15 @@ 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>();
|
||||||
@@ -58,20 +63,87 @@ 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 return an array of invited users', async () => {
|
test('should resolve right and apply pagination correctly', async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// 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();
|
const paginationArgs: OffsetPaginationArgs = { take: 5, skip: 2 };
|
||||||
|
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||||
|
|
||||||
|
expect(mockPrisma.invitedUsers.findMany).toHaveBeenCalledWith({
|
||||||
|
...paginationArgs,
|
||||||
|
orderBy: {
|
||||||
|
invitedOn: 'desc',
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
inviteeEmail: {
|
||||||
|
in: [dbAdminUsers[0].email],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should resolve right and return an array of invited users', async () => {
|
||||||
|
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||||
|
// @ts-ignore
|
||||||
|
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||||
|
|
||||||
|
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||||
expect(results).toEqual(invitedUsers);
|
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();
|
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||||
expect(results).toEqual([]);
|
expect(results).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -134,6 +206,58 @@ describe('AdminService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('revokeUserInvitations', () => {
|
||||||
|
test('should resolve left and return error if email not invited', async () => {
|
||||||
|
mockPrisma.invitedUsers.deleteMany.mockRejectedValueOnce(
|
||||||
|
'RecordNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.revokeUserInvitations([
|
||||||
|
'test@gmail.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(USER_INVITATION_DELETION_FAILED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve right and return deleted invitee email', async () => {
|
||||||
|
const adminUid = 'adminUid';
|
||||||
|
mockPrisma.invitedUsers.deleteMany.mockResolvedValueOnce({ count: 1 });
|
||||||
|
|
||||||
|
const result = await adminService.revokeUserInvitations([
|
||||||
|
invitedUsers[0].inviteeEmail,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeUsersAsAdmin', () => {
|
||||||
|
test('should resolve right and make admins to users', async () => {
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||||
|
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||||
|
|
||||||
|
return expect(
|
||||||
|
await adminService.demoteUsersByAdmin([dbAdminUsers[0].uid]),
|
||||||
|
).toEqualRight(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve left and return error if only one admin in the infra', async () => {
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||||
|
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||||
|
|
||||||
|
return expect(
|
||||||
|
await adminService.demoteUsersByAdmin(
|
||||||
|
dbAdminUsers.map((user) => user.uid),
|
||||||
|
),
|
||||||
|
).toEqualLeft(ONLY_ONE_ADMIN_ACCOUNT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getUsersCount', () => {
|
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,13 +6,16 @@ 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';
|
||||||
@@ -26,6 +29,8 @@ 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 {
|
||||||
@@ -48,12 +53,30 @@ 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
|
||||||
@@ -110,14 +133,68 @@ 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() {
|
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
||||||
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
const userEmailObjs = await this.prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const users: InvitedUser[] = invitedUsers.map(
|
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
|
||||||
|
take: paginationOption.take,
|
||||||
|
skip: paginationOption.skip,
|
||||||
|
orderBy: {
|
||||||
|
invitedOn: 'desc',
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
inviteeEmail: {
|
||||||
|
in: userEmailObjs.map((user) => user.email),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const users: InvitedUser[] = pendingInvitedUsers.map(
|
||||||
(user) => <InvitedUser>{ ...user },
|
(user) => <InvitedUser>{ ...user },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -337,6 +414,7 @@ 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);
|
||||||
@@ -349,10 +427,73 @@ 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);
|
||||||
@@ -360,10 +501,22 @@ 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();
|
||||||
@@ -374,6 +527,26 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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,7 +17,10 @@ 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 { PaginationArgs } from 'src/types/input-types.args';
|
import {
|
||||||
|
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';
|
||||||
@@ -30,6 +33,7 @@ import {
|
|||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnumForClient } 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)
|
||||||
@@ -76,6 +80,7 @@ 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[]> {
|
||||||
@@ -83,11 +88,33 @@ 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(): Promise<InvitedUser[]> {
|
async invitedUsers(
|
||||||
const users = await this.adminService.fetchInvitedUsers();
|
@Args() args: OffsetPaginationArgs,
|
||||||
|
): Promise<InvitedUser[]> {
|
||||||
|
const users = await this.adminService.fetchInvitedUsers(args);
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +311,25 @@ export class InfraResolver {
|
|||||||
return updatedRes.right;
|
return updatedRes.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Enable or disable analytics collection',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async toggleAnalyticsCollection(
|
||||||
|
@Args({
|
||||||
|
name: 'status',
|
||||||
|
type: () => ServiceStatus,
|
||||||
|
description: 'Toggle analytics collection',
|
||||||
|
})
|
||||||
|
analyticsCollectionStatus: ServiceStatus,
|
||||||
|
) {
|
||||||
|
const res = await this.infraConfigService.toggleAnalyticsCollection(
|
||||||
|
analyticsCollectionStatus,
|
||||||
|
);
|
||||||
|
if (E.isLeft(res)) throwErr(res.left);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Reset Infra Configs with default values (.env)',
|
description: 'Reset Infra Configs with default values (.env)',
|
||||||
})
|
})
|
||||||
@@ -306,7 +352,9 @@ export class InfraResolver {
|
|||||||
})
|
})
|
||||||
providerInfo: EnableAndDisableSSOArgs[],
|
providerInfo: EnableAndDisableSSOArgs[],
|
||||||
) {
|
) {
|
||||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
|
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
|
||||||
|
providerInfo,
|
||||||
|
);
|
||||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
|||||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||||
import { loadInfraConfiguration } from './infra-config/helper';
|
import { loadInfraConfiguration } from './infra-config/helper';
|
||||||
import { MailerModule } from './mailer/mailer.module';
|
import { MailerModule } from './mailer/mailer.module';
|
||||||
|
import { PosthogModule } from './posthog/posthog.module';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
|
PosthogModule,
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ 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)
|
||||||
@@ -99,6 +107,13 @@ 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)
|
||||||
@@ -696,3 +711,9 @@ export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
|||||||
*/
|
*/
|
||||||
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';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AUTH_PROVIDER_NOT_CONFIGURED } 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',
|
||||||
@@ -104,3 +105,12 @@ export function getConfiguredSSOProviders() {
|
|||||||
|
|
||||||
return configuredAuthProviders.join(',');
|
return configuredAuthProviders.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a hashed valued for analytics
|
||||||
|
* @returns Generated hashed value
|
||||||
|
*/
|
||||||
|
export function generateAnalyticsUserId() {
|
||||||
|
const hashedUserID = randomBytes(20).toString('hex');
|
||||||
|
return hashedUserID;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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 { throwHTTPErr } from 'src/auth/helper';
|
||||||
|
import { AuthError } from 'src/types/AuthError';
|
||||||
|
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||||
|
|
||||||
|
@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(
|
||||||
|
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(status))
|
||||||
|
throwHTTPErr(<AuthError>{
|
||||||
|
message: status.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
return status.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('setup')
|
||||||
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
|
async setSetupAsComplete() {
|
||||||
|
const res = await this.infraConfigService.update(
|
||||||
|
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
false.toString(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(res))
|
||||||
|
throwHTTPErr(<AuthError>{
|
||||||
|
message: res.left,
|
||||||
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
|
});
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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 {}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import {
|
|||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
import {
|
||||||
|
ServiceStatus,
|
||||||
|
generateAnalyticsUserId,
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -34,7 +39,9 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
await this.initializeInfraConfigTable();
|
await this.initializeInfraConfigTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
|
async getDefaultInfraConfigs(): Promise<
|
||||||
|
{ name: InfraConfigEnum; value: string }[]
|
||||||
|
> {
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||||
{
|
{
|
||||||
@@ -73,6 +80,18 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
value: getConfiguredSSOProviders(),
|
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 this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
return infraConfigDefaultObjs;
|
||||||
@@ -88,7 +107,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
const enumValues = Object.values(InfraConfigEnum);
|
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 = this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
||||||
|
|
||||||
// Check if all the 'names' are listed in the default values
|
// Check if all the 'names' are listed in the default values
|
||||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
||||||
@@ -147,11 +166,13 @@ 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(
|
async update(
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
name: InfraConfigEnumForClient | InfraConfigEnum,
|
||||||
value: string,
|
value: string,
|
||||||
|
restartEnabled = false,
|
||||||
) {
|
) {
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
@@ -162,7 +183,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
data: { value },
|
data: { value },
|
||||||
});
|
});
|
||||||
|
|
||||||
stopApp();
|
if (restartEnabled) stopApp();
|
||||||
|
|
||||||
return E.right(this.cast(infraConfig));
|
return E.right(this.cast(infraConfig));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -223,6 +244,22 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or Disable Analytics Collection
|
||||||
|
*
|
||||||
|
* @param status Status to enable or disable
|
||||||
|
* @returns Boolean of status of analytics collection
|
||||||
|
*/
|
||||||
|
async toggleAnalyticsCollection(status: ServiceStatus) {
|
||||||
|
const isUpdated = await this.update(
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
return E.right(isUpdated.right.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or Disable SSO for login/signup
|
* Enable or Disable SSO for login/signup
|
||||||
* @param provider Auth Provider to enable or disable
|
* @param provider Auth Provider to enable or disable
|
||||||
@@ -261,6 +298,7 @@ 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);
|
||||||
|
|
||||||
@@ -316,13 +354,24 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await 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: infraConfigDefaultObjs,
|
data: [
|
||||||
|
...updatedInfraConfigDefaultObjs,
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
stopApp();
|
stopApp();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class MailerService {
|
|||||||
): string {
|
): string {
|
||||||
switch (mailDesc.template) {
|
switch (mailDesc.template) {
|
||||||
case 'team-invitation':
|
case 'team-invitation':
|
||||||
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
return `A user has invited you to join a team workspace in Hoppscotch`;
|
||||||
|
|
||||||
case 'user-invitation':
|
case 'user-invitation':
|
||||||
return 'Sign in to Hoppscotch';
|
return 'Sign in to Hoppscotch';
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.nohighlight {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -458,7 +464,7 @@
|
|||||||
<td class="content-cell">
|
<td class="content-cell">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
<h1>Hi there,</h1>
|
<h1>Hi there,</h1>
|
||||||
<p>{{invitee}} with {{invite_team_name}} has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
<p><a class="nohighlight" name="invitee" href="#">{{invitee}}</a> with <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a> has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
|
||||||
<!-- Action -->
|
<!-- Action -->
|
||||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -484,7 +490,7 @@
|
|||||||
Welcome aboard, <br />
|
Welcome aboard, <br />
|
||||||
Your friends at Hoppscotch
|
Your friends at Hoppscotch
|
||||||
</p>
|
</p>
|
||||||
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, just ignore this email.</p>
|
<p><strong>P.S.</strong> If you don't associate with <a class="nohighlight" name="invitee" href="#">{{invitee}}</a> or <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a>, just ignore this email.</p>
|
||||||
<!-- Sub copy -->
|
<!-- Sub copy -->
|
||||||
<table class="body-sub">
|
<table class="body-sub">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<style type="text/css" rel="stylesheet" media="all">
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
/* Base ------------------------------ */
|
/* Base ------------------------------ */
|
||||||
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||||
body {
|
body {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -22,19 +22,25 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #3869D4;
|
color: #3869D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.nohighlight {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preheader {
|
.preheader {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -47,13 +53,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
/* Type ------------------------------ */
|
/* Type ------------------------------ */
|
||||||
|
|
||||||
body,
|
body,
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
@@ -61,7 +67,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
@@ -69,7 +75,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
@@ -77,12 +83,12 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
p,
|
||||||
ul,
|
ul,
|
||||||
ol,
|
ol,
|
||||||
@@ -91,25 +97,25 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.sub {
|
p.sub {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
/* Utilities ------------------------------ */
|
/* Utilities ------------------------------ */
|
||||||
|
|
||||||
.align-right {
|
.align-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.align-left {
|
.align-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.align-center {
|
.align-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
/* Buttons ------------------------------ */
|
/* Buttons ------------------------------ */
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
background-color: #3869D4;
|
background-color: #3869D4;
|
||||||
border-top: 10px solid #3869D4;
|
border-top: 10px solid #3869D4;
|
||||||
@@ -124,7 +130,7 @@
|
|||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--green {
|
.button--green {
|
||||||
background-color: #22BC66;
|
background-color: #22BC66;
|
||||||
border-top: 10px solid #22BC66;
|
border-top: 10px solid #22BC66;
|
||||||
@@ -132,7 +138,7 @@
|
|||||||
border-bottom: 10px solid #22BC66;
|
border-bottom: 10px solid #22BC66;
|
||||||
border-left: 18px solid #22BC66;
|
border-left: 18px solid #22BC66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--red {
|
.button--red {
|
||||||
background-color: #FF6136;
|
background-color: #FF6136;
|
||||||
border-top: 10px solid #FF6136;
|
border-top: 10px solid #FF6136;
|
||||||
@@ -140,7 +146,7 @@
|
|||||||
border-bottom: 10px solid #FF6136;
|
border-bottom: 10px solid #FF6136;
|
||||||
border-left: 18px solid #FF6136;
|
border-left: 18px solid #FF6136;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 500px) {
|
@media only screen and (max-width: 500px) {
|
||||||
.button {
|
.button {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -148,21 +154,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Attribute list ------------------------------ */
|
/* Attribute list ------------------------------ */
|
||||||
|
|
||||||
.attributes {
|
.attributes {
|
||||||
margin: 0 0 21px;
|
margin: 0 0 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes_content {
|
.attributes_content {
|
||||||
background-color: #F4F4F7;
|
background-color: #F4F4F7;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes_item {
|
.attributes_item {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
/* Related Items ------------------------------ */
|
/* Related Items ------------------------------ */
|
||||||
|
|
||||||
.related {
|
.related {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -171,31 +177,31 @@
|
|||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related_item {
|
.related_item {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
color: #CBCCCF;
|
color: #CBCCCF;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related_item-title {
|
.related_item-title {
|
||||||
display: block;
|
display: block;
|
||||||
margin: .5em 0 0;
|
margin: .5em 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related_item-thumb {
|
.related_item-thumb {
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related_heading {
|
.related_heading {
|
||||||
border-top: 1px solid #CBCCCF;
|
border-top: 1px solid #CBCCCF;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 25px 0 10px;
|
padding: 25px 0 10px;
|
||||||
}
|
}
|
||||||
/* Discount Code ------------------------------ */
|
/* Discount Code ------------------------------ */
|
||||||
|
|
||||||
.discount {
|
.discount {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -206,33 +212,33 @@
|
|||||||
background-color: #F4F4F7;
|
background-color: #F4F4F7;
|
||||||
border: 2px dashed #CBCCCF;
|
border: 2px dashed #CBCCCF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discount_heading {
|
.discount_heading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discount_body {
|
.discount_body {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
/* Social Icons ------------------------------ */
|
/* Social Icons ------------------------------ */
|
||||||
|
|
||||||
.social {
|
.social {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social td {
|
.social td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social_icon {
|
.social_icon {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 0 8px 10px 8px;
|
margin: 0 8px 10px 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
/* Data table ------------------------------ */
|
/* Data table ------------------------------ */
|
||||||
|
|
||||||
.purchase {
|
.purchase {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -241,7 +247,7 @@
|
|||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_content {
|
.purchase_content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -250,50 +256,50 @@
|
|||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_item {
|
.purchase_item {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_heading {
|
.purchase_heading {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid #EAEAEC;
|
border-bottom: 1px solid #EAEAEC;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_heading p {
|
.purchase_heading p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #85878E;
|
color: #85878E;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_footer {
|
.purchase_footer {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
border-top: 1px solid #EAEAEC;
|
border-top: 1px solid #EAEAEC;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_total {
|
.purchase_total {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase_total--label {
|
.purchase_total--label {
|
||||||
padding: 0 15px 0 0;
|
padding: 0 15px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #F2F4F6;
|
background-color: #F2F4F6;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-wrapper {
|
.email-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -303,7 +309,7 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
background-color: #F2F4F6;
|
background-color: #F2F4F6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-content {
|
.email-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -313,16 +319,16 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
}
|
}
|
||||||
/* Masthead ----------------------- */
|
/* Masthead ----------------------- */
|
||||||
|
|
||||||
.email-masthead {
|
.email-masthead {
|
||||||
padding: 25px 0;
|
padding: 25px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-masthead_logo {
|
.email-masthead_logo {
|
||||||
width: 94px;
|
width: 94px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-masthead_name {
|
.email-masthead_name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -331,7 +337,7 @@
|
|||||||
text-shadow: 0 1px 0 white;
|
text-shadow: 0 1px 0 white;
|
||||||
}
|
}
|
||||||
/* Body ------------------------------ */
|
/* Body ------------------------------ */
|
||||||
|
|
||||||
.email-body {
|
.email-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -340,7 +346,7 @@
|
|||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-body_inner {
|
.email-body_inner {
|
||||||
width: 570px;
|
width: 570px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -350,7 +356,7 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-footer {
|
.email-footer {
|
||||||
width: 570px;
|
width: 570px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -360,11 +366,11 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-footer p {
|
.email-footer p {
|
||||||
color: #A8AAAF;
|
color: #A8AAAF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-action {
|
.body-action {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
@@ -374,25 +380,25 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-sub {
|
.body-sub {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
border-top: 1px solid #EAEAEC;
|
border-top: 1px solid #EAEAEC;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-cell {
|
.content-cell {
|
||||||
padding: 45px;
|
padding: 45px;
|
||||||
}
|
}
|
||||||
/*Media Queries ------------------------------ */
|
/*Media Queries ------------------------------ */
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
.email-body_inner,
|
.email-body_inner,
|
||||||
.email-footer {
|
.email-footer {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body,
|
body,
|
||||||
.email-body,
|
.email-body,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PosthogService } from './posthog.service';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [PosthogService],
|
||||||
|
})
|
||||||
|
export class PosthogModule {}
|
||||||
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PostHog } from 'posthog-node';
|
||||||
|
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { CronJob } from 'cron';
|
||||||
|
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
@Injectable()
|
||||||
|
export class PosthogService {
|
||||||
|
private postHogClient: PostHog;
|
||||||
|
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private schedulerRegistry: SchedulerRegistry,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
|
||||||
|
console.log('Initializing PostHog');
|
||||||
|
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
|
||||||
|
host: 'https://eu.posthog.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule the cron job only if analytics collection is allowed
|
||||||
|
this.scheduleCronJob();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleCronJob() {
|
||||||
|
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
|
||||||
|
await this.capture();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.schedulerRegistry.addCronJob('captureAnalytics', job);
|
||||||
|
job.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async capture() {
|
||||||
|
if (!this.postHogClient) {
|
||||||
|
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postHogClient.capture({
|
||||||
|
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
|
||||||
|
event: 'sh_instance',
|
||||||
|
properties: {
|
||||||
|
type: 'COMMUNITY',
|
||||||
|
total_user_count: await this.prismaService.user.count(),
|
||||||
|
total_workspace_count: await this.prismaService.team.count(),
|
||||||
|
version: this.configService.get('npm_package_version'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Sent event to PostHog');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ export enum InfraConfigEnum {
|
|||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
|
||||||
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',
|
||||||
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InfraConfigEnumForClient {
|
export enum InfraConfigEnumForClient {
|
||||||
@@ -26,4 +30,7 @@ export enum InfraConfigEnumForClient {
|
|||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
|
||||||
|
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||||
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,21 @@ 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,3 +56,22 @@ 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, USER_NOT_FOUND } from 'src/errors';
|
import { JSON_INVALID, USERS_NOT_FOUND, 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,6 +176,26 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findUsersByIds', () => {
|
||||||
|
test('should successfully return users given valid user UIDs', async () => {
|
||||||
|
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||||
|
|
||||||
|
const result = await userService.findUsersByIds([
|
||||||
|
'123344',
|
||||||
|
'5555',
|
||||||
|
'6666',
|
||||||
|
]);
|
||||||
|
expect(result).toEqual(users);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array of users given a invalid user UIDs', async () => {
|
||||||
|
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await userService.findUsersByIds(['sdcvbdbr']);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('createUserViaMagicLink', () => {
|
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);
|
||||||
@@ -414,6 +434,54 @@ 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);
|
||||||
@@ -435,6 +503,36 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fetchAllUsersV2', () => {
|
||||||
|
test('should resolve right and return first 20 users when searchString is null', async () => {
|
||||||
|
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||||
|
|
||||||
|
const result = await userService.fetchAllUsersV2(null, {
|
||||||
|
take: 20,
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(users);
|
||||||
|
});
|
||||||
|
test('should resolve right and return next 20 users when searchString is provided', async () => {
|
||||||
|
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||||
|
|
||||||
|
const result = await userService.fetchAllUsersV2('.com', {
|
||||||
|
take: 20,
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(users);
|
||||||
|
});
|
||||||
|
test('should resolve left and return an empty array when users not found', async () => {
|
||||||
|
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await userService.fetchAllUsersV2('Unknown entry', {
|
||||||
|
take: 20,
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchAdminUsers', () => {
|
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);
|
||||||
@@ -556,4 +654,17 @@ 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,13 +8,14 @@ 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 { USER_NOT_FOUND } from 'src/errors';
|
import { USERS_NOT_FOUND, 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 {
|
||||||
@@ -88,6 +89,20 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find users with given IDs
|
||||||
|
* @param userUIDs User IDs
|
||||||
|
* @returns Array of found Users
|
||||||
|
*/
|
||||||
|
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
||||||
|
const users = await this.prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
uid: { in: userUIDs },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update User with new generated hashed refresh token
|
* Update User with new generated hashed refresh token
|
||||||
*
|
*
|
||||||
@@ -269,6 +284,30 @@ 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
|
||||||
@@ -286,6 +325,7 @@ 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({
|
||||||
@@ -296,6 +336,43 @@ 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
|
||||||
@@ -326,6 +403,23 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change users to admins by toggling isAdmin param to true
|
||||||
|
* @param userUID user UIDs
|
||||||
|
* @returns a Either of true or error
|
||||||
|
*/
|
||||||
|
async makeAdmins(userUIDs: string[]) {
|
||||||
|
try {
|
||||||
|
await this.prisma.user.updateMany({
|
||||||
|
where: { uid: { in: userUIDs } },
|
||||||
|
data: { isAdmin: true },
|
||||||
|
});
|
||||||
|
return E.right(true);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(USER_UPDATE_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the admin users
|
* Fetch all the admin users
|
||||||
* @returns an array of admin users
|
* @returns an array of admin users
|
||||||
@@ -444,4 +538,22 @@ 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,3 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// * The entry point of the CLI
|
|
||||||
require("../dist").cli(process.argv);
|
|
||||||
6
packages/hoppscotch-cli/bin/hopp.js
Executable file
6
packages/hoppscotch-cli/bin/hopp.js
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// * The entry point of the CLI
|
||||||
|
|
||||||
|
import { cli } from "../dist/index.js";
|
||||||
|
|
||||||
|
cli(process.argv);
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.5.2",
|
"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"
|
"hopp": "bin/hopp.js"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -39,27 +40,31 @@
|
|||||||
},
|
},
|
||||||
"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.92",
|
"@swc/core": "^1.3.105",
|
||||||
"@types/jest": "^29.5.5",
|
"@types/jest": "^29.5.11",
|
||||||
"@types/lodash": "^4.14.199",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/qs": "^6.9.8",
|
"@types/qs": "^6.9.11",
|
||||||
"axios": "^0.21.4",
|
"fp-ts": "^2.16.2",
|
||||||
"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.0.3",
|
"prettier": "^3.2.4",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.2",
|
||||||
"tsup": "^7.2.0",
|
"tsup": "^8.0.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.3.3",
|
||||||
|
"verzod": "^0.2.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,138 +3,247 @@ import { ExecException } from "child_process";
|
|||||||
import { HoppErrorCode } from "../../types/errors";
|
import { HoppErrorCode } from "../../types/errors";
|
||||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||||
|
|
||||||
describe("Test 'hopp test <file>' command:", () => {
|
describe("Test `hopp test <file>` command:", () => {
|
||||||
test("No collection file path provided.", async () => {
|
describe("Argument parsing", () => {
|
||||||
const args = "test";
|
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||||
const { stderr } = await runCLI(args);
|
const args = "test";
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not found.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||||
const args = "test notfound.json";
|
const args = "invalid-arg";
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
|
||||||
test("Collection file is invalid JSON.", async () => {
|
describe("Supplied collection export file validations", () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||||
"malformed-collection.json"
|
const args = "test notfound.json";
|
||||||
)}`;
|
const { stderr } = await runCLI(args);
|
||||||
const { stderr } = await runCLI(args);
|
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Malformed collection file.", async () => {
|
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
|
||||||
const args = `test ${getTestJsonFilePath(
|
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
|
||||||
"malformed-collection2.json"
|
const { stderr } = await runCLI(args);
|
||||||
)}`;
|
|
||||||
const { stderr } = await runCLI(args);
|
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid arguement.", async () => {
|
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
|
||||||
const args = "invalid-arg";
|
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not JSON type.", async () => {
|
test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
|
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Some errors occured (exit code 1).", async () => {
|
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("fails.json")}`;
|
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).not.toBeNull();
|
expect(error).not.toBeNull();
|
||||||
expect(error).toMatchObject(<ExecException>{
|
expect(error).toMatchObject(<ExecException>{
|
||||||
code: 1,
|
code: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("No errors occured (exit code 0).", async () => {
|
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
const args = `test ${getTestJsonFilePath(
|
||||||
|
"collection-level-headers-auth-coll.json", "collection"
|
||||||
|
)}`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
|
||||||
|
const args = `test ${getTestJsonFilePath(
|
||||||
|
"pre-req-script-env-var-persistence-coll.json", "collection"
|
||||||
|
)}`;
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test 'hopp test <file> --env <file>' command:", () => {
|
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
describe("Supplied environment export file validations", () => {
|
||||||
"passes.json"
|
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
)}`;
|
|
||||||
|
|
||||||
test("No env file path provided.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --env`;
|
const args = `${VALID_TEST_ARGS} --env`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
|
||||||
|
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
|
||||||
|
"notjson-coll.txt", "collection"
|
||||||
|
)}`;
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
|
const out = getErrorCode(stderr);
|
||||||
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
|
||||||
|
const args = `${VALID_TEST_ARGS} --env notfound.json`;
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
|
const out = getErrorCode(stderr);
|
||||||
|
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
|
||||||
|
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
|
||||||
|
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
|
const out = getErrorCode(stderr);
|
||||||
|
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
|
||||||
|
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
|
||||||
|
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
|
||||||
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
|
const out = getErrorCode(stderr);
|
||||||
|
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ENV file not JSON type.", async () => {
|
test("Successfully resolves values from the supplied environment export file", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
|
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
||||||
const { stderr } = await runCLI(args);
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ENV file not found.", async () => {
|
|
||||||
const args = `${VALID_TEST_ARGS} --env notfound.json`;
|
|
||||||
const { stderr } = await runCLI(args);
|
|
||||||
|
|
||||||
const out = getErrorCode(stderr);
|
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("No errors occured (exit code 0).", async () => {
|
|
||||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
|
||||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
|
||||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
|
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Correctly resolves environment variables referenced in the request body", async () => {
|
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json");
|
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
||||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json");
|
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
||||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Works with shorth `-e` flag", async () => {
|
||||||
|
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
|
||||||
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
|
||||||
|
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
|
||||||
|
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Secret environment variables", () => {
|
||||||
|
jest.setTimeout(10000);
|
||||||
|
|
||||||
|
// Reads secret environment values from system environment
|
||||||
|
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
secretBearerToken: "test-token",
|
||||||
|
secretBasicAuthUsername: "test-user",
|
||||||
|
secretBasicAuthPassword: "test-pass",
|
||||||
|
secretQueryParamValue: "secret-query-param-value",
|
||||||
|
secretBodyValue: "secret-body-value",
|
||||||
|
secretHeaderValue: "secret-header-value",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
||||||
|
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
|
||||||
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
|
const { error, stdout } = await runCLI(args, { env });
|
||||||
|
|
||||||
|
expect(stdout).toContain(
|
||||||
|
"https://httpbin.org/basic-auth/*********/*********"
|
||||||
|
);
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prefers values specified in the environment export file over values set in the system environment
|
||||||
|
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
|
||||||
|
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
|
||||||
|
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
||||||
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
|
const { error, stdout } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(stdout).toContain(
|
||||||
|
"https://httpbin.org/basic-auth/*********/*********"
|
||||||
|
);
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Values set from the scripting context takes the highest precedence
|
||||||
|
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
|
||||||
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
|
"secret-envs-persistence-coll.json", "collection"
|
||||||
|
);
|
||||||
|
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
|
||||||
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
|
const { error, stdout } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(stdout).toContain(
|
||||||
|
"https://httpbin.org/basic-auth/*********/*********"
|
||||||
|
);
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
|
||||||
|
const COLL_PATH = getTestJsonFilePath(
|
||||||
|
"secret-envs-persistence-scripting-coll.json", "collection"
|
||||||
|
);
|
||||||
|
const ENVS_PATH = getTestJsonFilePath(
|
||||||
|
"secret-envs-persistence-scripting-envs.json", "environment"
|
||||||
|
);
|
||||||
|
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||||
|
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||||
"passes.json"
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
test("No value passed to delay flag.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --delay`;
|
const args = `${VALID_TEST_ARGS} --delay`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -142,7 +251,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid value passed to delay flag.", async () => {
|
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||||
const { stderr } = await runCLI(args);
|
const { stderr } = await runCLI(args);
|
||||||
|
|
||||||
@@ -150,10 +259,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Valid value passed to delay flag.", async () => {
|
test("Successfully performs delayed request execution for a valid delay value", async () => {
|
||||||
const args = `${VALID_TEST_ARGS} --delay 1`;
|
const args = `${VALID_TEST_ARGS} --delay 1`;
|
||||||
const { error } = await runCLI(args);
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Works with the short `-d` flag", async () => {
|
||||||
|
const args = `${VALID_TEST_ARGS} -d 1`;
|
||||||
|
const { error } = await runCLI(args);
|
||||||
|
|
||||||
|
expect(error).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "pre-req-script-env-var-persistence-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "sample-req",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
|
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
||||||
|
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": { "authType": "inherit", "authActive": true },
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "secret-envs-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-headers",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Secret-Header-Key",
|
||||||
|
"value": "<<secretHeaderValue>>",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endpoint": "<<baseURL>>/headers",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
|
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": {
|
||||||
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
|
"contentType": "application/json"
|
||||||
|
},
|
||||||
|
"name": "test-secret-body",
|
||||||
|
"method": "POST",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/post",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
|
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-query-params",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "secretQueryParamKey",
|
||||||
|
"value": "<<secretQueryParamValue>>",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/get",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
|
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "basic",
|
||||||
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
|
"username": "<<secretBasicAuthUsername>>",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-basic-auth",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
|
"preRequestScript": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"token": "<<secretBearerToken>>",
|
||||||
|
"authType": "bearer",
|
||||||
|
"password": "testpassword",
|
||||||
|
"username": "testuser",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-bearer-auth",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
|
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"body": { "body": null, "contentType": null },
|
||||||
|
"name": "test-secret-fallback",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>",
|
||||||
|
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
||||||
|
"preRequestScript": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": { "authType": "inherit", "authActive": false },
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "secret-envs-setters-coll",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
|
"name": "test-secret-headers",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Secret-Header-Key",
|
||||||
|
"value": "<<secretHeaderValue>>",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endpoint": "<<baseURL>>/headers",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
|
"name": "test-secret-headers-overrides",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Secret-Header-Key",
|
||||||
|
"value": "<<secretHeaderValue>>",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endpoint": "<<baseURL>>/headers",
|
||||||
|
"testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})",
|
||||||
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
|
"contentType": "application/json"
|
||||||
|
},
|
||||||
|
"name": "test-secret-body",
|
||||||
|
"method": "POST",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/post",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
|
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "none",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
|
"name": "test-secret-query-params",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "secretQueryParamKey",
|
||||||
|
"value": "<<secretQueryParamValue>>",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/get",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
|
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"authType": "basic",
|
||||||
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
|
"username": "<<secretBasicAuthUsername>>",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
|
"name": "test-secret-basic-auth",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
|
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"auth": {
|
||||||
|
"token": "<<secretBearerToken>>",
|
||||||
|
"authType": "bearer",
|
||||||
|
"password": "testpassword",
|
||||||
|
"username": "testuser",
|
||||||
|
"authActive": true
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"body": null,
|
||||||
|
"contentType": null
|
||||||
|
},
|
||||||
|
"name": "test-secret-bearer-auth",
|
||||||
|
"method": "GET",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
|
"preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"authType": "inherit",
|
||||||
|
"authActive": false
|
||||||
|
},
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"v": 2,
|
||||||
|
"name": "secret-envs-persistence-scripting-req",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"endpoint": "https://httpbin.org/post",
|
||||||
|
"name": "req",
|
||||||
|
"params": [],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"key": "Custom-Header",
|
||||||
|
"value": "<<customHeaderValueFromSecretVar>>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"method": "POST",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")",
|
||||||
|
"testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": "application/json",
|
||||||
|
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": { "authType": "inherit", "authActive": false },
|
||||||
|
"headers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"v": 0,
|
||||||
|
"name": "Env-I",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "firstName",
|
||||||
|
"value": "John"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "lastName",
|
||||||
|
"value": "Doe"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"id": "2",
|
||||||
|
"name": "Env-II",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "https://echo.hoppscotch.io",
|
||||||
|
"secret": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretVar",
|
||||||
|
"secret": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"v": "1",
|
||||||
|
"name": "secret-envs",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"key": "secretVar",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "regularVar",
|
||||||
|
"secret": false,
|
||||||
|
"value": "regular-variable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"v": 0,
|
||||||
"name": "Response body sample",
|
"name": "Response body sample",
|
||||||
"variables": [
|
"variables": [
|
||||||
{
|
{
|
||||||
@@ -34,4 +35,4 @@
|
|||||||
"value": "<<salutation>> <<fullName>>"
|
"value": "<<salutation>> <<fullName>>"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"id": "2",
|
||||||
|
"name": "secret-envs-persistence-scripting-envs",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "preReqVarOne",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "preReqVarTwo",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "postReqVarOne",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "preReqVarTwo",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "customHeaderValueFromSecretVar",
|
||||||
|
"secret": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"v": 1,
|
||||||
|
"name": "secret-envs",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "secretBearerToken",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBasicAuthUsername",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBasicAuthPassword",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretQueryParamValue",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBodyValue",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretHeaderValue",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "nonExistentValueInSystemEnv",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "https://httpbin.org",
|
||||||
|
"secret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"id": "2",
|
||||||
|
"name": "secret-values-envs",
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"key": "secretBearerToken",
|
||||||
|
"value": "test-token",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBasicAuthUsername",
|
||||||
|
"value": "test-user",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBasicAuthPassword",
|
||||||
|
"value": "test-pass",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretQueryParamValue",
|
||||||
|
"value": "secret-query-param-value",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretBodyValue",
|
||||||
|
"value": "secret-body-value",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secretHeaderValue",
|
||||||
|
"value": "secret-header-value",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "nonExistentValueInSystemEnv",
|
||||||
|
"secret": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "https://httpbin.org",
|
||||||
|
"secret": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@ import { resolve } from "path";
|
|||||||
|
|
||||||
import { ExecResponse } from "./types";
|
import { ExecResponse } from "./types";
|
||||||
|
|
||||||
export const runCLI = (args: string): Promise<ExecResponse> =>
|
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
|
||||||
{
|
{
|
||||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||||
const command = `node ${CLI_PATH} ${args}`
|
const command = `node ${CLI_PATH} ${args}`
|
||||||
|
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,12 @@ export const getErrorCode = (out: string) => {
|
|||||||
return ansiTrimmedStr.split(" ")[0];
|
return ansiTrimmedStr.split(" ")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTestJsonFilePath = (file: string) => {
|
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
||||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`);
|
const kindDir = {
|
||||||
|
collection: "collections",
|
||||||
|
environment: "environments",
|
||||||
|
}[kind];
|
||||||
|
|
||||||
|
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
||||||
return filePath;
|
return filePath;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { program } from "commander";
|
import { Command } 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,6 +20,8 @@ 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")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface RequestStack {
|
|||||||
*/
|
*/
|
||||||
export interface RequestConfig extends AxiosRequestConfig {
|
export interface RequestConfig extends AxiosRequestConfig {
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
|
displayUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||||
@@ -30,6 +31,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
|||||||
* This contains path, params and environment variables all applied to it
|
* This contains path, params and environment variables all applied to it
|
||||||
*/
|
*/
|
||||||
effectiveFinalURL: string;
|
effectiveFinalURL: string;
|
||||||
|
effectiveFinalDisplayURL?: string;
|
||||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||||
effectiveFinalBody: FormData | string | null;
|
effectiveFinalBody: FormData | string | null;
|
||||||
|
|||||||
@@ -1,34 +1,42 @@
|
|||||||
|
import { Environment } from "@hoppscotch/data";
|
||||||
|
import { entityReference } from "verzod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { error } from "../../types/errors";
|
import { error } from "../../types/errors";
|
||||||
import {
|
import {
|
||||||
HoppEnvs,
|
|
||||||
HoppEnvPair,
|
|
||||||
HoppEnvKeyPairObject,
|
HoppEnvKeyPairObject,
|
||||||
HoppEnvExportObject,
|
HoppEnvPair,
|
||||||
HoppBulkEnvExportObject,
|
HoppEnvs
|
||||||
} from "../../types/request";
|
} from "../../types/request";
|
||||||
import { readJsonFile } from "../../utils/mutators";
|
import { readJsonFile } from "../../utils/mutators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses env json file for given path and validates the parsed env json object.
|
* Parses env json file for given path and validates the parsed env json object
|
||||||
* @param path Path of env.json file to be parsed.
|
* @param path Path of env.json file to be parsed
|
||||||
* @returns For successful parsing we get HoppEnvs object.
|
* @returns For successful parsing we get HoppEnvs object
|
||||||
*/
|
*/
|
||||||
export async function parseEnvsData(path: string) {
|
export async function parseEnvsData(path: string) {
|
||||||
const contents = await readJsonFile(path);
|
const contents = await readJsonFile(path);
|
||||||
const envPairs: Array<HoppEnvPair> = [];
|
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
||||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
|
||||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
|
||||||
const HoppBulkEnvExportObjectResult =
|
|
||||||
HoppBulkEnvExportObject.safeParse(contents);
|
|
||||||
|
|
||||||
// CLI doesnt support bulk environments export.
|
// The legacy key-value pair format that is still supported
|
||||||
// Hence we check for this case and throw an error if it matches the format.
|
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||||
|
|
||||||
|
// Shape of the single environment export object that is exported from the app
|
||||||
|
const HoppEnvExportObjectResult = Environment.safeParse(contents);
|
||||||
|
|
||||||
|
// Shape of the bulk environment export object that is exported from the app
|
||||||
|
const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
|
||||||
|
|
||||||
|
// CLI doesnt support bulk environments export
|
||||||
|
// Hence we check for this case and throw an error if it matches the format
|
||||||
if (HoppBulkEnvExportObjectResult.success) {
|
if (HoppBulkEnvExportObjectResult.success) {
|
||||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the environment file is of the correct format.
|
// Checks if the environment file is of the correct format
|
||||||
// If it doesnt match either of them, we throw an error.
|
// If it doesnt match either of them, we throw an error
|
||||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
||||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +44,8 @@ export async function parseEnvsData(path: string) {
|
|||||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||||
envPairs.push({ key, value });
|
envPairs.push({ key, value });
|
||||||
}
|
}
|
||||||
} else if (HoppEnvExportObjectResult.success) {
|
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||||
envPairs.push(...HoppEnvExportObjectResult.data.variables);
|
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TestReport } from "../interfaces/response";
|
import { TestReport } from "../interfaces/response";
|
||||||
import { HoppCLIError } from "./errors";
|
import { HoppCLIError } from "./errors";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export type FormDataEntry = {
|
export type FormDataEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
value: string | Blob;
|
value: string | Blob;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HoppEnvPair = { key: string; value: string };
|
export type HoppEnvPair = Environment["variables"][number];
|
||||||
|
|
||||||
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||||
|
|
||||||
// Shape of the single environment export object that is exported from the app.
|
|
||||||
export const HoppEnvExportObject = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
variables: z.array(
|
|
||||||
z.object({
|
|
||||||
key: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shape of the bulk environment export object that is exported from the app.
|
|
||||||
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
|
||||||
|
|
||||||
export type HoppEnvs = {
|
export type HoppEnvs = {
|
||||||
global: HoppEnvPair[];
|
global: HoppEnvPair[];
|
||||||
selected: HoppEnvPair[];
|
selected: HoppEnvPair[];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import { bold } from "chalk";
|
import chalk 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/round";
|
import { round } from "lodash-es";
|
||||||
|
|
||||||
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: ${bold(requestPath)}`));
|
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
|
||||||
|
|
||||||
// Processing current request.
|
// Processing current request.
|
||||||
const result = await processRequest(processRequestParams)();
|
const result = await processRequest(processRequestParams)();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { bold } from "chalk";
|
import chalk 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${bold(path)} reported errors:`);
|
const REPORTED_ERRORS_TITLE = FAIL(`\n${chalk.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${bold(path)} failed tests:`);
|
const FAILED_TESTS_PATH = FAIL(`\n${chalk.bold(path)} failed tests:`);
|
||||||
group(FAILED_TESTS_PATH);
|
group(FAILED_TESTS_PATH);
|
||||||
|
|
||||||
for (const failedTestReport of failedTestsReport) {
|
for (const failedTestReport of failedTestsReport) {
|
||||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
|||||||
*/
|
*/
|
||||||
start: (requestConfig: RequestConfig) => {
|
start: (requestConfig: RequestConfig) => {
|
||||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||||
const ENDPOINT = requestConfig.url;
|
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||||
|
|
||||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clone } from "lodash";
|
import { clone } from "lodash-es";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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/round";
|
import { round } from "lodash-es";
|
||||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
|
|||||||
export const preRequestScriptRunner = (
|
export const preRequestScriptRunner = (
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
envs: HoppEnvs
|
envs: HoppEnvs
|
||||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
): TE.TaskEither<
|
||||||
|
HoppCLIError,
|
||||||
|
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||||
|
> =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.of(request),
|
TE.of(request),
|
||||||
TE.chain(({ preRequestScript }) =>
|
TE.chain(({ preRequestScript }) =>
|
||||||
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
|
|||||||
export function getEffectiveRESTRequest(
|
export function getEffectiveRESTRequest(
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
environment: Environment
|
environment: Environment
|
||||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
): E.Either<
|
||||||
|
HoppCLIError,
|
||||||
|
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||||
|
> {
|
||||||
const envVariables = environment.variables;
|
const envVariables = environment.variables;
|
||||||
|
|
||||||
// Parsing final headers with applied ENVs.
|
// Parsing final headers with applied ENVs.
|
||||||
@@ -162,12 +168,30 @@ export function getEffectiveRESTRequest(
|
|||||||
}
|
}
|
||||||
const effectiveFinalURL = _effectiveFinalURL.right;
|
const effectiveFinalURL = _effectiveFinalURL.right;
|
||||||
|
|
||||||
|
// Secret environment variables referenced in the request endpoint should be masked
|
||||||
|
let effectiveFinalDisplayURL;
|
||||||
|
if (envVariables.some(({ secret }) => secret)) {
|
||||||
|
const _effectiveFinalDisplayURL = parseTemplateStringE(
|
||||||
|
request.endpoint,
|
||||||
|
envVariables,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isRight(_effectiveFinalDisplayURL)) {
|
||||||
|
effectiveFinalDisplayURL = _effectiveFinalDisplayURL.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return E.right({
|
return E.right({
|
||||||
...request,
|
effectiveRequest: {
|
||||||
effectiveFinalURL,
|
...request,
|
||||||
effectiveFinalHeaders,
|
effectiveFinalURL,
|
||||||
effectiveFinalParams,
|
effectiveFinalDisplayURL,
|
||||||
effectiveFinalBody,
|
effectiveFinalHeaders,
|
||||||
|
effectiveFinalParams,
|
||||||
|
effectiveFinalBody,
|
||||||
|
},
|
||||||
|
updatedEnvs: { global: [], selected: envVariables },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import axios, { Method } from "axios";
|
import axios, { Method } from "axios";
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import * as E from "fp-ts/Either";
|
import * as E from "fp-ts/Either";
|
||||||
@@ -29,6 +29,38 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
|||||||
|
|
||||||
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
|
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes given variable, which includes checking for secret variables
|
||||||
|
* and getting value from system environment
|
||||||
|
* @param variable Variable to be processed
|
||||||
|
* @returns Updated variable with value from system environment
|
||||||
|
*/
|
||||||
|
const processVariables = (variable: Environment["variables"][number]) => {
|
||||||
|
if (variable.secret) {
|
||||||
|
return {
|
||||||
|
...variable,
|
||||||
|
value:
|
||||||
|
"value" in variable ? variable.value : process.env[variable.key] || "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return variable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes given envs, which includes processing each variable in global
|
||||||
|
* and selected envs
|
||||||
|
* @param envs Global + selected envs used by requests with in collection
|
||||||
|
* @returns Processed envs with each variable processed
|
||||||
|
*/
|
||||||
|
const processEnvs = (envs: HoppEnvs) => {
|
||||||
|
const processedEnvs = {
|
||||||
|
global: envs.global.map(processVariables),
|
||||||
|
selected: envs.selected.map(processVariables),
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedEnvs
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms given request data to request-config used by request-runner to
|
* Transforms given request data to request-config used by request-runner to
|
||||||
* perform HTTP request.
|
* perform HTTP request.
|
||||||
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
|||||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||||
const config: RequestConfig = {
|
const config: RequestConfig = {
|
||||||
supported: true,
|
supported: true,
|
||||||
|
displayUrl: req.effectiveFinalDisplayURL
|
||||||
};
|
};
|
||||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||||
const reqParams = finalParams(req);
|
const reqParams = finalParams(req);
|
||||||
@@ -221,9 +254,13 @@ export const processRequest =
|
|||||||
effectiveFinalParams: [],
|
effectiveFinalParams: [],
|
||||||
effectiveFinalURL: "",
|
effectiveFinalURL: "",
|
||||||
};
|
};
|
||||||
|
let updatedEnvs = <HoppEnvs>{};
|
||||||
|
|
||||||
|
// Fetch values for secret environment variables from system environment
|
||||||
|
const processedEnvs = processEnvs(envs)
|
||||||
|
|
||||||
// Executing pre-request-script
|
// Executing pre-request-script
|
||||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
|
||||||
if (E.isLeft(preRequestRes)) {
|
if (E.isLeft(preRequestRes)) {
|
||||||
printPreRequestRunner.fail();
|
printPreRequestRunner.fail();
|
||||||
|
|
||||||
@@ -231,8 +268,8 @@ export const processRequest =
|
|||||||
report.errors.push(preRequestRes.left);
|
report.errors.push(preRequestRes.left);
|
||||||
report.result = report.result && false;
|
report.result = report.result && false;
|
||||||
} else {
|
} else {
|
||||||
// Updating effective-request
|
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||||
effectiveRequest = preRequestRes.right;
|
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creating request-config for request-runner.
|
// Creating request-config for request-runner.
|
||||||
@@ -270,7 +307,7 @@ export const processRequest =
|
|||||||
const testScriptParams = getTestScriptParams(
|
const testScriptParams = getTestScriptParams(
|
||||||
_requestRunnerRes,
|
_requestRunnerRes,
|
||||||
request,
|
request,
|
||||||
envs
|
updatedEnvs
|
||||||
);
|
);
|
||||||
|
|
||||||
// Executing test-runner.
|
// Executing test-runner.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ESNext",
|
||||||
"module": "commonjs",
|
"module": "ESNext",
|
||||||
"outDir": ".",
|
"outDir": ".",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ import { defineConfig } from "tsup";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: [ "./src/index.ts" ],
|
entry: [ "./src/index.ts" ],
|
||||||
outDir: "./dist/",
|
outDir: "./dist/",
|
||||||
format: ["cjs"],
|
format: ["esm"],
|
||||||
platform: "node",
|
platform: "node",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
target: "node12",
|
target: "esnext",
|
||||||
skipNodeModulesBundle: false,
|
skipNodeModulesBundle: false,
|
||||||
esbuildOptions(options) {
|
esbuildOptions(options) {
|
||||||
options.bundle = true
|
options.bundle = true
|
||||||
},
|
},
|
||||||
noExternal: [
|
|
||||||
/\w+/
|
|
||||||
],
|
|
||||||
clean: true,
|
clean: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -429,6 +429,11 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splitpanes__pane {
|
||||||
|
@apply will-change-auto;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
.smart-splitter .splitpanes__splitter {
|
.smart-splitter .splitpanes__splitter {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply before:absolute;
|
@apply before:absolute;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "Go forward",
|
||||||
"group_by": "Group by",
|
"group_by": "Group by",
|
||||||
|
"hide_secret": "Hide secret",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
"less": "Less",
|
"less": "Less",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
|
"show_secret": "Show secret",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -238,6 +240,7 @@
|
|||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
|
"secret_environments": "Secrets are not synced to Hoppscotch",
|
||||||
"shared_requests": "Shared requests are empty",
|
"shared_requests": "Shared requests are empty",
|
||||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
@@ -269,6 +272,8 @@
|
|||||||
"quick_peek": "Environment Quick Peek",
|
"quick_peek": "Environment Quick Peek",
|
||||||
"replace_with_variable": "Replace with variable",
|
"replace_with_variable": "Replace with variable",
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secret_value": "Secret value",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
"set": "Set environment",
|
"set": "Set environment",
|
||||||
"set_as_environment": "Set as environment",
|
"set_as_environment": "Set as environment",
|
||||||
@@ -277,6 +282,7 @@
|
|||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
|
"variables":"Variables",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -413,6 +419,8 @@
|
|||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
"environment": {
|
"environment": {
|
||||||
"add_environment": "Add to Environment",
|
"add_environment": "Add to Environment",
|
||||||
|
"add_environment_value": "Add value",
|
||||||
|
"empty_value": "Environment value is empty for the variable '{variable}' ",
|
||||||
"not_found": "Environment variable “{environment}” not found."
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
@@ -889,6 +897,7 @@
|
|||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"shared_requests": "Shared Requests",
|
"shared_requests": "Shared Requests",
|
||||||
|
"share_tab_request": "Share tab request",
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.12.3",
|
"version": "2023.12.6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle', undefined, 'mouseclick')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
<icon-lucide-search class="svg-icons mr-2" />
|
<icon-lucide-search class="svg-icons mr-2" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
styles="sm:max-w-lg"
|
styles="sm:max-w-lg"
|
||||||
full-width
|
full-width
|
||||||
@close="emit('hide-modal')"
|
@close="closeSpotlightModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col border-b border-divider transition">
|
<div class="flex flex-col border-b border-divider transition">
|
||||||
@@ -86,35 +86,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
import { computed, ref, watch } from "vue"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||||
import {
|
import {
|
||||||
SpotlightService,
|
|
||||||
SpotlightSearchState,
|
SpotlightSearchState,
|
||||||
SpotlightSearcherResult,
|
SpotlightSearcherResult,
|
||||||
|
SpotlightService,
|
||||||
} from "~/services/spotlight"
|
} from "~/services/spotlight"
|
||||||
import { isEqual } from "lodash-es"
|
|
||||||
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
|
||||||
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
|
||||||
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
|
||||||
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
|
||||||
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
||||||
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
|
||||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
|
||||||
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
|
||||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
|
||||||
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
|
||||||
import {
|
import {
|
||||||
EnvironmentsSpotlightSearcherService,
|
EnvironmentsSpotlightSearcherService,
|
||||||
SwitchEnvSpotlightSearcherService,
|
SwitchEnvSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/environment.searcher"
|
} from "~/services/spotlight/searchers/environment.searcher"
|
||||||
|
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
||||||
|
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
||||||
|
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||||
|
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
||||||
|
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
||||||
|
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
||||||
|
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
||||||
|
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
||||||
|
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
||||||
|
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
||||||
import {
|
import {
|
||||||
SwitchWorkspaceSpotlightSearcherService,
|
SwitchWorkspaceSpotlightSearcherService,
|
||||||
WorkspaceSpotlightSearcherService,
|
WorkspaceSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -290,4 +291,17 @@ function newUseArrowKeysForNavigation() {
|
|||||||
|
|
||||||
return { selectedEntry }
|
return { selectedEntry }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeSpotlightModal() {
|
||||||
|
const analyticsData: HoppSpotlightSessionEventData = {
|
||||||
|
action: "close",
|
||||||
|
searcherID: null,
|
||||||
|
rank: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the action indicating `close` and rank as `null` in the state for analytics event logging
|
||||||
|
spotlightService.setAnalyticsData(analyticsData)
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -614,8 +614,8 @@ const addNewRootCollection = (name: string) => {
|
|||||||
requests: [],
|
requests: [],
|
||||||
headers: [],
|
headers: [],
|
||||||
auth: {
|
auth: {
|
||||||
authType: "inherit",
|
authType: "none",
|
||||||
authActive: false,
|
authActive: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'cookie')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -102,6 +102,8 @@ import {
|
|||||||
useCopyResponse,
|
useCopyResponse,
|
||||||
useDownloadResponse,
|
useDownloadResponse,
|
||||||
} from "~/composables/lens-actions"
|
} from "~/composables/lens-actions"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
// TODO: Build Managed Mode!
|
// TODO: Build Managed Mode!
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ const toast = useToast()
|
|||||||
|
|
||||||
const cookieEditor = ref<HTMLElement>()
|
const cookieEditor = ref<HTMLElement>()
|
||||||
const rawCookieString = ref("")
|
const rawCookieString = ref("")
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "cookie")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
cookieEditor,
|
cookieEditor,
|
||||||
@@ -131,7 +133,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/plain",
|
mode: "text/plain",
|
||||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<SmartEnvInput
|
||||||
v-model="editingValue"
|
v-model="editingValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="input"
|
class="input"
|
||||||
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
|
|||||||
addGlobalEnvVariable({
|
addGlobalEnvVariable({
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else if (scope.value.type === "my-environment") {
|
} else if (scope.value.type === "my-environment") {
|
||||||
addEnvironmentVariable(scope.value.index, {
|
addEnvironmentVariable(scope.value.index, {
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
|
||||||
@@ -340,13 +340,13 @@ const showImportFailedError = () => {
|
|||||||
|
|
||||||
const handleImportToStore = async (
|
const handleImportToStore = async (
|
||||||
environments: Environment[],
|
environments: Environment[],
|
||||||
globalEnv?: Environment
|
globalEnv?: NonSecretEnvironment
|
||||||
) => {
|
) => {
|
||||||
// if there's a global env, add them to the store
|
// if there's a global env, add them to the store
|
||||||
if (globalEnv) {
|
if (globalEnv) {
|
||||||
globalEnv.variables.forEach(({ key, value }) => {
|
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||||
addGlobalEnvVariable({ key, value })
|
addGlobalEnvVariable({ key, value, secret })
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
|
|||||||
@@ -210,7 +210,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||||
@@ -265,7 +268,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
|
|||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: props.modelValue.index,
|
index: props.modelValue.index,
|
||||||
name: props.modelValue.environment?.name,
|
name: props.modelValue.environment?.name,
|
||||||
|
variables: props.modelValue.environment?.variables,
|
||||||
}
|
}
|
||||||
} else if (props.modelValue?.type === "team-environment") {
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
return {
|
return {
|
||||||
type: "TEAM_ENV",
|
type: "TEAM_ENV",
|
||||||
name: props.modelValue.environment.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: props.modelValue.environment.id,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
|
variables: props.modelValue.environment.environment.variables,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: "global", name: "Global" }
|
return {
|
||||||
|
type: "global",
|
||||||
|
name: "Global",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
const environment =
|
const environment =
|
||||||
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const editGlobalEnv = () => {
|
const editGlobalEnv = () => {
|
||||||
invokeAction("modals.my.environment.edit", {
|
invokeAction("modals.global.environment.update", {})
|
||||||
envName: "Global",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editEnv = () => {
|
const editEnv = () => {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:env-vars="envVars"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsAdd
|
<EnvironmentsAdd
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="showConfirmRemoveEnvModal"
|
:show="showConfirmRemoveEnvModal"
|
||||||
:title="t('confirm.remove_team')"
|
:title="`${t('confirm.remove_environment')}`"
|
||||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||||
@resolve="removeSelectedEnvironment()"
|
@resolve="removeSelectedEnvironment()"
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
|
|||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
|
|||||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
const globalEnvironment = computed(() => ({
|
const globalEnvironment = computed(() => ({
|
||||||
|
v: 1 as const,
|
||||||
|
id: "Global",
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: globalEnv.value,
|
variables: globalEnv.value,
|
||||||
}))
|
}))
|
||||||
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
|
|||||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
const editingVariableValue = ref("")
|
const editingVariableValue = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const position = ref({ top: 0, left: 0 })
|
const position = ref({ top: 0, left: 0 })
|
||||||
|
|
||||||
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
|||||||
const editEnvironment = (environmentIndex: "Global") => {
|
const editEnvironment = (environmentIndex: "Global") => {
|
||||||
editingEnvironmentIndex.value = environmentIndex
|
editingEnvironmentIndex.value = environmentIndex
|
||||||
action.value = "edit"
|
action.value = "edit"
|
||||||
|
editingVariableName.value = ""
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
|
|||||||
|
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
editingVariableValue.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("modals.environment.new", () => {
|
defineActionHandler("modals.environment.new", () => {
|
||||||
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
|||||||
showConfirmRemoveEnvModal.value = true
|
showConfirmRemoveEnvModal.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const additionalVars = ref<Environment["variables"]>([])
|
||||||
|
|
||||||
|
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.global.environment.update",
|
||||||
({ envName, variableName }) => {
|
({ variables, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variables) {
|
||||||
envName === "Global" && editEnvironment("Global")
|
additionalVars.value = variables
|
||||||
|
}
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
editEnvironment("Global")
|
||||||
|
editingVariableName.value = "Global"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,76 +16,103 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
/>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconHelpCircle"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="clearIcon"
|
||||||
|
@click="clearContent()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import { clone } from "lodash-es"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { environmentsStore } from "~/newstore/environments"
|
import { environmentsStore } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
import { uniqueId } from "lodash-es"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
|
||||||
value: string
|
value: string
|
||||||
|
key: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +186,7 @@ const props = withDefaults(
|
|||||||
action: "edit" | "new"
|
action: "edit" | "new"
|
||||||
editingEnvironmentIndex?: number | "Global" | null
|
editingEnvironmentIndex?: number | "Global" | null
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -162,6 +194,7 @@ const props = withDefaults(
|
|||||||
action: "edit",
|
action: "edit",
|
||||||
editingEnvironmentIndex: null,
|
editingEnvironmentIndex: null,
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -172,11 +205,55 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secrets"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string>("")
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
|||||||
|
|
||||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const workingEnv = computed(() => {
|
const workingEnv = computed(() => {
|
||||||
if (props.editingEnvironmentIndex === "Global") {
|
if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
const vars =
|
||||||
|
props.editingVariableName === "Global"
|
||||||
|
? props.envVars()
|
||||||
|
: getGlobalVariables()
|
||||||
return {
|
return {
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: getGlobalVariables(),
|
variables: vars,
|
||||||
} as Environment
|
} as Environment
|
||||||
} else if (props.action === "new") {
|
} else if (props.action === "new") {
|
||||||
return {
|
return {
|
||||||
|
id: uniqueId(),
|
||||||
name: "",
|
name: "",
|
||||||
variables: props.envVars(),
|
variables: props.envVars(),
|
||||||
}
|
}
|
||||||
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
|
|||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
variables,
|
variables,
|
||||||
|
A.filter(({ secret }) => !secret),
|
||||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -239,11 +326,29 @@ watch(
|
|||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = workingEnv.value?.name ?? null
|
editingName.value = workingEnv.value?.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
|
|
||||||
|
if (props.editingEnvironmentIndex !== "Global") {
|
||||||
|
editingID.value = workingEnv.value?.id ?? uniqueId()
|
||||||
|
}
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
workingEnv.value?.variables ?? [],
|
workingEnv.value?.variables ?? [],
|
||||||
A.map((e) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
props.editingEnvironmentIndex === "Global"
|
||||||
|
? "Global"
|
||||||
|
: workingEnv.value?.id,
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -251,7 +356,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
vars.value = []
|
vars.value = vars.value.filter((e) =>
|
||||||
|
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||||
|
)
|
||||||
|
|
||||||
clearIcon.value = IconDone
|
clearIcon.value = IconDone
|
||||||
toast.success(`${t("state.cleared")}`)
|
toast.success(`${t("state.cleared")}`)
|
||||||
}
|
}
|
||||||
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEnvironment = () => {
|
const saveEnvironment = () => {
|
||||||
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterdVariables = pipe(
|
const filteredVariables = pipe(
|
||||||
vars.value,
|
vars.value,
|
||||||
A.filterMap(
|
A.filterMap(
|
||||||
flow(
|
flow(
|
||||||
@@ -286,14 +398,43 @@ const saveEnvironment = () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (editingID.value) {
|
||||||
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
editingID.value,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
} else if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const environmentUpdated: Environment = {
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: uniqueId(),
|
||||||
name: editingName.value,
|
name: editingName.value,
|
||||||
variables: filterdVariables,
|
variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
// Creating a new environment
|
// Creating a new environment
|
||||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
createEnvironment(
|
||||||
|
editingName.value,
|
||||||
|
environmentUpdated.variables,
|
||||||
|
editingID.value
|
||||||
|
)
|
||||||
setSelectedEnvironmentIndex({
|
setSelectedEnvironmentIndex({
|
||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: envList.value.length - 1,
|
index: envList.value.length - 1,
|
||||||
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -150,6 +152,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () => {
|
const exportEnvironmentAsJSON = () => {
|
||||||
const { environment, environmentIndex } = props
|
const { environment, environmentIndex } = props
|
||||||
exportAsJSON(environment, environmentIndex)
|
exportAsJSON(environment, environmentIndex)
|
||||||
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
|
|||||||
if (props.environmentIndex === null) return
|
if (props.environmentIndex === null) return
|
||||||
if (props.environmentIndex !== "Global") {
|
if (props.environmentIndex !== "Global") {
|
||||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironmentIndex = ref<number | null>(null)
|
const editingEnvironmentIndex = ref<number | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||||
action.value = "new"
|
action.value = "new"
|
||||||
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.my.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const envIndex: number = environments.value.findIndex(
|
const envIndex: number = environments.value.findIndex(
|
||||||
(environment: Environment) => {
|
(environment: Environment) => {
|
||||||
return environment.name === envName
|
return environment.name === envName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (envName !== "Global") editEnvironment(envIndex)
|
if (envName !== "Global") {
|
||||||
|
editEnvironment(envIndex)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,90 +16,112 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:class="isViewer && 'opacity-25'"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
:disabled="isViewer"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
:readonly="isViewer"
|
|
||||||
/>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
v-if="isViewer"
|
:icon="IconHelpCircle"
|
||||||
disabled
|
/>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
v-if="!isViewer"
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
<HoppButtonSecondary
|
:title="t('action.clear_all')"
|
||||||
v-else
|
:icon="clearIcon"
|
||||||
:label="`${t('add.new')}`"
|
@click="clearContent()"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-if="!isViewer"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="!isViewer"
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
:disabled="isViewer"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:readonly="isViewer && !tab.isSecret"
|
||||||
|
/>
|
||||||
|
<div v-if="!isViewer" class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isViewer" #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="`${t('action.save')}`"
|
:label="`${t('action.save')}`"
|
||||||
@@ -119,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -141,13 +163,17 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +189,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: string | undefined
|
editingTeamId: string | undefined
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
isViewer?: boolean
|
isViewer?: boolean
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -172,6 +199,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: "",
|
editingTeamId: "",
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
isViewer: false,
|
isViewer: false,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -182,11 +210,59 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secrets"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string | null>(null)
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -215,22 +291,34 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
editingName.value = props.editingEnvironment?.environment.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
editingName.value = null
|
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.envVars() ?? [],
|
props.envVars() ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.map((e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: clone(e),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
} else if (props.editingEnvironment !== null) {
|
} else if (props.editingEnvironment !== null) {
|
||||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
editingID.value = props.editingEnvironment.id
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.editingEnvironment.environment.variables ?? [],
|
props.editingEnvironment.environment.variables ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
editingID.value ?? "",
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -250,12 +338,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -278,52 +370,102 @@ const saveEnvironment = async () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: editingID.value ?? "",
|
||||||
|
name: editingName.value,
|
||||||
|
variables,
|
||||||
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_CREATE_ENVIRONMENT",
|
type: "HOPP_CREATE_ENVIRONMENT",
|
||||||
workspaceType: "team",
|
workspaceType: "team",
|
||||||
})
|
})
|
||||||
|
|
||||||
await pipe(
|
if (!props.isViewer) {
|
||||||
createTeamEnvironment(
|
await pipe(
|
||||||
JSON.stringify(filterdVariables),
|
createTeamEnvironment(
|
||||||
props.editingTeamId,
|
JSON.stringify(environmentUpdated.variables),
|
||||||
editingName.value
|
props.editingTeamId,
|
||||||
),
|
environmentUpdated.name
|
||||||
TE.match(
|
),
|
||||||
(err: GQLError<string>) => {
|
TE.match(
|
||||||
console.error(err)
|
(err: GQLError<string>) => {
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
console.error(err)
|
||||||
},
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
() => {
|
isLoading.value = false
|
||||||
hideModal()
|
},
|
||||||
toast.success(`${t("environment.created")}`)
|
(res) => {
|
||||||
}
|
const envID = res.createTeamEnvironment.id
|
||||||
)
|
if (envID) {
|
||||||
)()
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
envID,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.created")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!props.editingEnvironment) {
|
if (!props.editingEnvironment) {
|
||||||
console.error("No Environment Found")
|
console.error("No Environment Found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipe(
|
if (editingID.value) {
|
||||||
updateTeamEnvironment(
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
JSON.stringify(filterdVariables),
|
editingID.value,
|
||||||
props.editingEnvironment.id,
|
secretVariables
|
||||||
editingName.value
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
hideModal()
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)()
|
|
||||||
|
// If the user is a viewer, we don't need to update the environment in BE
|
||||||
|
// just update the secret environment in the local storage
|
||||||
|
if (props.isViewer) {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isViewer) {
|
||||||
|
await pipe(
|
||||||
|
updateTeamEnvironment(
|
||||||
|
JSON.stringify(environmentUpdated.variables),
|
||||||
|
props.editingEnvironment.id,
|
||||||
|
environmentUpdated.name
|
||||||
|
),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
isLoading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
v-if="!isViewer"
|
|
||||||
ref="options"
|
ref="options"
|
||||||
interactive
|
interactive
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -57,6 +56,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="duplicate"
|
ref="duplicate"
|
||||||
:icon="IconCopy"
|
:icon="IconCopy"
|
||||||
:label="`${t('action.duplicate')}`"
|
:label="`${t('action.duplicate')}`"
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="exportAsJsonEl"
|
ref="exportAsJsonEl"
|
||||||
:icon="IconEdit"
|
:icon="IconEdit"
|
||||||
:label="`${t('export.as_json')}`"
|
:label="`${t('export.as_json')}`"
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
:icon="IconTrash2"
|
:icon="IconTrash2"
|
||||||
:label="`${t('action.delete')}`"
|
:label="`${t('action.delete')}`"
|
||||||
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -137,6 +141,8 @@ const emit = defineEmits<{
|
|||||||
(e: "edit-environment"): void
|
(e: "edit-environment"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () =>
|
const exportEnvironmentAsJSON = () =>
|
||||||
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
toast.success(`${t("team_environment.deleted")}`)
|
toast.success(`${t("team_environment.deleted")}`)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
:editing-environment="editingEnvironment"
|
:editing-environment="editingEnvironment"
|
||||||
:editing-team-id="team?.id"
|
:editing-team-id="team?.id"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
:is-viewer="team?.myRole === 'VIEWER'"
|
:is-viewer="team?.myRole === 'VIEWER'"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||||
|
|
||||||
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironment.value = null
|
editingEnvironment.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.team.environment.edit",
|
"modals.team.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const teamEnvToEdit = props.teamEnvironments.find(
|
const teamEnvToEdit = props.teamEnvironments.find(
|
||||||
(environment) => environment.environment.name === envName
|
(environment) => environment.environment.name === envName
|
||||||
)
|
)
|
||||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
if (teamEnvToEdit) {
|
||||||
|
editEnvironment(teamEnvToEdit)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,17 +31,6 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -54,6 +43,17 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -284,7 +284,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "none",
|
authType: "inherit",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlHeaders')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -315,6 +315,8 @@ import { commonHeaders } from "~/helpers/headers"
|
|||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { HoppGQLHeader } from "~/helpers/graphql"
|
import { HoppGQLHeader } from "~/helpers/graphql"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
@@ -338,7 +340,7 @@ const request = useVModel(props, "modelValue", emit)
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlHeaders")
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkHeaders = ref("")
|
const bulkHeaders = ref("")
|
||||||
|
|
||||||
@@ -353,7 +355,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -61,9 +61,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlQuery')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -112,6 +112,8 @@ import {
|
|||||||
socketDisconnect,
|
socketDisconnect,
|
||||||
subscriptionState,
|
subscriptionState,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const queryEditor = ref<any | null>(null)
|
const queryEditor = ref<any | null>(null)
|
||||||
@@ -137,7 +139,7 @@ const prettifyQueryIcon = refAutoReset<
|
|||||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||||
>(IconWand, 1000)
|
>(IconWand, 1000)
|
||||||
|
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlQuery")
|
||||||
|
|
||||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||||
|
|
||||||
@@ -184,7 +186,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "graphql",
|
mode: "graphql",
|
||||||
placeholder: `${t("request.query")}`,
|
placeholder: `${t("request.query")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: createGQLQueryLinter(schema),
|
linter: createGQLQueryLinter(schema),
|
||||||
completer: queryCompleter(schema),
|
completer: queryCompleter(schema),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-1 flex-col">
|
<div class="h-full">
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-model="selectedOptionTab"
|
v-model="selectedOptionTab"
|
||||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||||
|
|||||||
@@ -16,9 +16,11 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="
|
||||||
|
toggleNestedSetting('WRAP_LINES', 'graphqlResponseBody')
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -70,7 +72,9 @@
|
|||||||
</tippy>
|
</tippy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
<div class="h-full">
|
||||||
|
<div ref="schemaEditor"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="response[0].error.component"
|
:is="response[0].error.component"
|
||||||
@@ -99,6 +103,8 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||||
import {
|
import {
|
||||||
useCopyInterface,
|
useCopyInterface,
|
||||||
@@ -133,8 +139,8 @@ const responseString = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlResponseBody")
|
||||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
schemaEditor,
|
schemaEditor,
|
||||||
@@ -143,7 +149,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/ld+json",
|
mode: "application/ld+json",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -127,9 +127,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlSchema')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -145,11 +145,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="schemaString" class="h-full relative w-full">
|
||||||
v-if="schemaString"
|
<div ref="schemaEditor" class="absolute inset-0"></div>
|
||||||
ref="schemaEditor"
|
</div>
|
||||||
class="flex flex-1 flex-col"
|
|
||||||
></div>
|
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
@@ -202,6 +200,8 @@ import {
|
|||||||
subscriptionFields,
|
subscriptionFields,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||||
@@ -349,7 +349,7 @@ const handleJumpToType = async (type: GraphQLType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlSchema")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
schemaEditor,
|
schemaEditor,
|
||||||
@@ -358,7 +358,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "graphql",
|
mode: "graphql",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlVariables')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -67,7 +67,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
<div class="h-full relative">
|
||||||
|
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -93,6 +95,8 @@ import {
|
|||||||
socketDisconnect,
|
socketDisconnect,
|
||||||
subscriptionState,
|
subscriptionState,
|
||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -114,7 +118,7 @@ const variableString = useVModel(props, "modelValue", emit)
|
|||||||
|
|
||||||
const variableEditor = ref<any | null>(null)
|
const variableEditor = ref<any | null>(null)
|
||||||
|
|
||||||
const linewrapEnabled = ref(false)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")
|
||||||
|
|
||||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
IconCopy,
|
IconCopy,
|
||||||
@@ -131,7 +135,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/ld+json",
|
mode: "application/ld+json",
|
||||||
placeholder: `${t("request.variables")}`,
|
placeholder: `${t("request.variables")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: computed(() =>
|
linter: computed(() =>
|
||||||
variableString.value.length > 0 ? jsonLinter : null
|
variableString.value.length > 0 ? jsonLinter : null
|
||||||
|
|||||||
@@ -331,7 +331,8 @@ const deleteHistory = (entry: HistoryEntry) => {
|
|||||||
const addToCollection = (entry: HistoryEntry) => {
|
const addToCollection = (entry: HistoryEntry) => {
|
||||||
if (props.page === "rest") {
|
if (props.page === "rest") {
|
||||||
invokeAction("request.save-as", {
|
invokeAction("request.save-as", {
|
||||||
request: entry.request,
|
requestType: "rest",
|
||||||
|
request: entry.request as HoppRESTRequest,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,6 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
|
||||||
label="None"
|
|
||||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
|
||||||
:active="authName === 'None'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
auth.authType = 'none'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="!isRootCollection"
|
v-if="!isRootCollection"
|
||||||
label="Inherit"
|
label="Inherit"
|
||||||
@@ -54,6 +43,17 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
label="None"
|
||||||
|
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||||
|
:active="authName === 'None'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.authType = 'none'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
label="Basic Auth"
|
label="Basic Auth"
|
||||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||||
@@ -265,7 +265,7 @@ const authActive = pluckRef(auth, "authActive")
|
|||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
authType: "none",
|
authType: "inherit",
|
||||||
authActive: true,
|
authActive: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'codeGen')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -161,6 +161,8 @@ import cloneDeep from "lodash-es/cloneDeep"
|
|||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -187,6 +189,8 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
const requestCode = computed(() => {
|
const requestCode = computed(() => {
|
||||||
const aggregateEnvs = getAggregateEnvs()
|
const aggregateEnvs = getAggregateEnvs()
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: "env",
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: aggregateEnvs,
|
variables: aggregateEnvs,
|
||||||
}
|
}
|
||||||
@@ -222,7 +226,7 @@ const requestCode = computed(() => {
|
|||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
const generatedCode = ref<any | null>(null)
|
const generatedCode = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "codeGen")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
generatedCode,
|
generatedCode,
|
||||||
@@ -231,7 +235,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/plain",
|
mode: "text/plain",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
v-if="bulkMode"
|
v-if="bulkMode"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpHeaders')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -49,7 +49,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
<div v-if="bulkMode" class="h-full relative w-full">
|
||||||
|
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="workingHeaders"
|
v-model="workingHeaders"
|
||||||
@@ -332,6 +334,8 @@ 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"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -346,7 +350,7 @@ const idTicker = ref(0)
|
|||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkHeaders = ref("")
|
const bulkHeaders = ref("")
|
||||||
const bulkEditor = ref<any | null>(null)
|
const bulkEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpHeaders")
|
||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
@@ -371,7 +375,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter,
|
linter,
|
||||||
completer: null,
|
completer: null,
|
||||||
@@ -553,7 +557,7 @@ const clearContent = () => {
|
|||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||||
|
|
||||||
const computedHeaders = computed(() =>
|
const computedHeaders = computed(() =>
|
||||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
getComputedHeaders(request.value, aggregateEnvs.value, false).map(
|
||||||
(header, index) => ({
|
(header, index) => ({
|
||||||
id: `header-${index}`,
|
id: `header-${index}`,
|
||||||
...header,
|
...header,
|
||||||
@@ -606,7 +610,8 @@ const inheritedProperties = computed(() => {
|
|||||||
const computedAuthHeader = getComputedAuthHeaders(
|
const computedAuthHeader = getComputedAuthHeaders(
|
||||||
aggregateEnvs.value,
|
aggregateEnvs.value,
|
||||||
request.value,
|
request.value,
|
||||||
props.inheritedProperties.auth.inheritedAuth
|
props.inheritedProperties.auth.inheritedAuth,
|
||||||
|
false
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'importCurl')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@@ -96,6 +96,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ const tabs = useService(RESTTabService)
|
|||||||
const curl = ref("")
|
const curl = ref("")
|
||||||
|
|
||||||
const curlEditor = ref<any | null>(null)
|
const curlEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "importCurl")
|
||||||
|
|
||||||
const props = defineProps<{ show: boolean; text: string }>()
|
const props = defineProps<{ show: boolean; text: string }>()
|
||||||
|
|
||||||
@@ -117,7 +119,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/x-sh",
|
mode: "application/x-sh",
|
||||||
placeholder: `${t("request.enter_curl")}`,
|
placeholder: `${t("request.enter_curl")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -3,14 +3,25 @@
|
|||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="oidcDiscoveryURL"
|
v-model="oidcDiscoveryURL"
|
||||||
|
:styles="
|
||||||
|
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
||||||
|
"
|
||||||
placeholder="OpenID Connect Discovery URL"
|
placeholder="OpenID Connect Discovery URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" />
|
<SmartEnvInput
|
||||||
|
v-model="authURL"
|
||||||
|
placeholder="Authorization URL"
|
||||||
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
|
></SmartEnvInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" />
|
<SmartEnvInput
|
||||||
|
v-model="accessTokenURL"
|
||||||
|
placeholder="Access Token URL"
|
||||||
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||||
@@ -44,6 +55,7 @@ import { useToast } from "@composables/toast"
|
|||||||
import { tokenRequest } from "~/helpers/oauth"
|
import { tokenRequest } from "~/helpers/oauth"
|
||||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -66,10 +78,16 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
||||||
|
const hasOIDCURL = computed(() => {
|
||||||
|
return oidcDiscoveryURL.value
|
||||||
|
})
|
||||||
|
|
||||||
const authURL = pluckRef(auth, "authURL")
|
const authURL = pluckRef(auth, "authURL")
|
||||||
|
|
||||||
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
||||||
|
const hasAccessTokenOrAuthURL = computed(() => {
|
||||||
|
return accessTokenURL.value || authURL.value
|
||||||
|
})
|
||||||
|
|
||||||
const clientID = pluckRef(auth, "clientID")
|
const clientID = pluckRef(auth, "clientID")
|
||||||
|
|
||||||
@@ -88,13 +106,11 @@ function translateTokenRequestError(error: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAccessTokenRequest = async () => {
|
const handleAccessTokenRequest = async () => {
|
||||||
if (
|
if (!oidcDiscoveryURL.value && !(authURL.value || accessTokenURL.value)) {
|
||||||
oidcDiscoveryURL.value === "" &&
|
|
||||||
(authURL.value === "" || accessTokenURL.value === "")
|
|
||||||
) {
|
|
||||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const envs = getCombinedEnvVariables()
|
const envs = getCombinedEnvVariables()
|
||||||
const envVars = [...envs.selected, ...envs.global]
|
const envVars = [...envs.selected, ...envs.global]
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
v-if="bulkMode"
|
v-if="bulkMode"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpParams')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -44,7 +44,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
<div v-if="bulkMode" class="h-full relative">
|
||||||
|
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="workingParams"
|
v-model="workingParams"
|
||||||
@@ -205,6 +207,8 @@ 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"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
@@ -217,7 +221,7 @@ const idTicker = ref(0)
|
|||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkParams = ref("")
|
const bulkParams = ref("")
|
||||||
const bulkEditor = ref<any | null>(null)
|
const bulkEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpParams")
|
||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
@@ -228,7 +232,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter,
|
linter,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpPreRequest')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div 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 h-full relative">
|
||||||
<div ref="preRequestEditor" class="h-full"></div>
|
<div ref="preRequestEditor" class="h-full absolute inset-0"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/preRequest"
|
|||||||
import completer from "~/helpers/editor/completion/preRequest"
|
import completer from "~/helpers/editor/completion/preRequest"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ const emit = defineEmits<{
|
|||||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
const preRequestScript = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
const preRequestEditor = ref<any | null>(null)
|
const preRequestEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
preRequestEditor,
|
preRequestEditor,
|
||||||
@@ -93,7 +95,7 @@ useCodemirror(
|
|||||||
reactive({
|
reactive({
|
||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/javascript",
|
mode: "application/javascript",
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
placeholder: `${t("preRequest.javascript_code")}`,
|
placeholder: `${t("preRequest.javascript_code")}`,
|
||||||
},
|
},
|
||||||
linter,
|
linter,
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpRequestBody')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="
|
v-if="
|
||||||
@@ -59,7 +59,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="rawBodyParameters" class="flex flex-1 flex-col"></div>
|
<div class="h-full relative">
|
||||||
|
<div ref="rawBodyParameters" class="absolute inset-0"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,6 +87,8 @@ import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
|||||||
import jsonLinter from "~/helpers/editor/linting/json"
|
import jsonLinter from "~/helpers/editor/linting/json"
|
||||||
import { readFileAsText } from "~/helpers/functional/files"
|
import { readFileAsText } from "~/helpers/functional/files"
|
||||||
import xmlFormat from "xml-formatter"
|
import xmlFormat from "xml-formatter"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
type PossibleContentTypes = Exclude<
|
type PossibleContentTypes = Exclude<
|
||||||
ValidContentTypes,
|
ValidContentTypes,
|
||||||
@@ -122,7 +126,7 @@ const langLinter = computed(() =>
|
|||||||
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
||||||
)
|
)
|
||||||
|
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestBody")
|
||||||
const rawBodyParameters = ref<any | null>(null)
|
const rawBodyParameters = ref<any | null>(null)
|
||||||
|
|
||||||
const codemirrorValue: Ref<string | undefined> =
|
const codemirrorValue: Ref<string | undefined> =
|
||||||
@@ -148,7 +152,7 @@ useCodemirror(
|
|||||||
codemirrorValue,
|
codemirrorValue,
|
||||||
reactive({
|
reactive({
|
||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
mode: rawInputEditorLang,
|
mode: rawInputEditorLang,
|
||||||
placeholder: t("request.raw_body").toString(),
|
placeholder: t("request.raw_body").toString(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ import IconShare2 from "~icons/lucide/share-2"
|
|||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService } from "~/services/inspection"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
@@ -263,6 +263,7 @@ import { HoppTab } from "~/services/tab"
|
|||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||||
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const interceptorService = useService(InterceptorService)
|
const interceptorService = useService(InterceptorService)
|
||||||
@@ -326,6 +327,8 @@ const inspectionService = useService(InspectionService)
|
|||||||
|
|
||||||
const tabs = useService(RESTTabService)
|
const tabs = useService(RESTTabService)
|
||||||
|
|
||||||
|
const workspaceService = useService(WorkspaceService)
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
toast.error(`${t("empty.endpoint")}`)
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
@@ -341,6 +344,7 @@ const newSendRequest = async () => {
|
|||||||
type: "HOPP_REQUEST_RUN",
|
type: "HOPP_REQUEST_RUN",
|
||||||
platform: "rest",
|
platform: "rest",
|
||||||
strategy: interceptorService.currentInterceptorID.value!,
|
strategy: interceptorService.currentInterceptorID.value!,
|
||||||
|
workspaceType: workspaceService.currentWorkspace.value.type,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [cancel, streamPromise] = runRESTRequest$(tab)
|
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||||
@@ -395,17 +399,14 @@ const newSendRequest = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ensureMethodInEndpoint = () => {
|
const ensureMethodInEndpoint = () => {
|
||||||
if (
|
const endpoint = newEndpoint.value.trim()
|
||||||
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
tab.value.document.request.endpoint = endpoint
|
||||||
!newEndpoint.value.startsWith("<<")
|
if (!/^http[s]?:\/\//.test(endpoint) && !endpoint.startsWith("<<")) {
|
||||||
) {
|
const domain = endpoint.split(/[/:#?]+/)[0]
|
||||||
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
|
||||||
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||||
tab.value.document.request.endpoint =
|
tab.value.document.request.endpoint = "http://" + endpoint
|
||||||
"http://" + tab.value.document.request.endpoint
|
|
||||||
} else {
|
} else {
|
||||||
tab.value.document.request.endpoint =
|
tab.value.document.request.endpoint = "https://" + endpoint
|
||||||
"https://" + tab.value.document.request.endpoint
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,25 +578,12 @@ defineActionHandler("request.share-request", shareRequest)
|
|||||||
defineActionHandler("request.method.next", cycleDownMethod)
|
defineActionHandler("request.method.next", cycleDownMethod)
|
||||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
defineActionHandler(
|
defineActionHandler("request.save-as", (req) => {
|
||||||
"request.save-as",
|
showSaveRequestModal.value = true
|
||||||
(
|
if (req?.requestType === "rest") {
|
||||||
req:
|
request.value = req.request
|
||||||
| {
|
|
||||||
requestType: "rest"
|
|
||||||
request: HoppRESTRequest
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
requestType: "gql"
|
|
||||||
request: HoppGQLRequest
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
if (req && req.requestType === "rest") {
|
|
||||||
request.value = req.request
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||||
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<HttpResponse v-model:document="tab.document" />
|
<HttpResponse v-model:document="tab.document" :is-embed="false" />
|
||||||
</template>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.r="renameAction?.$el.click()"
|
@keyup.r="renameAction?.$el.click()"
|
||||||
|
@keyup.s="shareRequestAction?.$el.click()"
|
||||||
@keyup.d="duplicateAction?.$el.click()"
|
@keyup.d="duplicateAction?.$el.click()"
|
||||||
@keyup.w="closeAction?.$el.click()"
|
@keyup.w="closeAction?.$el.click()"
|
||||||
@keyup.x="closeOthersAction?.$el.click()"
|
@keyup.x="closeOthersAction?.$el.click()"
|
||||||
@@ -58,6 +59,18 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="shareRequestAction"
|
||||||
|
:icon="IconShare2"
|
||||||
|
:label="t('tab.share_tab_request')"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('share-tab-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
v-if="isRemovable"
|
v-if="isRemovable"
|
||||||
ref="closeAction"
|
ref="closeAction"
|
||||||
@@ -99,6 +112,7 @@ import IconXCircle from "~icons/lucide/x-circle"
|
|||||||
import IconXSquare from "~icons/lucide/x-square"
|
import IconXSquare from "~icons/lucide/x-square"
|
||||||
import IconFileEdit from "~icons/lucide/file-edit"
|
import IconFileEdit from "~icons/lucide/file-edit"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { HoppTab } from "~/services/tab"
|
import { HoppTab } from "~/services/tab"
|
||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
|
|
||||||
@@ -114,6 +128,7 @@ const emit = defineEmits<{
|
|||||||
(event: "close-tab"): void
|
(event: "close-tab"): void
|
||||||
(event: "close-other-tabs"): void
|
(event: "close-other-tabs"): void
|
||||||
(event: "duplicate-tab"): void
|
(event: "duplicate-tab"): void
|
||||||
|
(event: "share-tab-request"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
@@ -123,4 +138,5 @@ const renameAction = ref<HTMLButtonElement | null>(null)
|
|||||||
const closeAction = ref<HTMLButtonElement | null>(null)
|
const closeAction = ref<HTMLButtonElement | null>(null)
|
||||||
const closeOthersAction = ref<HTMLButtonElement | null>(null)
|
const closeOthersAction = ref<HTMLButtonElement | null>(null)
|
||||||
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const shareRequestAction = ref<HTMLButtonElement | null>(null)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import {
|
import {
|
||||||
globalEnv$,
|
globalEnv$,
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
setGlobalEnvVariables,
|
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||||
@@ -225,6 +224,7 @@ import { useColorMode } from "~/composables/theming"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppTestResult | null | undefined
|
modelValue: HoppTestResult | null | undefined
|
||||||
@@ -304,9 +304,10 @@ const globalHasAdditions = computed(() => {
|
|||||||
|
|
||||||
const addEnvToGlobal = () => {
|
const addEnvToGlobal = () => {
|
||||||
if (!testResults.value?.envDiff.selected.additions) return
|
if (!testResults.value?.envDiff.selected.additions) return
|
||||||
setGlobalEnvVariables([
|
|
||||||
...globalEnvVars.value,
|
invokeAction("modals.global.environment.update", {
|
||||||
...testResults.value.envDiff.selected.additions,
|
variables: testResults.value.envDiff.selected.additions,
|
||||||
])
|
isSecret: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpTest')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div 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 h-full relative">
|
||||||
<div ref="testScriptEditor" class="h-full"></div>
|
<div ref="testScriptEditor" class="h-full absolute inset-0"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/testScript"
|
|||||||
import completer from "~/helpers/editor/completion/testScript"
|
import completer from "~/helpers/editor/completion/testScript"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits(["update:modelValue"])
|
const emit = defineEmits(["update:modelValue"])
|
||||||
const testScript = useVModel(props, "modelValue", emit)
|
const testScript = useVModel(props, "modelValue", emit)
|
||||||
const testScriptEditor = ref<any | null>(null)
|
const testScriptEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
testScriptEditor,
|
testScriptEditor,
|
||||||
@@ -89,7 +91,7 @@ useCodemirror(
|
|||||||
reactive({
|
reactive({
|
||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/javascript",
|
mode: "application/javascript",
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
placeholder: `${t("test.javascript_code")}`,
|
placeholder: `${t("test.javascript_code")}`,
|
||||||
},
|
},
|
||||||
linter,
|
linter,
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
v-if="bulkMode"
|
v-if="bulkMode"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpUrlEncoded')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -44,7 +44,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
<div v-if="bulkMode" class="h-full relative">
|
||||||
|
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="workingUrlEncodedParams"
|
v-model="workingUrlEncodedParams"
|
||||||
@@ -196,6 +198,8 @@ import { useColorMode } from "@composables/theming"
|
|||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
type Body = HoppRESTReqBody & {
|
type Body = HoppRESTReqBody & {
|
||||||
contentType: "application/x-www-form-urlencoded"
|
contentType: "application/x-www-form-urlencoded"
|
||||||
@@ -220,7 +224,7 @@ const idTicker = ref(0)
|
|||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkUrlEncodedParams = ref("")
|
const bulkUrlEncodedParams = ref("")
|
||||||
const bulkEditor = ref<any | null>(null)
|
const bulkEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpUrlEncoded")
|
||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
@@ -231,7 +235,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter,
|
linter,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
@@ -44,11 +44,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-show="!previewEnabled" class="h-full">
|
||||||
v-show="!previewEnabled"
|
<div ref="htmlResponse" class="flex flex-1 flex-col"></div>
|
||||||
ref="htmlResponse"
|
</div>
|
||||||
class="flex flex-1 flex-col"
|
|
||||||
></div>
|
|
||||||
<iframe
|
<iframe
|
||||||
v-show="previewEnabled"
|
v-show="previewEnabled"
|
||||||
ref="previewFrame"
|
ref="previewFrame"
|
||||||
@@ -76,6 +74,8 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const htmlResponse = ref<any | null>(null)
|
const htmlResponse = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||||
|
|
||||||
const { responseBodyText } = useResponseBody(props.response)
|
const { responseBodyText } = useResponseBody(props.response)
|
||||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||||
@@ -104,7 +104,7 @@ useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "htmlmixed",
|
mode: "htmlmixed",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': linewrapEnabled }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
:icon="IconWrapText"
|
:icon="IconWrapText"
|
||||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
@@ -119,11 +119,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="h-full">
|
||||||
ref="jsonResponse"
|
<div
|
||||||
class="flex h-auto h-full flex-1 flex-col"
|
ref="jsonResponse"
|
||||||
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
||||||
></div>
|
></div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="outlinePath"
|
v-if="outlinePath"
|
||||||
class="sticky bottom-0 z-10 flex flex-shrink-0 flex-nowrap overflow-auto overflow-x-auto border-t border-dividerLight bg-primaryLight px-2"
|
class="sticky bottom-0 z-10 flex flex-shrink-0 flex-nowrap overflow-auto overflow-x-auto border-t border-dividerLight bg-primaryLight px-2"
|
||||||
@@ -260,6 +261,8 @@ import {
|
|||||||
} from "@composables/lens-actions"
|
} from "@composables/lens-actions"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -371,8 +374,8 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
|||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
const jsonResponse = ref<any | null>(null)
|
const jsonResponse = ref<any | null>(null)
|
||||||
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
|
||||||
|
|
||||||
const { cursor } = useCodemirror(
|
const { cursor } = useCodemirror(
|
||||||
jsonResponse,
|
jsonResponse,
|
||||||
@@ -381,7 +384,7 @@ const { cursor } = useCodemirror(
|
|||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "application/ld+json",
|
mode: "application/ld+json",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
lineWrapping: linewrapEnabled,
|
lineWrapping: WRAP_LINES,
|
||||||
},
|
},
|
||||||
linter: null,
|
linter: null,
|
||||||
completer: null,
|
completer: null,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user