Compare commits
122 Commits
feat/serve
...
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 | ||
|
|
aab76f1358 | ||
|
|
a28a576c41 | ||
|
|
0d0ad7a2f8 | ||
|
|
1df9de44b7 | ||
|
|
4cba03e53f | ||
|
|
9e1466a877 | ||
|
|
b81ccb4ee3 | ||
|
|
27d0a7c437 | ||
|
|
aca96dd5f2 | ||
|
|
c0dbcc901f | ||
|
|
ba52c8cc37 | ||
|
|
d1f6f40ef8 | ||
|
|
99f5070f71 | ||
|
|
cd371fc9d4 | ||
|
|
59fef248c0 | ||
|
|
286fcd2bb0 | ||
|
|
b2d98f7b66 | ||
|
|
c6c220091a | ||
|
|
8f503479b6 | ||
|
|
54d8378ccf | ||
|
|
0df194f9c5 | ||
|
|
ddf7eb6ad6 | ||
|
|
7db7b9b068 | ||
|
|
3d25ef48d1 | ||
|
|
4f138beb8a | ||
|
|
3d7a76bced | ||
|
|
74359ea74e | ||
|
|
a694d3f7eb | ||
|
|
58a9514b67 | ||
|
|
a75bfa9d9e | ||
|
|
7374a35b41 | ||
|
|
5ad8f6c2ce | ||
|
|
f28298afe7 | ||
|
|
56c6e8c643 | ||
|
|
1b36de4fa3 | ||
|
|
2f773bec79 | ||
|
|
d3e04c59cc | ||
|
|
5179cf59a4 | ||
|
|
fad31a47ee | ||
|
|
72c71ddbd4 | ||
|
|
a0f5ebee39 | ||
|
|
f93558324f | ||
|
|
d80e6c01c8 | ||
|
|
06f0f1c91b | ||
|
|
9b870f876a | ||
|
|
cf8b5975ac | ||
|
|
93082c3816 | ||
|
|
d66537ac34 | ||
|
|
fc4c15e52d | ||
|
|
b521604b66 | ||
|
|
9bc81a6d67 | ||
|
|
c47e2e7767 | ||
|
|
5209c0a8ca | ||
|
|
47e009267b | ||
|
|
f3edd001d7 | ||
|
|
a8cc569786 | ||
|
|
3ae49ca483 | ||
|
|
37e6497e88 | ||
|
|
b522ae9e05 | ||
|
|
62b11fcec8 | ||
|
|
51ebb57623 | ||
|
|
ff5c2ba51c | ||
|
|
6abc0e6071 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -81,10 +81,7 @@ web_modules/
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
@@ -239,7 +239,7 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
|
||||
|
||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
||||
|
||||
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
|
||||
- **[Hoppscotch CLI](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-cli)** - Command-line interface for Hoppscotch.
|
||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
|
||||
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ services:
|
||||
# The service that spins up all 3 services at once in one container
|
||||
hoppscotch-aio:
|
||||
container_name: hoppscotch-aio
|
||||
restart: unless-stopped
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
context: .
|
||||
@@ -117,7 +118,7 @@ services:
|
||||
restart: always
|
||||
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)
|
||||
- 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
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@types/node": "17.0.27",
|
||||
"cross-env": "^7.0.3",
|
||||
"http-server": "^14.1.1",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@lezer/lr": "^1.3.13"
|
||||
"@codemirror/language": "6.9.3",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lezer/lr": "^1.3.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.8.4-1",
|
||||
"version": "2023.12.6",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -28,17 +28,20 @@
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/apollo": "^12.0.9",
|
||||
"@nestjs/common": "^10.2.6",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.6",
|
||||
"@nestjs/graphql": "^12.0.9",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.2.6",
|
||||
"@nestjs/schedule": "^4.0.1",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"argon2": "^0.30.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cron": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.3",
|
||||
"fp-ts": "^2.13.1",
|
||||
@@ -56,7 +59,8 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-microsoft": "^1.0.0",
|
||||
"prisma": "^4.16.2",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.8.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.6.0"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "InfraConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"value" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedOn" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "InfraConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InfraConfig_name_key" ON "InfraConfig"("name");
|
||||
@@ -209,3 +209,12 @@ enum TeamMemberRole {
|
||||
VIEWER
|
||||
EDITOR
|
||||
}
|
||||
|
||||
model InfraConfig {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
value String?
|
||||
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AdminService } from './admin.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { MailerModule } from '../mailer/mailer.module';
|
||||
import { TeamModule } from '../team/team.module';
|
||||
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
||||
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
||||
@@ -12,19 +11,20 @@ import { TeamCollectionModule } from '../team-collection/team-collection.module'
|
||||
import { TeamRequestModule } from '../team-request/team-request.module';
|
||||
import { InfraResolver } from './infra.resolver';
|
||||
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
PubSubModule,
|
||||
UserModule,
|
||||
MailerModule,
|
||||
TeamModule,
|
||||
TeamInvitationModule,
|
||||
TeamEnvironmentsModule,
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [InfraResolver, AdminResolver, AdminService],
|
||||
exports: [AdminService],
|
||||
|
||||
@@ -27,9 +27,7 @@ import {
|
||||
} from './input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Admin)
|
||||
@@ -49,203 +47,6 @@ export class AdminResolver {
|
||||
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 */
|
||||
|
||||
@Mutation(() => InvitedUser, {
|
||||
@@ -269,8 +70,26 @@ export class AdminResolver {
|
||||
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, {
|
||||
description: 'Delete an user account from infra',
|
||||
deprecationReason: 'Use removeUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserByAdmin(
|
||||
@@ -281,12 +100,33 @@ export class AdminResolver {
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||
return invitedUser.right;
|
||||
const removedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
||||
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, {
|
||||
description: 'Make user an admin',
|
||||
deprecationReason: 'Use makeUsersAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUserAdmin(
|
||||
@@ -302,8 +142,51 @@ export class AdminResolver {
|
||||
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, {
|
||||
description: 'Remove user as admin',
|
||||
deprecationReason: 'Use demoteUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserAsAdmin(
|
||||
@@ -319,6 +202,23 @@ export class AdminResolver {
|
||||
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, {
|
||||
description:
|
||||
'Create a new team by providing the user uid to nominate as Team owner',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AdminService } from './admin.service';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
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 { TeamService } from '../team/team.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
@@ -13,9 +13,15 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
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 mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -27,6 +33,7 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||
const mockMailerService = mockDeep<MailerService>();
|
||||
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
|
||||
const adminService = new AdminService(
|
||||
mockUserService,
|
||||
@@ -39,6 +46,7 @@ const adminService = new AdminService(
|
||||
mockPrisma as any,
|
||||
mockMailerService,
|
||||
mockShortcodeService,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
const invitedUsers: InvitedUsers[] = [
|
||||
@@ -55,20 +63,87 @@ const invitedUsers: InvitedUsers[] = [
|
||||
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('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
|
||||
// @ts-ignore
|
||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||
// @ts-ignore
|
||||
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);
|
||||
});
|
||||
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([]);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -131,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', () => {
|
||||
test('should return count of all users in the organization', async () => {
|
||||
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
||||
|
||||
@@ -6,13 +6,16 @@ import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { validateEmail } from '../utils';
|
||||
import {
|
||||
ADMIN_CAN_NOT_BE_DELETED,
|
||||
DUPLICATE_EMAIL,
|
||||
EMAIL_FAILED,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
USERS_NOT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_IS_ADMIN,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
@@ -25,6 +28,9 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
|
||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -39,6 +45,7 @@ export class AdminService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -46,12 +53,30 @@ export class AdminService {
|
||||
* @param cursorID Users uid
|
||||
* @param take number of users to fetch
|
||||
* @returns an Either of array of user or error
|
||||
* @deprecated use fetchUsersV2 instead
|
||||
*/
|
||||
async fetchUsers(cursorID: string, take: number) {
|
||||
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
||||
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.
|
||||
* @param adminUID Admin's UID
|
||||
@@ -79,7 +104,7 @@ export class AdminService {
|
||||
template: 'user-invitation',
|
||||
variables: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -108,14 +133,68 @@ export class AdminService {
|
||||
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.
|
||||
* @returns an Either of array of `InvitedUser` object or error
|
||||
*/
|
||||
async fetchInvitedUsers() {
|
||||
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
||||
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
||||
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 },
|
||||
);
|
||||
|
||||
@@ -335,6 +414,7 @@ export class AdminService {
|
||||
* Remove a user account by UID
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use removeUserAccounts instead
|
||||
*/
|
||||
async removeUserAccount(userUid: string) {
|
||||
const user = await this.userService.findUserById(userUid);
|
||||
@@ -347,10 +427,73 @@ export class AdminService {
|
||||
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
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use makeUsersAdmin instead
|
||||
*/
|
||||
async makeUserAdmin(userUID: string) {
|
||||
const admin = await this.userService.makeAdmin(userUID);
|
||||
@@ -358,10 +501,22 @@ export class AdminService {
|
||||
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
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use demoteUsersByAdmin instead
|
||||
*/
|
||||
async removeUserAsAdmin(userUID: string) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
@@ -372,6 +527,26 @@ export class AdminService {
|
||||
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
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
ID,
|
||||
Mutation,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { Infra } from './infra.model';
|
||||
import { AdminService } from './admin.service';
|
||||
@@ -10,17 +17,31 @@ import { AuthUser } from 'src/types/AuthUser';
|
||||
import { throwErr } from 'src/utils';
|
||||
import * as E from 'fp-ts/Either';
|
||||
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 { Team } from 'src/team/team.model';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
|
||||
import { InfraConfig } from 'src/infra-config/infra-config.model';
|
||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||
import {
|
||||
EnableAndDisableSSOArgs,
|
||||
InfraConfigArgs,
|
||||
} from 'src/infra-config/input-args';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from 'src/infra-config/helper';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Infra)
|
||||
export class InfraResolver {
|
||||
constructor(private adminService: AdminService) {}
|
||||
constructor(
|
||||
private adminService: AdminService,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
|
||||
@Query(() => Infra, {
|
||||
description: 'Fetch details of the Infrastructure',
|
||||
@@ -59,6 +80,7 @@ export class InfraResolver {
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use allUsersV2 instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
||||
@@ -66,11 +88,33 @@ export class InfraResolver {
|
||||
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], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
})
|
||||
async invitedUsers(): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
async invitedUsers(
|
||||
@Args() args: OffsetPaginationArgs,
|
||||
): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers(args);
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -222,4 +266,97 @@ export class InfraResolver {
|
||||
userEmail,
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => [InfraConfig], {
|
||||
description: 'Retrieve configuration details for the instance',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async infraConfigs(
|
||||
@Args({
|
||||
name: 'configNames',
|
||||
type: () => [InfraConfigEnumForClient],
|
||||
description: 'Configs to fetch',
|
||||
})
|
||||
names: InfraConfigEnumForClient[],
|
||||
) {
|
||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||
return infraConfigs.right;
|
||||
}
|
||||
|
||||
@Query(() => [String], {
|
||||
description: 'Allowed Auth Provider list',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
allowedAuthProviders() {
|
||||
return this.infraConfigService.getAllowedAuthProviders();
|
||||
}
|
||||
|
||||
/* Mutations */
|
||||
|
||||
@Mutation(() => [InfraConfig], {
|
||||
description: 'Update Infra Configs',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async updateInfraConfigs(
|
||||
@Args({
|
||||
name: 'infraConfigs',
|
||||
type: () => [InfraConfigArgs],
|
||||
description: 'InfraConfigs to update',
|
||||
})
|
||||
infraConfigs: InfraConfigArgs[],
|
||||
) {
|
||||
const updatedRes = await this.infraConfigService.updateMany(infraConfigs);
|
||||
if (E.isLeft(updatedRes)) throwErr(updatedRes.left);
|
||||
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, {
|
||||
description: 'Reset Infra Configs with default values (.env)',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async resetInfraConfigs() {
|
||||
const resetRes = await this.infraConfigService.reset();
|
||||
if (E.isLeft(resetRes)) throwErr(resetRes.left);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Enable or Disable SSO for login/signup',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async enableAndDisableSSO(
|
||||
@Args({
|
||||
name: 'providerInfo',
|
||||
type: () => [EnableAndDisableSSOArgs],
|
||||
description: 'SSO provider and status',
|
||||
})
|
||||
providerInfo: EnableAndDisableSSOArgs[],
|
||||
) {
|
||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
|
||||
providerInfo,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,51 +20,71 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||
import { COOKIES_NOT_FOUND } from './errors';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AppController } from './app.controller';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||
import { loadInfraConfiguration } from './infra-config/helper';
|
||||
import { MailerModule } from './mailer/mailer.module';
|
||||
import { PosthogModule } from './posthog/posthog.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
playground: process.env.PRODUCTION !== 'true',
|
||||
autoSchemaFile: true,
|
||||
installSubscriptionHandlers: true,
|
||||
subscriptions: {
|
||||
'subscriptions-transport-ws': {
|
||||
path: '/graphql',
|
||||
onConnect: (_, websocket) => {
|
||||
try {
|
||||
const cookies = subscriptionContextCookieParser(
|
||||
websocket.upgradeReq.headers.cookie,
|
||||
);
|
||||
|
||||
return {
|
||||
headers: { ...websocket?.upgradeReq?.headers, cookies },
|
||||
};
|
||||
} catch (error) {
|
||||
throw new HttpException(COOKIES_NOT_FOUND, 400, {
|
||||
cause: new Error(COOKIES_NOT_FOUND),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
context: ({ req, res, connection }) => ({
|
||||
req,
|
||||
res,
|
||||
connection,
|
||||
}),
|
||||
driver: ApolloDriver,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [async () => loadInfraConfiguration()],
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
playground: configService.get('PRODUCTION') !== 'true',
|
||||
autoSchemaFile: true,
|
||||
installSubscriptionHandlers: true,
|
||||
subscriptions: {
|
||||
'subscriptions-transport-ws': {
|
||||
path: '/graphql',
|
||||
onConnect: (_, websocket) => {
|
||||
try {
|
||||
const cookies = subscriptionContextCookieParser(
|
||||
websocket.upgradeReq.headers.cookie,
|
||||
);
|
||||
return {
|
||||
headers: { ...websocket?.upgradeReq?.headers, cookies },
|
||||
};
|
||||
} catch (error) {
|
||||
throw new HttpException(COOKIES_NOT_FOUND, 400, {
|
||||
cause: new Error(COOKIES_NOT_FOUND),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
context: ({ req, res, connection }) => ({
|
||||
req,
|
||||
res,
|
||||
connection,
|
||||
}),
|
||||
};
|
||||
},
|
||||
]),
|
||||
}),
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => [
|
||||
{
|
||||
ttl: +configService.get('RATE_LIMIT_TTL'),
|
||||
limit: +configService.get('RATE_LIMIT_MAX'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
MailerModule.register(),
|
||||
UserModule,
|
||||
AuthModule,
|
||||
AuthModule.register(),
|
||||
AdminModule,
|
||||
UserSettingsModule,
|
||||
UserEnvironmentsModule,
|
||||
@@ -77,6 +97,9 @@ import { AppController } from './app.controller';
|
||||
TeamInvitationModule,
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
PosthogModule,
|
||||
ScheduleModule.forRoot(),
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
@@ -31,11 +30,21 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'auth', version: '1' })
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('providers')
|
||||
async getAuthProviders() {
|
||||
const providers = await this.authService.getAuthProviders();
|
||||
return { providers };
|
||||
}
|
||||
|
||||
/**
|
||||
** Route to initiate magic-link auth for a users email
|
||||
@@ -45,8 +54,14 @@ export class AuthController {
|
||||
@Body() authData: SignInMagicDto,
|
||||
@Query('origin') origin: string,
|
||||
) {
|
||||
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.EMAIL,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
const deviceIdToken = await this.authService.signInMagicLink(
|
||||
authData.email,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { MailerModule } from 'src/mailer/mailer.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@@ -12,25 +11,47 @@ import { GoogleStrategy } from './strategies/google.strategy';
|
||||
import { GithubStrategy } from './strategies/github.strategy';
|
||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||
import { AuthProvider, authProviderCheck } from './helper';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
UserModule,
|
||||
MailerModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
}),
|
||||
}),
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
RTJwtStrategy,
|
||||
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||
],
|
||||
providers: [AuthService, JwtStrategy, RTJwtStrategy],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule {
|
||||
static async register() {
|
||||
const env = await loadInfraConfiguration();
|
||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||
|
||||
const providers = [
|
||||
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
|
||||
? [GoogleStrategy]
|
||||
: []),
|
||||
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
|
||||
? [GithubStrategy]
|
||||
: []),
|
||||
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
|
||||
? [MicrosoftStrategy]
|
||||
: []),
|
||||
];
|
||||
|
||||
return {
|
||||
module: AuthModule,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,26 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as argon2 from 'argon2';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockUser = mockDeep<UserService>();
|
||||
const mockJWT = mockDeep<JwtService>();
|
||||
const mockMailer = mockDeep<MailerService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
|
||||
const authService = new AuthService(
|
||||
mockUser,
|
||||
mockPrisma,
|
||||
mockJWT,
|
||||
mockMailer,
|
||||
mockConfigService,
|
||||
mockInfraConfigService,
|
||||
);
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
@@ -91,6 +102,8 @@ describe('signInMagicLink', () => {
|
||||
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
||||
// create new entry in VerificationToken table
|
||||
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
||||
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
|
||||
mockConfigService.get.mockReturnValue('3');
|
||||
|
||||
const result = await authService.signInMagicLink(
|
||||
'dwight@dundermifflin.com',
|
||||
|
||||
@@ -28,6 +28,8 @@ import { AuthError } from 'src/types/AuthError';
|
||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||
import { VerificationToken } from '@prisma/client';
|
||||
import { Origin } from './helper';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -36,6 +38,8 @@ export class AuthService {
|
||||
private prismaService: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly configService: ConfigService,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -46,10 +50,12 @@ export class AuthService {
|
||||
*/
|
||||
private async generateMagicLinkTokens(user: AuthUser) {
|
||||
const salt = await bcrypt.genSalt(
|
||||
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
|
||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
||||
);
|
||||
const expiresOn = DateTime.now()
|
||||
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
|
||||
.plus({
|
||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toISO()
|
||||
.toString();
|
||||
|
||||
@@ -95,13 +101,13 @@ export class AuthService {
|
||||
*/
|
||||
private async generateRefreshToken(userUid: string) {
|
||||
const refreshTokenPayload: RefreshTokenPayload = {
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
sub: userUid,
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
};
|
||||
|
||||
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
||||
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
|
||||
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
|
||||
});
|
||||
|
||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||
@@ -127,9 +133,9 @@ export class AuthService {
|
||||
*/
|
||||
async generateAuthTokens(userUid: string) {
|
||||
const accessTokenPayload: AccessTokenPayload = {
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
sub: userUid,
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
};
|
||||
|
||||
const refreshToken = await this.generateRefreshToken(userUid);
|
||||
@@ -137,7 +143,7 @@ export class AuthService {
|
||||
|
||||
return E.right(<AuthTokens>{
|
||||
access_token: await this.jwtService.sign(accessTokenPayload, {
|
||||
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
|
||||
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
|
||||
}),
|
||||
refresh_token: refreshToken.right,
|
||||
});
|
||||
@@ -218,14 +224,14 @@ export class AuthService {
|
||||
let url: string;
|
||||
switch (origin) {
|
||||
case Origin.ADMIN:
|
||||
url = process.env.VITE_ADMIN_URL;
|
||||
url = this.configService.get('VITE_ADMIN_URL');
|
||||
break;
|
||||
case Origin.APP:
|
||||
url = process.env.VITE_BASE_URL;
|
||||
url = this.configService.get('VITE_BASE_URL');
|
||||
break;
|
||||
default:
|
||||
// if origin is invalid by default set URL to Hoppscotch-App
|
||||
url = process.env.VITE_BASE_URL;
|
||||
url = this.configService.get('VITE_BASE_URL');
|
||||
}
|
||||
|
||||
await this.mailerService.sendEmail(email, {
|
||||
@@ -377,4 +383,8 @@ export class AuthService {
|
||||
|
||||
return E.right(<IsAdmin>{ isAdmin: false });
|
||||
}
|
||||
|
||||
getAuthProviders() {
|
||||
return this.infraConfigService.getAllowedAuthProviders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.GITHUB,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.GOOGLE,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,20 +3,31 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftSSOGuard
|
||||
extends AuthGuard('microsoft')
|
||||
implements CanActivate
|
||||
{
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.MICROSOFT,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
throwHTTPErr({
|
||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Response } from 'express';
|
||||
import * as cookie from 'cookie';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
enum AuthTokenType {
|
||||
ACCESS_TOKEN = 'access_token',
|
||||
@@ -45,15 +46,17 @@ export const authCookieHandler = (
|
||||
redirect: boolean,
|
||||
redirectUrl: string | null,
|
||||
) => {
|
||||
const configService = new ConfigService();
|
||||
|
||||
const currentTime = DateTime.now();
|
||||
const accessTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
|
||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toMillis();
|
||||
const refreshTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
|
||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toMillis();
|
||||
|
||||
@@ -75,10 +78,12 @@ export const authCookieHandler = (
|
||||
}
|
||||
|
||||
// check to see if redirectUrl is a whitelisted url
|
||||
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
|
||||
const whitelistedOrigins = configService
|
||||
.get('WHITELISTED_ORIGINS')
|
||||
.split(',');
|
||||
if (!whitelistedOrigins.includes(redirectUrl))
|
||||
// if it is not redirect by default to REDIRECT_URL
|
||||
redirectUrl = process.env.REDIRECT_URL;
|
||||
redirectUrl = configService.get('REDIRECT_URL');
|
||||
|
||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
};
|
||||
@@ -112,13 +117,16 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
||||
* @param provider Provider we want to check the presence of
|
||||
* @returns Boolean if provider specified is present or not
|
||||
*/
|
||||
export function authProviderCheck(provider: string) {
|
||||
export function authProviderCheck(
|
||||
provider: string,
|
||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
||||
) {
|
||||
if (!provider) {
|
||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
provider.trim().toUpperCase(),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -5,18 +5,20 @@ import { AuthService } from '../auth.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||
scope: [process.env.GITHUB_SCOPE],
|
||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
||||
scope: [configService.get('GITHUB_SCOPE')],
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,18 +5,20 @@ import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { AuthService } from '../auth.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
||||
passReqToCallback: true,
|
||||
store: true,
|
||||
});
|
||||
|
||||
@@ -15,10 +15,14 @@ import {
|
||||
INVALID_ACCESS_TOKEN,
|
||||
USER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(private usersService: UserService) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -29,7 +33,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
return ATCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,21 @@ import { AuthService } from '../auth.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.MICROSOFT_CLIENT_ID,
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||
scope: [process.env.MICROSOFT_SCOPE],
|
||||
tenant: process.env.MICROSOFT_TENANT,
|
||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
||||
tenant: configService.get('MICROSOFT_TENANT'),
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ import {
|
||||
USER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
constructor(private usersService: UserService) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -28,7 +32,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
return RTCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ export const DUPLICATE_EMAIL = 'email/both_emails_cannot_be_same' as const;
|
||||
export const ONLY_ONE_ADMIN_ACCOUNT =
|
||||
'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)
|
||||
* (GqlAuthGuard)
|
||||
@@ -28,6 +36,13 @@ export const JSON_INVALID = 'json_invalid';
|
||||
*/
|
||||
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||
|
||||
/**
|
||||
* Auth Provider not specified
|
||||
* (Auth)
|
||||
*/
|
||||
export const AUTH_PROVIDER_NOT_CONFIGURED =
|
||||
'auth/provider_not_configured_correctly';
|
||||
|
||||
/**
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||
*/
|
||||
@@ -92,6 +107,13 @@ export const USER_IS_OWNER = 'user/is_owner' 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
|
||||
* (TeamsService)
|
||||
@@ -644,3 +666,54 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
|
||||
*/
|
||||
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
||||
'shortcode/properties_not_found' as const;
|
||||
|
||||
/**
|
||||
* Infra Config not found
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_NOT_FOUND = 'infra_config/not_found' as const;
|
||||
|
||||
/**
|
||||
* Infra Config update failed
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_UPDATE_FAILED = 'infra_config/update_failed' as const;
|
||||
|
||||
/**
|
||||
* Infra Config not listed for onModuleInit creation
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_NOT_LISTED =
|
||||
'infra_config/properly_not_listed' as const;
|
||||
|
||||
/**
|
||||
* Infra Config reset failed
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
|
||||
|
||||
/**
|
||||
* Infra Config invalid input for Config variable
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
||||
|
||||
/**
|
||||
* Infra Config service (auth provider/mailer/audit logs) not configured
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||
'infra_config/service_not_configured' as const;
|
||||
|
||||
/**
|
||||
* Error message for when the database table does not exist
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* PostHog client is not initialized
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||
|
||||
116
packages/hoppscotch-backend/src/infra-config/helper.ts
Normal file
116
packages/hoppscotch-backend/src/infra-config/helper.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export enum ServiceStatus {
|
||||
ENABLE = 'ENABLE',
|
||||
DISABLE = 'DISABLE',
|
||||
}
|
||||
|
||||
const AuthProviderConfigurations = {
|
||||
[AuthProvider.GOOGLE]: [
|
||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||
],
|
||||
[AuthProvider.GITHUB]: [
|
||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||
],
|
||||
[AuthProvider.MICROSOFT]: [
|
||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||
],
|
||||
[AuthProvider.EMAIL]: [
|
||||
InfraConfigEnum.MAILER_SMTP_URL,
|
||||
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Load environment variables from the database and set them in the process
|
||||
*
|
||||
* @Description Fetch the 'infra_config' table from the database and return it as an object
|
||||
* (ConfigModule will set the environment variables in the process)
|
||||
*/
|
||||
export async function loadInfraConfiguration() {
|
||||
try {
|
||||
const prisma = new PrismaService();
|
||||
|
||||
const infraConfigs = await prisma.infraConfig.findMany();
|
||||
|
||||
let environmentObject: Record<string, any> = {};
|
||||
infraConfigs.forEach((infraConfig) => {
|
||||
environmentObject[infraConfig.name] = infraConfig.value;
|
||||
});
|
||||
|
||||
return { INFRA: environmentObject };
|
||||
} catch (error) {
|
||||
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
|
||||
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
|
||||
return { INFRA: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the app after 5 seconds
|
||||
* (Docker will re-start the app)
|
||||
*/
|
||||
export function stopApp() {
|
||||
console.log('Stopping app in 5 seconds...');
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('Stopping app now...');
|
||||
process.kill(process.pid, 'SIGTERM');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured SSO providers
|
||||
* @returns Array of configured SSO providers
|
||||
*/
|
||||
export function getConfiguredSSOProviders() {
|
||||
const allowedAuthProviders: string[] =
|
||||
process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',');
|
||||
let configuredAuthProviders: string[] = [];
|
||||
|
||||
const addProviderIfConfigured = (provider) => {
|
||||
const configParameters: string[] = AuthProviderConfigurations[provider];
|
||||
|
||||
const isConfigured = configParameters.every((configParameter) => {
|
||||
return process.env[configParameter];
|
||||
});
|
||||
|
||||
if (isConfigured) configuredAuthProviders.push(provider);
|
||||
};
|
||||
|
||||
allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider));
|
||||
|
||||
if (configuredAuthProviders.length === 0) {
|
||||
throwErr(AUTH_PROVIDER_NOT_CONFIGURED);
|
||||
} else if (allowedAuthProviders.length !== configuredAuthProviders.length) {
|
||||
const unConfiguredAuthProviders = allowedAuthProviders.filter(
|
||||
(provider) => {
|
||||
return !configuredAuthProviders.includes(provider);
|
||||
},
|
||||
);
|
||||
console.log(
|
||||
`${unConfiguredAuthProviders.join(
|
||||
',',
|
||||
)} SSO auth provider(s) are not configured properly. Do configure them from Admin Dashboard.`,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from './helper';
|
||||
|
||||
@ObjectType()
|
||||
export class InfraConfig {
|
||||
@Field({
|
||||
description: 'Infra Config Name',
|
||||
})
|
||||
name: InfraConfigEnumForClient;
|
||||
|
||||
@Field({
|
||||
description: 'Infra Config Value',
|
||||
})
|
||||
value: string;
|
||||
}
|
||||
|
||||
registerEnumType(InfraConfigEnumForClient, {
|
||||
name: 'InfraConfigEnum',
|
||||
});
|
||||
|
||||
registerEnumType(AuthProvider, {
|
||||
name: 'AuthProvider',
|
||||
});
|
||||
|
||||
registerEnumType(ServiceStatus, {
|
||||
name: 'ServiceStatus',
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InfraConfigService } from './infra-config.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { SiteController } from './infra-config.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [InfraConfigService],
|
||||
exports: [InfraConfigService],
|
||||
controllers: [SiteController],
|
||||
})
|
||||
export class InfraConfigModule {}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfigService } from './infra-config.service';
|
||||
import {
|
||||
InfraConfigEnum,
|
||||
InfraConfigEnumForClient,
|
||||
} from 'src/types/InfraConfig';
|
||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as helper from './helper';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const infraConfigService = new InfraConfigService(
|
||||
mockPrisma,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(mockPrisma);
|
||||
});
|
||||
|
||||
describe('InfraConfigService', () => {
|
||||
describe('update', () => {
|
||||
it('should update the infra config', async () => {
|
||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
|
||||
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||
id: '',
|
||||
name,
|
||||
value,
|
||||
active: true,
|
||||
createdOn: new Date(),
|
||||
updatedOn: new Date(),
|
||||
});
|
||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||
|
||||
const result = await infraConfigService.update(name, value);
|
||||
expect(result).toEqualRight({ name, value });
|
||||
});
|
||||
|
||||
it('should pass correct params to prisma update', async () => {
|
||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
|
||||
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||
|
||||
await infraConfigService.update(name, value);
|
||||
|
||||
expect(mockPrisma.infraConfig.update).toHaveBeenCalledWith({
|
||||
where: { name },
|
||||
data: { value },
|
||||
});
|
||||
expect(mockPrisma.infraConfig.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw an error if the infra config update failed', async () => {
|
||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
|
||||
mockPrisma.infraConfig.update.mockRejectedValueOnce('null');
|
||||
|
||||
const result = await infraConfigService.update(name, value);
|
||||
expect(result).toEqualLeft(INFRA_CONFIG_UPDATE_FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should get the infra config', async () => {
|
||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||
const value = 'true';
|
||||
|
||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||
id: '',
|
||||
name,
|
||||
value,
|
||||
active: true,
|
||||
createdOn: new Date(),
|
||||
updatedOn: new Date(),
|
||||
});
|
||||
const result = await infraConfigService.get(name);
|
||||
expect(result).toEqualRight({ name, value });
|
||||
});
|
||||
|
||||
it('should pass correct params to prisma findUnique', async () => {
|
||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||
|
||||
await infraConfigService.get(name);
|
||||
|
||||
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledWith({
|
||||
where: { name },
|
||||
});
|
||||
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw an error if the infra config does not exist', async () => {
|
||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
||||
|
||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
||||
|
||||
const result = await infraConfigService.get(name);
|
||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,429 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { InfraConfig } from './infra-config.model';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import {
|
||||
InfraConfigEnum,
|
||||
InfraConfigEnumForClient,
|
||||
} from 'src/types/InfraConfig';
|
||||
import {
|
||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
DATABASE_TABLE_NOT_EXIST,
|
||||
INFRA_CONFIG_INVALID_INPUT,
|
||||
INFRA_CONFIG_NOT_FOUND,
|
||||
INFRA_CONFIG_NOT_LISTED,
|
||||
INFRA_CONFIG_RESET_FAILED,
|
||||
INFRA_CONFIG_UPDATE_FAILED,
|
||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||
} from 'src/errors';
|
||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ServiceStatus,
|
||||
generateAnalyticsUserId,
|
||||
getConfiguredSSOProviders,
|
||||
stopApp,
|
||||
} from './helper';
|
||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@Injectable()
|
||||
export class InfraConfigService implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.initializeInfraConfigTable();
|
||||
}
|
||||
|
||||
async getDefaultInfraConfigs(): Promise<
|
||||
{ name: InfraConfigEnum; value: string }[]
|
||||
> {
|
||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||
value: process.env.MAILER_SMTP_URL,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||
value: process.env.MAILER_ADDRESS_FROM,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||
value: process.env.GOOGLE_CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||
value: process.env.GITHUB_CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||
value: process.env.GITHUB_CLIENT_SECRET,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||
value: process.env.MICROSOFT_CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
value: getConfiguredSSOProviders(),
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the 'infra_config' table with values from .env
|
||||
* @description This function create rows 'infra_config' in very first time (only once)
|
||||
*/
|
||||
async initializeInfraConfigTable() {
|
||||
try {
|
||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
||||
const enumValues = Object.values(InfraConfigEnum);
|
||||
|
||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
||||
|
||||
// Check if all the 'names' are listed in the default values
|
||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
||||
throw new Error(INFRA_CONFIG_NOT_LISTED);
|
||||
}
|
||||
|
||||
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
||||
const propsToInsert = infraConfigDefaultObjs.filter(
|
||||
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
||||
);
|
||||
|
||||
if (propsToInsert.length > 0) {
|
||||
await this.prisma.infraConfig.createMany({ data: propsToInsert });
|
||||
stopApp();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'P1001') {
|
||||
// Prisma error code for 'Can't reach at database server'
|
||||
// We're not throwing error here because we want to allow the app to run 'pnpm install'
|
||||
} else if (error.code === 'P2021') {
|
||||
// Prisma error code for 'Table does not exist'
|
||||
throwErr(DATABASE_TABLE_NOT_EXIST);
|
||||
} else {
|
||||
throwErr(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typecast a database InfraConfig to a InfraConfig model
|
||||
* @param dbInfraConfig database InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
cast(dbInfraConfig: DBInfraConfig) {
|
||||
return <InfraConfig>{
|
||||
name: dbInfraConfig.name,
|
||||
value: dbInfraConfig.value ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the InfraConfigs as map
|
||||
* @returns InfraConfig map
|
||||
*/
|
||||
async getInfraConfigsMap() {
|
||||
const infraConfigs = await this.prisma.infraConfig.findMany();
|
||||
const infraConfigMap: Record<string, string> = {};
|
||||
infraConfigs.forEach((config) => {
|
||||
infraConfigMap[config.name] = config.value;
|
||||
});
|
||||
return infraConfigMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update InfraConfig by name
|
||||
* @param name Name of the InfraConfig
|
||||
* @param value Value of the InfraConfig
|
||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async update(
|
||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
||||
value: string,
|
||||
restartEnabled = false,
|
||||
) {
|
||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||
|
||||
try {
|
||||
const infraConfig = await this.prisma.infraConfig.update({
|
||||
where: { name },
|
||||
data: { value },
|
||||
});
|
||||
|
||||
if (restartEnabled) stopApp();
|
||||
|
||||
return E.right(this.cast(infraConfig));
|
||||
} catch (e) {
|
||||
return E.left(INFRA_CONFIG_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update InfraConfigs by name
|
||||
* @param infraConfigs InfraConfigs to update
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
||||
const isValidate = this.validateEnvValues(infraConfigs);
|
||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
for (let i = 0; i < infraConfigs.length; i++) {
|
||||
await tx.infraConfig.update({
|
||||
where: { name: infraConfigs[i].name },
|
||||
data: { value: infraConfigs[i].value },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
stopApp();
|
||||
|
||||
return E.right(infraConfigs);
|
||||
} catch (e) {
|
||||
return E.left(INFRA_CONFIG_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured or not
|
||||
* @param service Service can be Auth Provider, Mailer, Audit Log etc.
|
||||
* @param configMap Map of all the infra configs
|
||||
* @returns Either true or false
|
||||
*/
|
||||
isServiceConfigured(
|
||||
service: AuthProvider,
|
||||
configMap: Record<string, string>,
|
||||
) {
|
||||
switch (service) {
|
||||
case AuthProvider.GOOGLE:
|
||||
return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET;
|
||||
case AuthProvider.GITHUB:
|
||||
return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET;
|
||||
case AuthProvider.MICROSOFT:
|
||||
return (
|
||||
configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET
|
||||
);
|
||||
case AuthProvider.EMAIL:
|
||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param provider Auth Provider to enable or disable
|
||||
* @param status Status to enable or disable
|
||||
* @returns Either true or an error
|
||||
*/
|
||||
async enableAndDisableSSO(providerInfo: EnableAndDisableSSOArgs[]) {
|
||||
const allowedAuthProviders = this.configService
|
||||
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
||||
.split(',');
|
||||
|
||||
let updatedAuthProviders = allowedAuthProviders;
|
||||
|
||||
const infraConfigMap = await this.getInfraConfigsMap();
|
||||
|
||||
providerInfo.forEach(({ provider, status }) => {
|
||||
if (status === ServiceStatus.ENABLE) {
|
||||
const isConfigured = this.isServiceConfigured(provider, infraConfigMap);
|
||||
if (!isConfigured) {
|
||||
throwErr(INFRA_CONFIG_SERVICE_NOT_CONFIGURED);
|
||||
}
|
||||
updatedAuthProviders.push(provider);
|
||||
} else if (status === ServiceStatus.DISABLE) {
|
||||
updatedAuthProviders = updatedAuthProviders.filter(
|
||||
(p) => p !== provider,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
updatedAuthProviders = [...new Set(updatedAuthProviders)];
|
||||
|
||||
if (updatedAuthProviders.length === 0) {
|
||||
return E.left(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const isUpdated = await this.update(
|
||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
updatedAuthProviders.join(','),
|
||||
true,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get InfraConfig by name
|
||||
* @param name Name of the InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async get(name: InfraConfigEnumForClient) {
|
||||
try {
|
||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||
where: { name },
|
||||
});
|
||||
|
||||
return E.right(this.cast(infraConfig));
|
||||
} catch (e) {
|
||||
return E.left(INFRA_CONFIG_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get InfraConfigs by names
|
||||
* @param names Names of the InfraConfigs
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async getMany(names: InfraConfigEnumForClient[]) {
|
||||
try {
|
||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||
where: { name: { in: names } },
|
||||
});
|
||||
|
||||
return E.right(infraConfigs.map((p) => this.cast(p)));
|
||||
} catch (e) {
|
||||
return E.left(INFRA_CONFIG_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed auth providers for login/signup
|
||||
* @returns string[]
|
||||
*/
|
||||
getAllowedAuthProviders() {
|
||||
return this.configService
|
||||
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
||||
.split(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the InfraConfigs to their default values (from .env)
|
||||
*/
|
||||
async reset() {
|
||||
try {
|
||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
||||
|
||||
await this.prisma.infraConfig.deleteMany({
|
||||
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({
|
||||
data: [
|
||||
...updatedInfraConfigDefaultObjs,
|
||||
{
|
||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
stopApp();
|
||||
|
||||
return E.right(true);
|
||||
} catch (e) {
|
||||
return E.left(INFRA_CONFIG_RESET_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the values of the InfraConfigs
|
||||
*/
|
||||
validateEnvValues(
|
||||
infraConfigs: {
|
||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
||||
value: string;
|
||||
}[],
|
||||
) {
|
||||
for (let i = 0; i < infraConfigs.length; i++) {
|
||||
switch (infraConfigs[i].name) {
|
||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
|
||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
30
packages/hoppscotch-backend/src/infra-config/input-args.ts
Normal file
30
packages/hoppscotch-backend/src/infra-config/input-args.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from './helper';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@InputType()
|
||||
export class InfraConfigArgs {
|
||||
@Field(() => InfraConfigEnumForClient, {
|
||||
description: 'Infra Config Name',
|
||||
})
|
||||
name: InfraConfigEnumForClient;
|
||||
|
||||
@Field({
|
||||
description: 'Infra Config Value',
|
||||
})
|
||||
value: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class EnableAndDisableSSOArgs {
|
||||
@Field(() => AuthProvider, {
|
||||
description: 'Auth Provider',
|
||||
})
|
||||
provider: AuthProvider;
|
||||
|
||||
@Field(() => ServiceStatus, {
|
||||
description: 'Auth Provider Status',
|
||||
})
|
||||
status: ServiceStatus;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { MailerService } from './mailer.service';
|
||||
@@ -7,24 +7,42 @@ import {
|
||||
MAILER_FROM_ADDRESS_UNDEFINED,
|
||||
MAILER_SMTP_URL_UNDEFINED,
|
||||
} from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
NestMailerModule.forRoot({
|
||||
transport:
|
||||
process.env.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
||||
defaults: {
|
||||
from:
|
||||
process.env.MAILER_ADDRESS_FROM ??
|
||||
throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
||||
},
|
||||
template: {
|
||||
dir: __dirname + '/templates',
|
||||
adapter: new HandlebarsAdapter(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
imports: [],
|
||||
providers: [MailerService],
|
||||
exports: [MailerService],
|
||||
})
|
||||
export class MailerModule {}
|
||||
export class MailerModule {
|
||||
static async register() {
|
||||
const env = await loadInfraConfiguration();
|
||||
|
||||
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
|
||||
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
|
||||
|
||||
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
|
||||
const config = new ConfigService();
|
||||
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
|
||||
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
|
||||
}
|
||||
|
||||
return {
|
||||
module: MailerModule,
|
||||
imports: [
|
||||
NestMailerModule.forRoot({
|
||||
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
||||
defaults: {
|
||||
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
||||
},
|
||||
template: {
|
||||
dir: __dirname + '/templates',
|
||||
adapter: new HandlebarsAdapter(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MailerService {
|
||||
): string {
|
||||
switch (mailDesc.template) {
|
||||
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':
|
||||
return 'Sign in to Hoppscotch';
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
@@ -458,7 +464,7 @@
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<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 -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
@@ -484,7 +490,7 @@
|
||||
Welcome aboard, <br />
|
||||
Your friends at Hoppscotch
|
||||
</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 -->
|
||||
<table class="body-sub">
|
||||
<tr>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
@@ -22,19 +22,25 @@
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
@@ -47,13 +53,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -61,7 +67,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -69,7 +75,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -77,12 +83,12 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
@@ -91,25 +97,25 @@
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
@@ -124,7 +130,7 @@
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
@@ -132,7 +138,7 @@
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
@@ -140,7 +146,7 @@
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
@@ -148,21 +154,21 @@
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -171,31 +177,31 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -206,33 +212,33 @@
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -241,7 +247,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -250,50 +256,50 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -303,7 +309,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -313,16 +319,16 @@
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
@@ -331,7 +337,7 @@
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -340,7 +346,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -350,7 +356,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -360,11 +366,11 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
@@ -374,25 +380,25 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
|
||||
@@ -6,18 +6,24 @@ import { VersioningType } from '@nestjs/common';
|
||||
import * as session from 'express-session';
|
||||
import { emitGQLSchemaFile } from './gql-schema';
|
||||
import { checkEnvironmentAuthProvider } from './utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
async function bootstrap() {
|
||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||
console.log(`Port: ${process.env.PORT}`);
|
||||
|
||||
checkEnvironmentAuthProvider();
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
|
||||
console.log(`Port: ${configService.get('PORT')}`);
|
||||
|
||||
checkEnvironmentAuthProvider(
|
||||
configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') ??
|
||||
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
);
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET,
|
||||
secret: configService.get('SESSION_SECRET'),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -28,18 +34,18 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
if (process.env.PRODUCTION === 'false') {
|
||||
if (configService.get('PRODUCTION') === 'false') {
|
||||
console.log('Enabling CORS with development settings');
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
credentials: true,
|
||||
});
|
||||
} else {
|
||||
console.log('Enabling CORS with production settings');
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
credentials: true,
|
||||
});
|
||||
}
|
||||
@@ -47,7 +53,13 @@ async function bootstrap() {
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
app.use(cookieParser());
|
||||
await app.listen(process.env.PORT || 3170);
|
||||
await app.listen(configService.get('PORT') || 3170);
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.info('SIGTERM signal received');
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.GENERATE_GQL_SCHEMA) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -504,20 +504,24 @@ describe('ShortcodeService', () => {
|
||||
);
|
||||
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||
{
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
id: shortcodesWithUserEmail[0].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[0].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[0].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[0].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||
createdOn: shortcodes[1].createdOn,
|
||||
id: shortcodesWithUserEmail[1].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[1].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[1].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[1].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailerModule } from 'src/mailer/mailer.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||
import { TeamModule } from 'src/team/team.module';
|
||||
@@ -12,7 +11,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
|
||||
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
|
||||
providers: [
|
||||
TeamInvitationService,
|
||||
TeamInvitationResolver,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { validateEmail } from '../utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class TeamInvitationService {
|
||||
@@ -28,8 +29,8 @@ export class TeamInvitationService {
|
||||
private readonly userService: UserService,
|
||||
private readonly teamService: TeamService,
|
||||
private readonly mailerService: MailerService,
|
||||
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -150,7 +151,9 @@ export class TeamInvitationService {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
|
||||
dbInvitation.id
|
||||
}`,
|
||||
invite_team_name: team.name,
|
||||
},
|
||||
});
|
||||
|
||||
36
packages/hoppscotch-backend/src/types/InfraConfig.ts
Normal file
36
packages/hoppscotch-backend/src/types/InfraConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export enum InfraConfigEnum {
|
||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
||||
|
||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||
|
||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||
|
||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||
|
||||
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 {
|
||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
||||
|
||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||
|
||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||
|
||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@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, {
|
||||
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 { PrismaService } from 'src/prisma/prisma.service';
|
||||
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', () => {
|
||||
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
||||
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', () => {
|
||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||
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', () => {
|
||||
test('should return a list of admin users', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
||||
@@ -556,4 +654,17 @@ describe('UserService', () => {
|
||||
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 { pipe, constVoid } from 'fp-ts/function';
|
||||
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 { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
||||
import { UserDataHandler } from './user.data.handler';
|
||||
import { User as DbUser } from '@prisma/client';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
|
||||
@Injectable()
|
||||
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
|
||||
*
|
||||
@@ -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
|
||||
* @param sessionData string of the session
|
||||
@@ -286,6 +325,7 @@ export class UserService {
|
||||
* @param cursorID string of userUID or null
|
||||
* @param take number of users to query
|
||||
* @returns an array of `User` object
|
||||
* @deprecated use fetchAllUsersV2 instead
|
||||
*/
|
||||
async fetchAllUsers(cursorID: string, take: number) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
@@ -296,6 +336,43 @@ export class UserService {
|
||||
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
|
||||
* @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
|
||||
* @returns an array of admin users
|
||||
@@ -444,4 +538,22 @@ export class UserService {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,48 @@ export const validateEmail = (email: string) => {
|
||||
).test(email);
|
||||
};
|
||||
|
||||
// Regular expressions for supported address object formats by nodemailer
|
||||
// check out for more info https://nodemailer.com/message/addresses
|
||||
const emailRegex1 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
const emailRegex2 =
|
||||
/^[\w\s]* <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
|
||||
const emailRegex3 =
|
||||
/^"[\w\s]+" <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
|
||||
|
||||
/**
|
||||
* Checks to see if the SMTP email is valid or not
|
||||
* @param email
|
||||
* @returns A Boolean depending on the format of the email
|
||||
*/
|
||||
export const validateSMTPEmail = (email: string) => {
|
||||
// Check if the input matches any of the formats
|
||||
return (
|
||||
emailRegex1.test(email) ||
|
||||
emailRegex2.test(email) ||
|
||||
emailRegex3.test(email)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if the URL is valid or not
|
||||
* @param url The URL to validate
|
||||
* @returns boolean
|
||||
*/
|
||||
export const validateSMTPUrl = (url: string) => {
|
||||
// Possible valid formats
|
||||
// smtp(s)://mail.example.com
|
||||
// smtp(s)://user:pass@mail.example.com
|
||||
// smtp(s)://mail.example.com:587
|
||||
// smtp(s)://user:pass@mail.example.com:587
|
||||
|
||||
if (!url || url.length === 0) return false;
|
||||
|
||||
const regex =
|
||||
/^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
|
||||
if (regex.test(url)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* String to JSON parser
|
||||
* @param {str} str The string to parse
|
||||
@@ -161,21 +203,23 @@ export function isValidLength(title: string, length: number) {
|
||||
|
||||
/**
|
||||
* This function is called by bootstrap() in main.ts
|
||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* If not, it throws an error.
|
||||
*/
|
||||
export function checkEnvironmentAuthProvider() {
|
||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||
export function checkEnvironmentAuthProvider(
|
||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
||||
) {
|
||||
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
|
||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||
',',
|
||||
).map((provider) => provider.toLocaleUpperCase());
|
||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||
(provider) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||
(provider: string) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"hopp": "bin/hopp"
|
||||
"hopp": "bin/hopp.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -39,27 +40,31 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@swc/core": "^1.3.92",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/qs": "^6.9.8",
|
||||
"axios": "^0.21.4",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"fp-ts": "^2.16.1",
|
||||
"io-ts": "^2.2.20",
|
||||
"@swc/core": "^1.3.105",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"fp-ts": "^2.16.2",
|
||||
"jest": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier": "^3.2.4",
|
||||
"qs": "^6.11.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"verzod": "^0.2.2",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,274 @@
|
||||
import { ExecException } from "child_process";
|
||||
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const cmd = `node ./bin/hopp test`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
describe("Test `hopp test <file>` command:", () => {
|
||||
describe("Argument parsing", () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const cmd = `node ./bin/hopp test notfound.json`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
})
|
||||
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
describe("Supplied collection export file validations", () => {
|
||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const cmd = `node ./bin/hopp invalid-arg`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
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-coll.txt", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Some errors occured (exit code 1).", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("fails.json")}`;
|
||||
const { error } = await execAsync(cmd);
|
||||
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("passes.json")}`;
|
||||
const { error } = await execAsync(cmd);
|
||||
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"collection-level-headers-auth-coll.json", "collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
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:", () => {
|
||||
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
describe("Supplied environment export file validations", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
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("Successfully resolves values from the supplied environment export file", 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} --env ${ENV_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
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:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("ENV file not JSON type.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("ENV file not found.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
test("Successfully performs delayed request execution for a valid delay value", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay 1`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
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 cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
const { error, stdout } = await execAsync(cmd);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
|
||||
test("No value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Invalid value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Valid value passed to delay flag.", async () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 1`;
|
||||
const { error } = await execAsync(cmd);
|
||||
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,221 @@
|
||||
[
|
||||
{
|
||||
"v": 1,
|
||||
"name": "CollectionA",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"name": "FolderA",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"name": "FolderB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"name": "FolderC",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestD",
|
||||
"params": [],
|
||||
"headers": [
|
||||
{
|
||||
"active": true,
|
||||
"key": "X-Test-Header",
|
||||
"value": "Overriden at RequestD"
|
||||
}
|
||||
],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "basic",
|
||||
"authActive": true,
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Overrides auth and headers set at the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Overriden at RequestD\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestC",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Overriden at FolderB\");\n pw.expect(pw.response.body.headers[\"key\"]).toBe(\"test-key\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "api-key",
|
||||
"authActive": true,
|
||||
"addTo": "Headers",
|
||||
"key": "key",
|
||||
"value": "test-key"
|
||||
},
|
||||
"headers": [
|
||||
{
|
||||
"active": true,
|
||||
"key": "X-Test-Header",
|
||||
"value": "Overriden at FolderB"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Correctly inherits auth and headers from the root collection\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"active": true,
|
||||
"key": "X-Test-Header",
|
||||
"value": "Set at root collection"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "bearer",
|
||||
"authActive": true,
|
||||
"token": "BearerToken"
|
||||
}
|
||||
},
|
||||
{
|
||||
"v": 1,
|
||||
"name": "CollectionB",
|
||||
"folders": [
|
||||
{
|
||||
"v": 1,
|
||||
"name": "FolderA",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestB",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "RequestA",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Correctly inherits auth and headers from the root collection\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"id": "clpttpdq00003qp16kut6doqv"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"active": true,
|
||||
"key": "X-Test-Header",
|
||||
"value": "Set at root collection"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "bearer",
|
||||
"authActive": true,
|
||||
"token": "BearerToken"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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,30 @@
|
||||
{
|
||||
"v": 2,
|
||||
"name": "Test environment variables in request body",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"name": "test-request",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"method": "POST",
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"v": 0,
|
||||
"name": "Response body sample",
|
||||
"variables": [
|
||||
{
|
||||
"key": "firstName",
|
||||
"value": "John"
|
||||
},
|
||||
{
|
||||
"key": "lastName",
|
||||
"value": "Doe"
|
||||
},
|
||||
{
|
||||
"key": "id",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"key": "fullName",
|
||||
"value": "<<firstName>> <<lastName>>"
|
||||
},
|
||||
{
|
||||
"key": "recursiveVarX",
|
||||
"value": "<<recursiveVarY>>"
|
||||
},
|
||||
{
|
||||
"key": "recursiveVarY",
|
||||
"value": "<<salutation>>"
|
||||
},
|
||||
{
|
||||
"key": "salutation",
|
||||
"value": "Hello"
|
||||
},
|
||||
{
|
||||
"key": "greetText",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import { exec } from "child_process";
|
||||
import { resolve } from "path";
|
||||
|
||||
import { ExecResponse } from "./types";
|
||||
|
||||
export const execAsync = (command: string): Promise<ExecResponse> =>
|
||||
new Promise((resolve) =>
|
||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
|
||||
{
|
||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||
const command = `node ${CLI_PATH} ${args}`
|
||||
|
||||
return new Promise((resolve) =>
|
||||
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
}
|
||||
|
||||
export const trimAnsi = (target: string) => {
|
||||
const ansiRegex =
|
||||
@@ -15,12 +22,15 @@ export const trimAnsi = (target: string) => {
|
||||
|
||||
export const getErrorCode = (out: string) => {
|
||||
const ansiTrimmedStr = trimAnsi(out);
|
||||
|
||||
return ansiTrimmedStr.split(" ")[0];
|
||||
};
|
||||
|
||||
export const getTestJsonFilePath = (file: string) => {
|
||||
const filePath = `${process.cwd()}/src/__tests__/samples/${file}`;
|
||||
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
||||
const kindDir = {
|
||||
collection: "collections",
|
||||
environment: "environments",
|
||||
}[kind];
|
||||
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import chalk from "chalk";
|
||||
import { program } from "commander";
|
||||
import { Command } from "commander";
|
||||
import * as E from "fp-ts/Either";
|
||||
import { version } from "../package.json";
|
||||
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"
|
||||
)}`;
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program
|
||||
.name("hopp")
|
||||
.version(version, "-v, --ver", "see the current version of hopp-cli")
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface RequestStack {
|
||||
*/
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
supported: boolean;
|
||||
displayUrl?: string
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
effectiveFinalURL: string;
|
||||
effectiveFinalDisplayURL?: string;
|
||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||
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 {
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
HoppEnvPair,
|
||||
HoppEnvs
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs 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
|
||||
* @returns For successful parsing we get HoppEnvs object
|
||||
*/
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.safeParse(contents);
|
||||
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
||||
|
||||
// CLI doesnt support bulk environments export.
|
||||
// Hence we check for this case and throw an error if it matches the format.
|
||||
// The legacy key-value pair format that is still supported
|
||||
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) {
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
// Checks if the environment file is of the correct format.
|
||||
// If it doesnt match either of them, we throw an error.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
// Checks if the environment file is of the correct format
|
||||
// If it doesnt match either of them, we throw an error
|
||||
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
@@ -36,9 +44,8 @@ export async function parseEnvsData(path: string) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
|
||||
envPairs.push({ key, value });
|
||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||
}
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { HoppCollection } from "@hoppscotch/data";
|
||||
import { HoppEnvs } from "./request";
|
||||
|
||||
export type CollectionRunnerParam = {
|
||||
collections: HoppCollection<HoppRESTRequest>[];
|
||||
collections: HoppCollection[];
|
||||
envs: HoppEnvs;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
@@ -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 { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
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());
|
||||
|
||||
// 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 = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
@@ -33,7 +20,7 @@ export type HoppEnvs = {
|
||||
|
||||
export type CollectionStack = {
|
||||
path: string;
|
||||
collection: HoppCollection<HoppRESTRequest>;
|
||||
collection: HoppCollection;
|
||||
};
|
||||
|
||||
export type RequestReport = {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
isHoppRESTRequest,
|
||||
} from "@hoppscotch/data";
|
||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { CommanderError } from "commander";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
@@ -24,9 +20,7 @@ export const hasProperty = <P extends PropertyKey>(
|
||||
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
|
||||
* False, otherwise.
|
||||
*/
|
||||
export const isRESTCollection = (
|
||||
param: unknown
|
||||
): param is HoppCollection<HoppRESTRequest> => {
|
||||
export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
||||
if (!!param && typeof param === "object") {
|
||||
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
||||
return false;
|
||||
@@ -62,7 +56,6 @@ export const isRESTCollection = (
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if given error data is of type HoppCLIError, based on existence
|
||||
* of code property.
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import chalk from "chalk";
|
||||
import { log } from "console";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { bold } from "chalk";
|
||||
import { log } from "console";
|
||||
import round from "lodash/round";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { round } from "lodash-es";
|
||||
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
import {
|
||||
HoppEnvs,
|
||||
CollectionStack,
|
||||
RequestReport,
|
||||
HoppEnvs,
|
||||
ProcessRequestParams,
|
||||
RequestReport,
|
||||
} from "../types/request";
|
||||
import {
|
||||
getRequestMetrics,
|
||||
preProcessRequest,
|
||||
processRequest,
|
||||
} from "./request";
|
||||
import { exceptionColors } from "./getters";
|
||||
PreRequestMetrics,
|
||||
RequestMetrics,
|
||||
TestMetrics,
|
||||
} from "../types/response";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
import {
|
||||
printErrorsReport,
|
||||
printFailedTestsReport,
|
||||
@@ -23,15 +25,14 @@ import {
|
||||
printRequestsMetrics,
|
||||
printTestsMetrics,
|
||||
} from "./display";
|
||||
import {
|
||||
PreRequestMetrics,
|
||||
RequestMetrics,
|
||||
TestMetrics,
|
||||
} from "../types/response";
|
||||
import { getTestMetrics } from "./test";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
import { exceptionColors } from "./getters";
|
||||
import { getPreRequestMetrics } from "./pre-request";
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
import {
|
||||
getRequestMetrics,
|
||||
preProcessRequest,
|
||||
processRequest,
|
||||
} from "./request";
|
||||
import { getTestMetrics } from "./test";
|
||||
|
||||
const { WARN, FAIL } = exceptionColors;
|
||||
|
||||
@@ -41,23 +42,23 @@ const { WARN, FAIL } = exceptionColors;
|
||||
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
|
||||
* @returns List of report for each processed request.
|
||||
*/
|
||||
export const collectionsRunner =
|
||||
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
|
||||
{
|
||||
const envs: HoppEnvs = param.envs;
|
||||
const delay = param.delay ?? 0;
|
||||
const requestsReport: RequestReport[] = [];
|
||||
const collectionStack: CollectionStack[] = getCollectionStack(
|
||||
param.collections
|
||||
);
|
||||
export const collectionsRunner = async (
|
||||
param: CollectionRunnerParam
|
||||
): Promise<RequestReport[]> => {
|
||||
const envs: HoppEnvs = param.envs;
|
||||
const delay = param.delay ?? 0;
|
||||
const requestsReport: RequestReport[] = [];
|
||||
const collectionStack: CollectionStack[] = getCollectionStack(
|
||||
param.collections
|
||||
);
|
||||
|
||||
while (collectionStack.length) {
|
||||
// Pop out top-most collection from stack to be processed.
|
||||
const { collection, path } = <CollectionStack>collectionStack.pop();
|
||||
while (collectionStack.length) {
|
||||
// Pop out top-most collection from stack to be processed.
|
||||
const { collection, path } = <CollectionStack>collectionStack.pop();
|
||||
|
||||
// Processing each request in collection
|
||||
for (const request of collection.requests) {
|
||||
const _request = preProcessRequest(request);
|
||||
const _request = preProcessRequest(request as HoppRESTRequest, collection);
|
||||
const requestPath = `${path}/${_request.name}`;
|
||||
const processRequestParams: ProcessRequestParams = {
|
||||
path: requestPath,
|
||||
@@ -67,15 +68,15 @@ export const collectionsRunner =
|
||||
};
|
||||
|
||||
// Request processing initiated message.
|
||||
log(WARN(`\nRunning: ${bold(requestPath)}`));
|
||||
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
|
||||
|
||||
// Processing current request.
|
||||
const result = await processRequest(processRequestParams)();
|
||||
// Processing current request.
|
||||
const result = await processRequest(processRequestParams)();
|
||||
|
||||
// Updating global & selected envs with new envs from processed-request output.
|
||||
const { global, selected } = result.envs;
|
||||
envs.global = global;
|
||||
envs.selected = selected;
|
||||
// Updating global & selected envs with new envs from processed-request output.
|
||||
const { global, selected } = result.envs;
|
||||
envs.global = global;
|
||||
envs.selected = selected;
|
||||
|
||||
// Storing current request's report.
|
||||
const requestReport = result.report;
|
||||
@@ -84,15 +85,30 @@ export const collectionsRunner =
|
||||
|
||||
// Pushing remaining folders realted collection to stack.
|
||||
for (const folder of collection.folders) {
|
||||
const updatedFolder: HoppCollection = { ...folder }
|
||||
|
||||
if (updatedFolder.auth?.authType === "inherit") {
|
||||
updatedFolder.auth = collection.auth;
|
||||
}
|
||||
|
||||
if (collection.headers?.length) {
|
||||
// Filter out header entries present in the parent collection under the same name
|
||||
// This ensures the folder headers take precedence over the collection headers
|
||||
const filteredHeaders = collection.headers.filter((collectionHeaderEntries) => {
|
||||
return !updatedFolder.headers.some((folderHeaderEntries) => folderHeaderEntries.key === collectionHeaderEntries.key)
|
||||
})
|
||||
updatedFolder.headers.push(...filteredHeaders);
|
||||
}
|
||||
|
||||
collectionStack.push({
|
||||
path: `${path}/${folder.name}`,
|
||||
collection: folder,
|
||||
path: `${path}/${updatedFolder.name}`,
|
||||
collection: updatedFolder,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestsReport;
|
||||
};
|
||||
return requestsReport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms collections to generate collection-stack which describes each collection's
|
||||
@@ -100,9 +116,7 @@ export const collectionsRunner =
|
||||
* @param collections Hopp-collection objects to be mapped to collection-stack type.
|
||||
* @returns Mapped collections to collection-stack.
|
||||
*/
|
||||
const getCollectionStack = (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
): CollectionStack[] =>
|
||||
const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
|
||||
pipe(
|
||||
collections,
|
||||
A.map(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bold } from "chalk";
|
||||
import chalk from "chalk";
|
||||
import { groupEnd, group, log } from "console";
|
||||
import { handleError } from "../handlers/error";
|
||||
import { RequestConfig } from "../interfaces/request";
|
||||
@@ -120,7 +120,7 @@ export const printErrorsReport = (
|
||||
errorsReport: HoppCLIError[]
|
||||
) => {
|
||||
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);
|
||||
for (const errorReport of errorsReport) {
|
||||
@@ -143,7 +143,7 @@ export const printFailedTestsReport = (
|
||||
|
||||
// Only printing test-reports with failed test-cases.
|
||||
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);
|
||||
|
||||
for (const failedTestReport of failedTestsReport) {
|
||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
||||
*/
|
||||
start: (requestConfig: RequestConfig) => {
|
||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||
const ENDPOINT = requestConfig.url;
|
||||
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||
|
||||
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.
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as E from "fp-ts/Either";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as O from "fp-ts/Option";
|
||||
import { error } from "../types/errors";
|
||||
import round from "lodash/round";
|
||||
import { round } from "lodash-es";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "fs/promises";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { error } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { HoppCollection } from "@hoppscotch/data";
|
||||
|
||||
/**
|
||||
* Parses array of FormDataEntry to FormData.
|
||||
@@ -35,20 +35,20 @@ export const parseErrorMessage = (e: unknown) => {
|
||||
};
|
||||
|
||||
export async function readJsonFile(path: string): Promise<unknown> {
|
||||
if(!path.endsWith('.json')) {
|
||||
throw error({ code: "INVALID_FILE_TYPE", data: path })
|
||||
if (!path.endsWith(".json")) {
|
||||
throw error({ code: "INVALID_FILE_TYPE", data: path });
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(path)
|
||||
await fs.access(path);
|
||||
} catch (e) {
|
||||
throw error({ code: "FILE_NOT_FOUND", path: path })
|
||||
throw error({ code: "FILE_NOT_FOUND", path: path });
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse((await fs.readFile(path)).toString())
|
||||
} catch(e) {
|
||||
throw error({ code: "UNKNOWN_ERROR", data: e })
|
||||
return JSON.parse((await fs.readFile(path)).toString());
|
||||
} catch (e) {
|
||||
throw error({ code: "UNKNOWN_ERROR", data: e });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,22 +56,24 @@ export async function readJsonFile(path: string): Promise<unknown> {
|
||||
* Parses collection json file for given path:context.path, and validates
|
||||
* the parsed collectiona array.
|
||||
* @param path Collection json file path.
|
||||
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||
* @returns For successful parsing we get array of HoppCollection,
|
||||
*/
|
||||
export async function parseCollectionData(
|
||||
path: string
|
||||
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||
let contents = await readJsonFile(path)
|
||||
): Promise<HoppCollection[]> {
|
||||
let contents = await readJsonFile(path);
|
||||
|
||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
|
||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents)
|
||||
? contents
|
||||
: [contents];
|
||||
|
||||
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||
};
|
||||
return maybeArrayOfCollections as HoppCollection[];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
|
||||
export const preRequestScriptRunner = (
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> =>
|
||||
pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
|
||||
export function getEffectiveRESTRequest(
|
||||
request: HoppRESTRequest,
|
||||
environment: Environment
|
||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
||||
): E.Either<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> {
|
||||
const envVariables = environment.variables;
|
||||
|
||||
// Parsing final headers with applied ENVs.
|
||||
@@ -162,12 +168,30 @@ export function getEffectiveRESTRequest(
|
||||
}
|
||||
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({
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
effectiveRequest: {
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalDisplayURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
},
|
||||
updatedEnvs: { global: [], selected: envVariables },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import axios, { Method } from "axios";
|
||||
import { URL } from "url";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { responseErrors } from "./constants";
|
||||
import { getDurationInSeconds, getMetaDataPairs } from "./getters";
|
||||
import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test";
|
||||
import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import * as S from "fp-ts/string";
|
||||
import { hrtime } from "process";
|
||||
import { URL } from "url";
|
||||
import { EffectiveHoppRESTRequest, RequestConfig } from "../interfaces/request";
|
||||
import { RequestRunnerResponse } from "../interfaces/response";
|
||||
import { preRequestScriptRunner } from "./pre-request";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import {
|
||||
HoppEnvs,
|
||||
ProcessRequestParams,
|
||||
RequestReport,
|
||||
} from "../types/request";
|
||||
import { RequestMetrics } from "../types/response";
|
||||
import { responseErrors } from "./constants";
|
||||
import {
|
||||
printPreRequestRunner,
|
||||
printRequestRunner,
|
||||
printTestRunner,
|
||||
} from "./display";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { hrtime } from "process";
|
||||
import { RequestMetrics } from "../types/response";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { getDurationInSeconds, getMetaDataPairs } from "./getters";
|
||||
import { preRequestScriptRunner } from "./pre-request";
|
||||
import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
|
||||
// !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
|
||||
* perform HTTP request.
|
||||
@@ -38,6 +70,7 @@ import { pipe } from "fp-ts/function";
|
||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL
|
||||
};
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
const reqParams = finalParams(req);
|
||||
@@ -221,9 +254,13 @@ export const processRequest =
|
||||
effectiveFinalParams: [],
|
||||
effectiveFinalURL: "",
|
||||
};
|
||||
let updatedEnvs = <HoppEnvs>{};
|
||||
|
||||
// Fetch values for secret environment variables from system environment
|
||||
const processedEnvs = processEnvs(envs)
|
||||
|
||||
// Executing pre-request-script
|
||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
||||
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
||||
@@ -231,8 +268,8 @@ export const processRequest =
|
||||
report.errors.push(preRequestRes.left);
|
||||
report.result = report.result && false;
|
||||
} else {
|
||||
// Updating effective-request
|
||||
effectiveRequest = preRequestRes.right;
|
||||
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||
}
|
||||
|
||||
// Creating request-config for request-runner.
|
||||
@@ -270,7 +307,7 @@ export const processRequest =
|
||||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
request,
|
||||
envs
|
||||
updatedEnvs
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
@@ -309,9 +346,12 @@ export const processRequest =
|
||||
* @returns Updated request object free of invalid/missing data.
|
||||
*/
|
||||
export const preProcessRequest = (
|
||||
request: HoppRESTRequest
|
||||
request: HoppRESTRequest,
|
||||
collection: HoppCollection,
|
||||
): HoppRESTRequest => {
|
||||
const tempRequest = Object.assign({}, request);
|
||||
const { headers: parentHeaders, auth: parentAuth } = collection;
|
||||
|
||||
if (!tempRequest.v) {
|
||||
tempRequest.v = "1";
|
||||
}
|
||||
@@ -327,18 +367,31 @@ export const preProcessRequest = (
|
||||
if (!tempRequest.params) {
|
||||
tempRequest.params = [];
|
||||
}
|
||||
if (!tempRequest.headers) {
|
||||
|
||||
if (parentHeaders?.length) {
|
||||
// Filter out header entries present in the parent (folder/collection) under the same name
|
||||
// This ensures the child headers take precedence over the parent headers
|
||||
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
|
||||
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
|
||||
})
|
||||
tempRequest.headers.push(...filteredEntries);
|
||||
} else if (!tempRequest.headers) {
|
||||
tempRequest.headers = [];
|
||||
}
|
||||
|
||||
if (!tempRequest.preRequestScript) {
|
||||
tempRequest.preRequestScript = "";
|
||||
}
|
||||
if (!tempRequest.testScript) {
|
||||
tempRequest.testScript = "";
|
||||
}
|
||||
if (!tempRequest.auth) {
|
||||
|
||||
if (tempRequest.auth?.authType === "inherit") {
|
||||
tempRequest.auth = parentAuth;
|
||||
} else if (!tempRequest.auth) {
|
||||
tempRequest.auth = { authActive: false, authType: "none" };
|
||||
}
|
||||
|
||||
if (!tempRequest.body) {
|
||||
tempRequest.body = { contentType: null, body: null };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"outDir": ".",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
|
||||
@@ -3,17 +3,14 @@ import { defineConfig } from "tsup";
|
||||
export default defineConfig({
|
||||
entry: [ "./src/index.ts" ],
|
||||
outDir: "./dist/",
|
||||
format: ["cjs"],
|
||||
format: ["esm"],
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
bundle: true,
|
||||
target: "node12",
|
||||
target: "esnext",
|
||||
skipNodeModulesBundle: false,
|
||||
esbuildOptions(options) {
|
||||
options.bundle = true
|
||||
},
|
||||
noExternal: [
|
||||
/\w+/
|
||||
],
|
||||
clean: true,
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@ a {
|
||||
@apply shadow-none #{!important};
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-8;
|
||||
@apply -mt-7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ pre.ace_editor {
|
||||
|
||||
.toasted-container {
|
||||
@apply max-w-md;
|
||||
@apply z-[10000];
|
||||
|
||||
.toasted {
|
||||
&.toasted-primary {
|
||||
@@ -428,6 +429,11 @@ pre.ace_editor {
|
||||
}
|
||||
}
|
||||
|
||||
.splitpanes__pane {
|
||||
@apply will-change-auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@@ -516,9 +522,10 @@ pre.ace_editor {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-1;
|
||||
@apply min-w-[1.25rem];
|
||||
@apply min-h-[1.25rem];
|
||||
@apply px-0.5;
|
||||
@apply min-w-[1rem];
|
||||
@apply min-h-[1rem];
|
||||
@apply leading-none;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply border border-dividerDark;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
--lower-tertiary-sticky-fold: 7.125rem;
|
||||
--lower-fourth-sticky-fold: 9.188rem;
|
||||
--sidebar-primary-sticky-fold: 2rem;
|
||||
--properties-primary-sticky-fold: 2.063rem;
|
||||
}
|
||||
|
||||
@mixin light-theme {
|
||||
|
||||
@@ -78,12 +78,6 @@
|
||||
"iso": "he-HE",
|
||||
"name": "עִברִית"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"file": "hi.json",
|
||||
"iso": "hi-HI",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "hu",
|
||||
"file": "hu.json",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Kanselleer",
|
||||
"choose_file": "Kies 'n lêer",
|
||||
@@ -10,6 +11,7 @@
|
||||
"connect": "Koppel",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Kopieer",
|
||||
"create": "Create",
|
||||
"delete": "Vee uit",
|
||||
"disconnect": "Ontkoppel",
|
||||
"dismiss": "Weier",
|
||||
@@ -31,6 +33,7 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"properties": "Properties",
|
||||
"remove": "Verwyder",
|
||||
"rename": "Rename",
|
||||
"restore": "Herstel",
|
||||
@@ -39,6 +42,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Soek",
|
||||
"send": "Stuur",
|
||||
"share": "Share",
|
||||
"start": "Begin",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "Gesels met ons",
|
||||
"contact_us": "Kontak Ons",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Kopieer",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
@@ -73,6 +79,7 @@
|
||||
"keyboard_shortcuts": "Sleutelbord kortpaaie",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "Nuwe weergawe gevind. Herlaai om op te dateer.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "Volmag privaatheidsbeleid",
|
||||
"reload": "Herlaai",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Genereer teken",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Sluit in by URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Leer hoe",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Wagwoord",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Teken",
|
||||
"type": "Magtigingstipe",
|
||||
"username": "Gebruikersnaam"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "Versameling geskep",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Wysig versameling",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Gee 'n geldige naam vir die versameling",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -132,6 +157,8 @@
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "Nuwe versameling",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "Versameling hernoem",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Stoor as",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "Weet u seker dat u hierdie vouer permanent wil uitvee?",
|
||||
"remove_history": "Is u seker dat u alle geskiedenis permanent wil uitvee?",
|
||||
"remove_request": "Is u seker dat u hierdie versoek permanent wil uitvee?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Weet u seker dat u hierdie span wil uitvee?",
|
||||
"remove_telemetry": "Weet u seker dat u van Telemetry wil afskakel?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
@@ -162,6 +190,24 @@
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "Koptekst {count}",
|
||||
"message": "Boodskap {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokolle is leeg",
|
||||
"schema": "Koppel aan 'n GraphQL -eindpunt",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Spannaam leeg",
|
||||
"teams": "Spanne is leeg",
|
||||
"tests": "Daar is geen toetse vir hierdie versoek nie"
|
||||
"tests": "Daar is geen toetse vir hierdie versoek nie",
|
||||
"shortcodes": "Shortcodes are empty"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Add to Global",
|
||||
@@ -209,6 +257,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Gee 'n geldige naam vir die omgewing",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -232,8 +281,10 @@
|
||||
"variable_list": "Veranderlike lys"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "Dit lyk nie asof hierdie blaaier ondersteuning vir bedieners gestuurde geleenthede het nie.",
|
||||
"check_console_details": "Kyk na die konsole -log vir meer inligting.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL is nie behoorlik geformateer nie",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
@@ -249,9 +300,12 @@
|
||||
"json_prettify_invalid_body": "Kon nie 'n ongeldige liggaam mooi maak nie, los json -sintaksisfoute op en probeer weer",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Kon nie versoek stuur nie",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "Geen duur nie",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Kon nie voorafversoekskrip uitvoer nie",
|
||||
"something_went_wrong": "Iets het verkeerd geloop",
|
||||
@@ -260,6 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "Uitvoer as JSON",
|
||||
"create_secret_gist": "Skep geheime Gist",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"gist_created": "Gis geskep",
|
||||
"require_github": "Teken in met GitHub om 'n geheime idee te skep",
|
||||
"title": "Export"
|
||||
@@ -286,6 +341,9 @@
|
||||
"subscriptions": "Inskrywings",
|
||||
"switch_connection": "Switch connection"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
@@ -297,6 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "Die magtigingskop sal outomaties gegenereer word wanneer u die versoek stuur.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "Genereer eers dokumentasie",
|
||||
"network_fail": "Kon nie die API -eindpunt bereik nie. Kontroleer u netwerkverbinding en probeer weer.",
|
||||
"offline": "Dit lyk asof u vanlyn is. Data in hierdie werkruimte is moontlik nie op datum nie.",
|
||||
@@ -316,7 +376,10 @@
|
||||
"import": {
|
||||
"collections": "Voer versamelings in",
|
||||
"curl": "Voer cURL in",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "Invoer misluk",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "Invoer vanaf Gist",
|
||||
"from_gist_description": "Import from Gist URL",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
@@ -331,11 +394,17 @@
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Voer Gist URL in",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "Invoer"
|
||||
},
|
||||
"inspections": {
|
||||
@@ -373,8 +442,10 @@
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "Versamelings",
|
||||
"confirm": "Bevestig",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "Wysig versoek",
|
||||
"import_export": "Invoer uitvoer"
|
||||
"import_export": "Invoer uitvoer",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
@@ -449,13 +520,14 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Kopieer skakel",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Duur",
|
||||
"enter_curl": "Voer cURL in",
|
||||
"generate_code": "Genereer kode",
|
||||
"generated_code": "Kode gegenereer",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "Koplys",
|
||||
"invalid_name": "Gee 'n naam vir die versoek",
|
||||
"method": "Metode",
|
||||
@@ -480,12 +552,14 @@
|
||||
"saved": "Versoek gestoor",
|
||||
"share": "Deel",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Versoek",
|
||||
"type": "Soort versoek",
|
||||
"url": "URL",
|
||||
"variables": "Veranderlikes",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "View my links",
|
||||
"copy_link": "Kopieer skakel"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +587,7 @@
|
||||
"account_description": "Pas u rekeninginstellings aan.",
|
||||
"account_email_description": "Jou primêre e -posadres.",
|
||||
"account_name_description": "Dit is u vertoonnaam.",
|
||||
"additional": "Additional Settings",
|
||||
"background": "Agtergrond",
|
||||
"black_mode": "Swart",
|
||||
"choose_language": "Kies taal",
|
||||
@@ -559,14 +634,31 @@
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -596,7 +688,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Kopieer versoekskakel",
|
||||
"delete_method": "Kies DELETE metode",
|
||||
"get_method": "Kies GET -metode",
|
||||
"head_method": "Kies HOOF metode",
|
||||
@@ -611,8 +702,10 @@
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "Stoor in versamelings",
|
||||
"send_request": "Stuur versoek",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "Versoek"
|
||||
"title": "Versoek",
|
||||
"copy_request_link": "Kopieer versoekskakel"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
@@ -735,6 +828,7 @@
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "Na knipbord gekopieer",
|
||||
"deleted": "Uitgevee",
|
||||
"deprecated": "GEDRAGTEER",
|
||||
@@ -742,10 +836,12 @@
|
||||
"disconnected": "Ontkoppel",
|
||||
"disconnected_from": "Ontkoppel van {name}",
|
||||
"docs_generated": "Dokumentasie gegenereer",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "Aflaai begin",
|
||||
"enabled": "Geaktiveer",
|
||||
"file_imported": "Lêer ingevoer",
|
||||
"finished_in": "Klaar in {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "Geskiedenis uitgevee",
|
||||
"linewrap": "Draai lyne toe",
|
||||
"loading": "Laai tans ...",
|
||||
@@ -756,6 +852,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -791,6 +888,7 @@
|
||||
"queries": "Navrae",
|
||||
"query": "Navraag",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Toetse",
|
||||
@@ -807,6 +905,7 @@
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
||||
"exit": "Verlaat span",
|
||||
"exit_disabled": "Slegs eienaar kan nie die span verlaat nie",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "Die e -posformaat is ongeldig",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
@@ -848,6 +947,7 @@
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "Span gered",
|
||||
"select_a_team": "Select a team",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Spanne",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
@@ -879,5 +979,14 @@
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "الغاء",
|
||||
"choose_file": "اختيار ملف",
|
||||
@@ -10,6 +11,7 @@
|
||||
"connect": "الاتصال",
|
||||
"connecting": "Connecting",
|
||||
"copy": "نسخ",
|
||||
"create": "Create",
|
||||
"delete": "حذف",
|
||||
"disconnect": "قطع الاتصال",
|
||||
"dismiss": "رفض",
|
||||
@@ -31,6 +33,7 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "لصق",
|
||||
"prettify": "جمال",
|
||||
"properties": "Properties",
|
||||
"remove": "ازالة",
|
||||
"rename": "Rename",
|
||||
"restore": "اعادة",
|
||||
@@ -39,6 +42,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "بحث",
|
||||
"send": "ارسل",
|
||||
"share": "Share",
|
||||
"start": "ابدأ",
|
||||
"starting": "Starting",
|
||||
"stop": "قف",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "دردش معنا",
|
||||
"contact_us": "اتصل بنا",
|
||||
"cookies": "Cookies",
|
||||
"copy": "انسخ",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
@@ -73,6 +79,7 @@
|
||||
"keyboard_shortcuts": "اختصارات لوحة المفاتيح",
|
||||
"name": "هوبسكوتش",
|
||||
"new_version_found": "تم العثور على نسخة جديدة. قم بالتحديث للتحديث.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "سياسة خصوصية الوكيل",
|
||||
"reload": "إعادة تحميل",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "توليد رمز",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "تضمين في URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "تعلم كيف",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "كلمة المرور",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "رمز",
|
||||
"type": "نوع التفويض",
|
||||
"username": "اسم المستخدم"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "تم إنشاء المجموعة",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "تحرير المجموعة",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "الرجاء تقديم اسم صالح للمجموعة",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -132,6 +157,8 @@
|
||||
"name_length_insufficient": "اسم المجموعة يجب ان لايقل على 3 رموز",
|
||||
"new": "مجموعة جديدة",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "تمت إعادة تسمية المجموعة",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "حفظ باسم",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "هل أنت متأكد أنك تريد حذف هذا المجلد نهائيًا؟",
|
||||
"remove_history": "هل أنت متأكد أنك تريد حذف كل المحفوظات بشكل دائم؟",
|
||||
"remove_request": "هل أنت متأكد أنك تريد حذف هذا الطلب نهائيًا؟",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "هل أنت متأكد أنك تريد حذف هذا الفريق؟",
|
||||
"remove_telemetry": "هل أنت متأكد أنك تريد الانسحاب من القياس عن بعد؟",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
@@ -162,6 +190,24 @@
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "رأس {count}",
|
||||
"message": "الرسالة {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "سجل الدخول لرؤية فريقك",
|
||||
"protocols": "البروتوكولات فارغة",
|
||||
"schema": "اتصل بنقطة نهاية GraphQL",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "اسم الفريق فارغ",
|
||||
"teams": "الفرق فارغة",
|
||||
"tests": "لا توجد اختبارات لهذا الطلب"
|
||||
"tests": "لا توجد اختبارات لهذا الطلب",
|
||||
"shortcodes": "Shortcodes are empty"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Add to Global",
|
||||
@@ -209,6 +257,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "الرجاء تقديم اسم صالح للبيئة",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -232,8 +281,10 @@
|
||||
"variable_list": "قائمة متغيرة"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "يبدو أن هذا المستعرض لا يدعم أحداث إرسال الخادم.",
|
||||
"check_console_details": "تحقق من سجل وحدة التحكم للحصول على التفاصيل.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "لم يتم تنسيق cURL بشكل صحيح",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
@@ -249,9 +300,12 @@
|
||||
"json_prettify_invalid_body": "تعذر تجميل جسم غير صالح وحل أخطاء بناء جملة json وحاول مرة أخرى",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "تعذر إرسال الطلب",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "لا مدة",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "تعذر تنفيذ نص الطلب المسبق",
|
||||
"something_went_wrong": "هناك خطأ ما",
|
||||
@@ -260,6 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "تصدير بتنسيق JSON",
|
||||
"create_secret_gist": "إنشاء جوهر سري",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"gist_created": "خلقت الجست",
|
||||
"require_github": "تسجيل الدخول باستخدام GitHub لإنشاء جوهر سري",
|
||||
"title": "Export"
|
||||
@@ -286,6 +341,9 @@
|
||||
"subscriptions": "الاشتراكات",
|
||||
"switch_connection": "Switch connection"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
@@ -297,6 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "سيتم إنشاء رأس التفويض تلقائيًا عند إرسال الطلب.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "قم بإنشاء الوثائق أولاً",
|
||||
"network_fail": "تعذر الوصول إلى نقطة نهاية API. تحقق من اتصالك بالشبكة وحاول مرة أخرى.",
|
||||
"offline": "يبدو أنك غير متصل بالإنترنت. قد لا تكون البيانات الموجودة في مساحة العمل هذه محدثة.",
|
||||
@@ -316,7 +376,10 @@
|
||||
"import": {
|
||||
"collections": "مجموعات الاستيراد",
|
||||
"curl": "استيراد cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "فشل الاستيراد",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "الاستيراد من Gist",
|
||||
"from_gist_description": "استيراد من Gist URL",
|
||||
"from_insomnia": "استيراد من Insomnia",
|
||||
@@ -331,11 +394,17 @@
|
||||
"from_postman_description": "استيراد من مجموعة Postman",
|
||||
"from_url": "استيراد من رابط",
|
||||
"gist_url": "أدخل عنوان URL لـ Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "استيراد مجموعة من ملفHoppscotch Collections JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "يستورد"
|
||||
},
|
||||
"inspections": {
|
||||
@@ -373,8 +442,10 @@
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "المجموعات",
|
||||
"confirm": "يتأكد",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "تحرير الطلب",
|
||||
"import_export": "استيراد و تصدير"
|
||||
"import_export": "استيراد و تصدير",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
@@ -449,13 +520,14 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "نسخ الوصلة",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "مدة",
|
||||
"enter_curl": "أدخل cURL",
|
||||
"generate_code": "إنشاء التعليمات البرمجية",
|
||||
"generated_code": "رمز تم إنشاؤه",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "قائمة الرأس",
|
||||
"invalid_name": "يرجى تقديم اسم للطلب",
|
||||
"method": "طريقة",
|
||||
@@ -480,12 +552,14 @@
|
||||
"saved": "تم حفظ الطلب",
|
||||
"share": "يشارك",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "طلب",
|
||||
"type": "نوع الطلب",
|
||||
"url": "URL",
|
||||
"variables": "المتغيرات",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "View my links",
|
||||
"copy_link": "نسخ الوصلة"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +587,7 @@
|
||||
"account_description": "تخصيص إعدادات حسابك.",
|
||||
"account_email_description": "عنوان بريدك الإلكتروني الأساسي.",
|
||||
"account_name_description": "هذا هو اسم العرض الخاص بك.",
|
||||
"additional": "Additional Settings",
|
||||
"background": "خلفية",
|
||||
"black_mode": "أسود",
|
||||
"choose_language": "اختر اللغة",
|
||||
@@ -559,14 +634,31 @@
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "تأكيد البريد الإلكتروني"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -596,7 +688,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "نسخ ارتباط الطلب",
|
||||
"delete_method": "حدد طريقة الحذف",
|
||||
"get_method": "حدد طريقة GET",
|
||||
"head_method": "حدد طريقة HEAD",
|
||||
@@ -611,8 +702,10 @@
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "حفظ في المجموعات",
|
||||
"send_request": "ارسل طلب",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "طلب"
|
||||
"title": "طلب",
|
||||
"copy_request_link": "نسخ ارتباط الطلب"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
@@ -735,6 +828,7 @@
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "نسخ إلى الحافظة",
|
||||
"deleted": "تم الحذف",
|
||||
"deprecated": "إهمال",
|
||||
@@ -742,10 +836,12 @@
|
||||
"disconnected": "انقطع الاتصال",
|
||||
"disconnected_from": "انقطع الاتصال بـ {name}",
|
||||
"docs_generated": "تم إنشاء الوثائق",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "بدأ التنزيل",
|
||||
"enabled": "ممكن",
|
||||
"file_imported": "تم استيراد الملف",
|
||||
"finished_in": "انتهى في {duration} مللي ثانية",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "تم حذف السجل",
|
||||
"linewrap": "خطوط الالتفاف",
|
||||
"loading": "تحميل...",
|
||||
@@ -756,6 +852,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -791,6 +888,7 @@
|
||||
"queries": "استفسارات",
|
||||
"query": "استفسار",
|
||||
"schema": "مخطط",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "مقبس",
|
||||
"sse": "SSE",
|
||||
"tests": "الاختبارات",
|
||||
@@ -807,6 +905,7 @@
|
||||
"email_do_not_match": "البريد الإلكتروني لا يتوافق مع معلومات حسابك. اتصل بمدير الفريق.",
|
||||
"exit": "فريق الخروج",
|
||||
"exit_disabled": "فقط المالك لا يمكنه الخروج من الفريق",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "تنسيق البريد الإلكتروني غير صالح",
|
||||
"invalid_id": "معرف الفريق غير صالح. اتصل بمدير الفريق.",
|
||||
@@ -848,6 +947,7 @@
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "فريق حفظ",
|
||||
"select_a_team": "اختر فريق",
|
||||
"success_invites": "Success invites",
|
||||
"title": "فرق",
|
||||
"we_sent_invite_link": "لقد أرسلنا رابط دعوة لجميع المدعوين!",
|
||||
"we_sent_invite_link_description": "اطلب من جميع المدعوين التحقق من صندوق الوارد الخاص بهم. انقر على الرابط للانضمام إلى الفريق."
|
||||
@@ -879,5 +979,14 @@
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Cancel·lar",
|
||||
"choose_file": "Triar un fitxer",
|
||||
@@ -10,6 +11,7 @@
|
||||
"connect": "Connectar",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Copiar",
|
||||
"create": "Create",
|
||||
"delete": "Eliminar",
|
||||
"disconnect": "Desconnectar",
|
||||
"dismiss": "Tancar",
|
||||
@@ -31,6 +33,7 @@
|
||||
"open_workspace": "Obrir espai de treball",
|
||||
"paste": "Enganxar",
|
||||
"prettify": "Fes-ho bonic",
|
||||
"properties": "Properties",
|
||||
"remove": "Eliminar",
|
||||
"rename": "Rename",
|
||||
"restore": "Restaurar",
|
||||
@@ -39,6 +42,7 @@
|
||||
"scroll_to_top": "Desplaceu-vos cap a dalt",
|
||||
"search": "Cercar",
|
||||
"send": "Enviar",
|
||||
"share": "Share",
|
||||
"start": "Començar",
|
||||
"starting": "Starting",
|
||||
"stop": "Aturar",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "Xateja amb nosaltres",
|
||||
"contact_us": "Contacta amb nosaltres",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Copiar",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "Copiar User Auth Token",
|
||||
"developer_option": "Opcions de desenvolupador",
|
||||
"developer_option_description": "Eines de desenvolupament que ajuden en el desenvolupament i manteniment de Hoppscotch.",
|
||||
@@ -73,6 +79,7 @@
|
||||
"keyboard_shortcuts": "Dreceres de teclat",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "S'ha trobat una nova versió. Refresca per actualitzar.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Opcions",
|
||||
"proxy_privacy_policy": "Política de privadesa del servidor intermediari (proxy)",
|
||||
"reload": "Recarregar",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generar Token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Inclou a l'URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Aprèn com",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Passar per",
|
||||
"password": "Contrasenya",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Token",
|
||||
"type": "Tipus d'autorització",
|
||||
"username": "Nom d'usuari"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "Col·lecció creada",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Editar la col·lecció",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Proporcioneu un nom vàlid per a la col·lecció",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -132,6 +157,8 @@
|
||||
"name_length_insufficient": "El nom de la col·lecció ha de tenir almenys 3 caràcters",
|
||||
"new": "Nova col · lecció",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "S'ha canviat el nom de la col·lecció",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Guardar com",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "Està segur que vol suprimir definitivament aquesta carpeta?",
|
||||
"remove_history": "Està segur que vol suprimir definitivament tot l'historial?",
|
||||
"remove_request": "Està segur que vol suprimir definitivament aquesta sol·licitud?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Està segur que vol suprimir aquest equip?",
|
||||
"remove_telemetry": "Està segur que vol desactivar Telemetry?",
|
||||
"request_change": "Està segur que vol descartar la sol·licitud actual, els canvis no desats es perdran.",
|
||||
@@ -162,6 +190,24 @@
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "Capçalera {count}",
|
||||
"message": "Missatges {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "Inicia sessió per veure el vostre perfil",
|
||||
"protocols": "Els protocols estan buits",
|
||||
"schema": "Connecta't a un endpoint GraphQL",
|
||||
"shortcodes": "Els shortcodes estan buits",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "El nom de l'equip és buit",
|
||||
"teams": "Els equips estan buits",
|
||||
"tests": "No hi ha proves per a aquesta sol·licitud"
|
||||
"tests": "No hi ha proves per a aquesta sol·licitud",
|
||||
"shortcodes": "Els shortcodes estan buits"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Afegir-ho a Global",
|
||||
@@ -209,6 +257,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Proporcioneu un nom vàlid per a l'entorn",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -232,8 +281,10 @@
|
||||
"variable_list": "Llista de variables"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "Sembla que aquest navegador no és compatible amb els Esdeveniments Enviats pel Servidor (Server Sent Events).",
|
||||
"check_console_details": "Consulta el registre de la consola per obtenir més informació.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL no està formatat correctament",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
@@ -249,9 +300,12 @@
|
||||
"json_prettify_invalid_body": "No s'ha pogut personalitzar un cos no vàlid, resol els errors de sintaxi json i tornar-ho a provar",
|
||||
"network_error": "Sembla que hi ha un error de xarxa. Si us plau torna-ho a provar.",
|
||||
"network_fail": "No s'ha pogut enviar la sol·licitud",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "Sense durada",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No s'ha trobat cap coincidència",
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "No s'ha pogut executar l'script de sol·licitud prèvia",
|
||||
"something_went_wrong": "Alguna cosa ha anat malament",
|
||||
@@ -260,6 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "Exporta com a JSON",
|
||||
"create_secret_gist": "Crear un Gist secret",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"gist_created": "Gist creat",
|
||||
"require_github": "Inicieu la sessió amb GitHub per crear un Gisst secret",
|
||||
"title": "Exportar"
|
||||
@@ -286,6 +341,9 @@
|
||||
"subscriptions": "Subscripcions",
|
||||
"switch_connection": "Switch connection"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
@@ -297,6 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "La capçalera de l'autorització es generarà automàticament quan envieu la sol·licitud.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "Genereu documentació primer",
|
||||
"network_fail": "No es pot arribar al punt final de l'API. Comproveu la connexió de xarxa i torneu-ho a provar.",
|
||||
"offline": "Sembla que estàs fora de línia. És possible que les dades d'aquest espai de treball no estiguin actualitzades.",
|
||||
@@ -316,7 +376,10 @@
|
||||
"import": {
|
||||
"collections": "Importar col·leccions",
|
||||
"curl": "Importar cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "La importació ha fallat",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "Importar des de Gist",
|
||||
"from_gist_description": "Importar des de l'URL de Gist",
|
||||
"from_insomnia": "Importar des d'Insomnia",
|
||||
@@ -331,11 +394,17 @@
|
||||
"from_postman_description": "Importar des de la col·lecció de Postman",
|
||||
"from_url": "Importar des de l'URL",
|
||||
"gist_url": "Introduïu l'URL del Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "No s'han pogut obtenir dades de l'URL",
|
||||
"import_from_url_invalid_file_format": "S'ha produït un error en importar les col·leccions",
|
||||
"import_from_url_invalid_type": "Tipus no compatible. Els valors acceptats són 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Col·leccions importades",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "Importar col·leccions des d'un fitxer JSON de col·leccions Hoppscotch",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "Importació"
|
||||
},
|
||||
"inspections": {
|
||||
@@ -373,8 +442,10 @@
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "Col·leccions",
|
||||
"confirm": "Confirmar",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "Sol·licitud d'edició",
|
||||
"import_export": "Importar / Exportar"
|
||||
"import_export": "Importar / Exportar",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
@@ -449,13 +520,14 @@
|
||||
"structured": "Estructurat",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Copia l'enllaç",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Durada",
|
||||
"enter_curl": "Introduïu cURL",
|
||||
"generate_code": "Generar codi",
|
||||
"generated_code": "Codi generat",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "Llista de capçaleres",
|
||||
"invalid_name": "Proporcioneu un nom per a la sol·licitud",
|
||||
"method": "Mètode",
|
||||
@@ -480,12 +552,14 @@
|
||||
"saved": "S'ha desat la sol·licitud",
|
||||
"share": "Compartir",
|
||||
"share_description": "Comparteix Hoppscotch amb els teus amics",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Sol·licitud",
|
||||
"type": "Tipus de sol·licitud",
|
||||
"url": "URL",
|
||||
"variables": "Variables",
|
||||
"view_my_links": "Visualitzar els meus enllaços"
|
||||
"view_my_links": "Visualitzar els meus enllaços",
|
||||
"copy_link": "Copia l'enllaç"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +587,7 @@
|
||||
"account_description": "Personalitzeu la configuració del compte.",
|
||||
"account_email_description": "La vostra adreça de correu electrònic principal.",
|
||||
"account_name_description": "Aquest és el vostre nom d'exposició",
|
||||
"additional": "Additional Settings",
|
||||
"background": "Fons",
|
||||
"black_mode": "Negre",
|
||||
"choose_language": "Tria l'idioma",
|
||||
@@ -559,14 +634,31 @@
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "Verificar correu electronic"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Accions",
|
||||
"created_on": "Creat el",
|
||||
"deleted": "S'ha suprimit el shortcode",
|
||||
"method": "Mètode",
|
||||
"not_found": "No s'ha trobat el shortcode",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -596,7 +688,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copiar l'enllaç de la sol·licitud",
|
||||
"delete_method": "Seleccionar el mètode DELETE",
|
||||
"get_method": "Seleccionar el mètode GET",
|
||||
"head_method": "Seleccionar el mètode HEAD",
|
||||
@@ -611,8 +702,10 @@
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "Guardar a les col·leccions",
|
||||
"send_request": "Enviar sol.licitud",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "Sol·licitud"
|
||||
"title": "Sol·licitud",
|
||||
"copy_request_link": "Copiar l'enllaç de la sol·licitud"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
@@ -735,6 +828,7 @@
|
||||
"connection_error": "No s'ha pogut connectar",
|
||||
"connection_failed": "Connexió fallida",
|
||||
"connection_lost": "Connexió perduda",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "Copiat al porta-retalls",
|
||||
"deleted": "Eliminat",
|
||||
"deprecated": "Obsolet",
|
||||
@@ -742,10 +836,12 @@
|
||||
"disconnected": "Desconnectat",
|
||||
"disconnected_from": "Desconnectat de {name}",
|
||||
"docs_generated": "Documentació generada",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "S'ha iniciat la baixada",
|
||||
"enabled": "Activat",
|
||||
"file_imported": "Fitxer importat",
|
||||
"finished_in": "Acabat en {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "S'ha suprimit l'historial",
|
||||
"linewrap": "Embolcar línies",
|
||||
"loading": "S'està carregant...",
|
||||
@@ -756,6 +852,7 @@
|
||||
"published_error": "S'ha produït un error en publicar el missatge: {topic} al tema: {message}",
|
||||
"published_message": "Missatge publicat: {missatge} al tema: {tema}",
|
||||
"reconnection_error": "No s'ha pogut tornar a connectar",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "No s'ha pogut subscriure al tema: {topic}",
|
||||
"subscribed_success": "S'ha subscrit correctament al tema: {topic}",
|
||||
"unsubscribed_failed": "No s'ha pogut cancel·lar la subscripció al tema: {topic}",
|
||||
@@ -791,6 +888,7 @@
|
||||
"queries": "Consultes",
|
||||
"query": "Consulta",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Proves",
|
||||
@@ -807,6 +905,7 @@
|
||||
"email_do_not_match": "El correu electrònic no coincideix amb les dades del vostre compte. Contacta amb el propietari del teu equip.",
|
||||
"exit": "Sortir de l'equip",
|
||||
"exit_disabled": "L'únic propietari no pot sortir de l'equip",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "El format del correu electrònic no és vàlid",
|
||||
"invalid_id": "Identificador d'equip no vàlid. Contacta amb el propietari del teu equip.",
|
||||
@@ -848,6 +947,7 @@
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "S'ha guardat l'equip",
|
||||
"select_a_team": "Select a team",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Equips",
|
||||
"we_sent_invite_link": "Hem enviat un enllaç d'invitació a tots els convidats!",
|
||||
"we_sent_invite_link_description": "Demaneu a tots els convidats que comprovin la seva safata d'entrada. Feu clic a l'enllaç per unir-vos a l'equip."
|
||||
@@ -879,5 +979,14 @@
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Accions",
|
||||
"created_on": "Creat el",
|
||||
"deleted": "S'ha suprimit el shortcode",
|
||||
"method": "Mètode",
|
||||
"not_found": "No s'ha trobat el shortcode",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "自动滚动",
|
||||
"cancel": "取消",
|
||||
"choose_file": "选择文件",
|
||||
"clear": "清除",
|
||||
"clear_all": "全部清除",
|
||||
"clear_history": "Clear all History",
|
||||
"clear_history": "清除全部历史记录",
|
||||
"close": "关闭",
|
||||
"connect": "连接",
|
||||
"connecting": "连接中",
|
||||
"copy": "复制",
|
||||
"create": "Create",
|
||||
"delete": "删除",
|
||||
"disconnect": "断开连接",
|
||||
"dismiss": "忽略",
|
||||
@@ -31,14 +33,16 @@
|
||||
"open_workspace": "打开工作区",
|
||||
"paste": "粘贴",
|
||||
"prettify": "美化",
|
||||
"properties": "Properties",
|
||||
"remove": "移除",
|
||||
"rename": "Rename",
|
||||
"rename": "重命名",
|
||||
"restore": "恢复",
|
||||
"save": "保存",
|
||||
"scroll_to_bottom": "滚动至底部",
|
||||
"scroll_to_top": "滚动至顶部",
|
||||
"search": "搜索",
|
||||
"send": "发送",
|
||||
"share": "Share",
|
||||
"start": "开始",
|
||||
"starting": "正在开始",
|
||||
"stop": "停止",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "与我们交谈",
|
||||
"contact_us": "联系我们",
|
||||
"cookies": "Cookies",
|
||||
"copy": "复制",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "复制认证 Token",
|
||||
"developer_option": "开发者选项",
|
||||
"developer_option_description": "开发者工具,有助于开发和维护 Hoppscotch。",
|
||||
@@ -73,14 +79,15 @@
|
||||
"keyboard_shortcuts": "键盘快捷键",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "已发现新版本。刷新页面以更新。",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "选项",
|
||||
"proxy_privacy_policy": "代理隐私政策",
|
||||
"reload": "重新加载",
|
||||
"search": "搜索",
|
||||
"share": "分享",
|
||||
"shortcuts": "快捷方式",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "在社交媒体上关注我们,了解最新新闻、更新和发布。",
|
||||
"social_links": "社交媒体链接",
|
||||
"spotlight": "聚光灯",
|
||||
"status": "状态",
|
||||
"status_description": "检查网站状态",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "生成令牌",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "包含在 URL 内",
|
||||
"inherited_from": "Inherited {auth} from parent collection {collection} ",
|
||||
"learn": "了解更多",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "传递方式",
|
||||
"password": "密码",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "令牌",
|
||||
"type": "授权类型",
|
||||
"username": "用户名"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "集合已创建",
|
||||
"different_parent": "不能用不同的父类来重新排序集合",
|
||||
"edit": "编辑集合",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "请提供有效的集合名称",
|
||||
"invalid_root_move": "该集合已经在根级了",
|
||||
"moved": "移动完成",
|
||||
@@ -132,18 +157,20 @@
|
||||
"name_length_insufficient": "集合名字至少需要 3 个字符",
|
||||
"new": "新建集合",
|
||||
"order_changed": "集合顺序已更新",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "集合已更名",
|
||||
"request_in_use": "请求正在使用中",
|
||||
"save_as": "另存为",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "保存至集合",
|
||||
"select": "选择一个集合",
|
||||
"select_location": "选择位置",
|
||||
"select_team": "选择一个团队",
|
||||
"team_collections": "团队集合"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"close_unsaved_tab": "你确定要关闭此标签页吗?",
|
||||
"close_unsaved_tabs": "你确定要关闭所有标签页吗? {count} 个未保存的标签页将被丢失。",
|
||||
"exit_team": "你确定要离开此团队吗?",
|
||||
"logout": "你确定要登出吗?",
|
||||
"remove_collection": "你确定要永久删除该集合吗?",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "你确定要永久删除该文件夹吗?",
|
||||
"remove_history": "你确定要永久删除全部历史记录吗?",
|
||||
"remove_request": "你确定要永久删除该请求吗?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "你确定要删除该团队吗?",
|
||||
"remove_telemetry": "你确定要退出遥测服务吗?",
|
||||
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
|
||||
@@ -158,9 +186,27 @@
|
||||
"sync": "您确定要同步该工作区吗?"
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "添加至参数",
|
||||
"open_request_in_new_tab": "在新标签页中打开请求",
|
||||
"set_environment_variable": "设置为变量"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "请求头 {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "登录以查看你的个人资料",
|
||||
"protocols": "协议为空",
|
||||
"schema": "连接至 GraphQL 端点",
|
||||
"shortcodes": "Shortcodes 为空",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "订阅为空",
|
||||
"team_name": "团队名称为空",
|
||||
"teams": "团队为空",
|
||||
"tests": "没有针对该请求的测试"
|
||||
"tests": "没有针对该请求的测试",
|
||||
"shortcodes": "短链接为空"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "添加到全局环境",
|
||||
@@ -204,36 +252,39 @@
|
||||
"create_new": "创建新环境",
|
||||
"created": "环境已创建",
|
||||
"deleted": "环境已删除",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "环境已复制",
|
||||
"edit": "编辑环境",
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"empty_variables": "没有变量",
|
||||
"global": "全局",
|
||||
"global_variables": "全局变量",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "请提供有效的环境名称",
|
||||
"list": "Environment variables",
|
||||
"list": "环境变量",
|
||||
"my_environments": "我的环境",
|
||||
"name": "Name",
|
||||
"name": "名称",
|
||||
"nested_overflow": "环境嵌套深度超过限制(10层)",
|
||||
"new": "新建环境",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_active_environment": "没有激活的环境",
|
||||
"no_environment": "无环境",
|
||||
"no_environment_description": "没有选择环境。选择如何处理以下变量。",
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"quick_peek": "快速浏览环境",
|
||||
"replace_with_variable": "替换为变量",
|
||||
"scope": "范围",
|
||||
"select": "选择环境",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"set": "设置环境",
|
||||
"set_as_environment": "设置为环境",
|
||||
"team_environments": "团队环境",
|
||||
"title": "环境",
|
||||
"updated": "环境已更新",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"value": "值",
|
||||
"variable": "变量",
|
||||
"variable_list": "变量列表"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "该浏览器似乎不支持 SSE。",
|
||||
"check_console_details": "检查控制台日志以获悉详情",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL 格式不正确",
|
||||
"danger_zone": "危险区域",
|
||||
"delete_account": "您的帐号目前为这些团队的拥有者:",
|
||||
@@ -245,14 +296,18 @@
|
||||
"incorrect_email": "电子邮箱错误",
|
||||
"invalid_link": "无效链接",
|
||||
"invalid_link_description": "你点击的链接无效或已过期。",
|
||||
"invalid_embed_link": "The embed does not exist or is invalid.",
|
||||
"json_parsing_failed": "不合法的 JSON",
|
||||
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
|
||||
"network_error": "好像发生了网络错误,请重试。",
|
||||
"network_fail": "无法发送请求",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "无持续时间",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "找不到结果",
|
||||
"page_not_found": "找不到此頁面",
|
||||
"proxy_error": "Proxy error",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "代理错误",
|
||||
"script_fail": "无法执行预请求脚本",
|
||||
"something_went_wrong": "发生了一些错误",
|
||||
"test_script_fail": "无法执行请求脚本"
|
||||
@@ -260,9 +315,13 @@
|
||||
"export": {
|
||||
"as_json": "导出为 JSON",
|
||||
"create_secret_gist": "创建私密 Gist",
|
||||
"gist_created": "已创建 Gist",
|
||||
"create_secret_gist_tooltip_text": "Export as secret Gist",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"secret_gist_success": "Successfully exported as secret Gist",
|
||||
"require_github": "使用 GitHub 登录以创建私密 Gist",
|
||||
"title": "导出"
|
||||
"title": "导出",
|
||||
"success": "Successfully exported",
|
||||
"gist_created": "已创建 Gist"
|
||||
},
|
||||
"filter": {
|
||||
"all": "全部",
|
||||
@@ -278,13 +337,16 @@
|
||||
"renamed": "文件夹已更名"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "您想连接最新的 GraphQL 端点吗?",
|
||||
"connection_switch_new_url": "切换到标签页将使您与活动的 GraphQL 连接断开。新的连接 URL 是",
|
||||
"connection_switch_url": "您已连接到 GraphQL 端点,连接 URL 为",
|
||||
"mutations": "变更",
|
||||
"schema": "模式",
|
||||
"subscriptions": "订阅",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "切换连接"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "时间",
|
||||
@@ -297,6 +359,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "授权头将会在你发送请求时自动生成。",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "请先生成文档",
|
||||
"network_fail": "无法到达 API 端点。请检查网络连接并重试。",
|
||||
"offline": "你似乎处于离线状态,该工作区中的数据可能不是最新。",
|
||||
@@ -316,7 +380,10 @@
|
||||
"import": {
|
||||
"collections": "导入集合",
|
||||
"curl": "导入 cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "导入失败",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "从 Gist 导入",
|
||||
"from_gist_description": "从 Gist URL 导入",
|
||||
"from_insomnia": "从 Insomnia 导入",
|
||||
@@ -331,35 +398,41 @@
|
||||
"from_postman_description": "从 Postman 集合中导入",
|
||||
"from_url": "从 URL 导入",
|
||||
"gist_url": "输入 Gist URL",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "无法从网址取得资料",
|
||||
"import_from_url_invalid_file_format": "导入组合时发生错误",
|
||||
"import_from_url_invalid_type": "不支持此类型。可接受的值为 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||
"import_from_url_success": "已导入组合",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "从 Hoppscotch 的集合文件导入(JSON)",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "导入"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "查可能的错误",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "添加到环境",
|
||||
"not_found": "环境变量“{environment}”未找到。"
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "浏览器不允许 Hoppscotch 设置 Cookie 标头。当前我们正在开发 Hoppscotch 桌面应用程序(即将推出),与此同时请改用授权标头。"
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "请检查您的身份验证凭据。",
|
||||
"404_error": "请检查您的请求 URL 和方法类型。",
|
||||
"cors_error": "请检查您的跨源资源共享配置。",
|
||||
"default_error": "请检查您的请求。",
|
||||
"network_error": "请检查您的网络连接。"
|
||||
},
|
||||
"title": "Inspector",
|
||||
"url": {
|
||||
"extension_not_installed": "Extension not installed.",
|
||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "未安装扩展。",
|
||||
"extension_unknown_origin": "确保您已将 API 端点的源添加到 Hoppscotch 浏览器扩展列表中。",
|
||||
"extention_enable_action": "启用浏览器扩展",
|
||||
"extention_not_enabled": "扩展未启用。"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -373,8 +446,10 @@
|
||||
"close_unsaved_tab": "有未保存的变更",
|
||||
"collections": "集合",
|
||||
"confirm": "确认",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "编辑请求",
|
||||
"import_export": "导入/导出"
|
||||
"import_export": "导入/导出",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "您已经订阅了此主题。",
|
||||
@@ -389,10 +464,10 @@
|
||||
"invalid_topic": "请提供该订阅的主题",
|
||||
"keep_alive": "Keep Alive",
|
||||
"log": "日志",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "遗嘱消息",
|
||||
"lw_qos": "遗嘱消息QoS",
|
||||
"lw_retain": "遗嘱消息保留",
|
||||
"lw_topic": "遗嘱消息主题",
|
||||
"message": "消息",
|
||||
"new": "新订阅",
|
||||
"not_connected": "请先启动MQTT连接。",
|
||||
@@ -449,13 +524,14 @@
|
||||
"structured": "结构",
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "复制链接",
|
||||
"different_collection": "不能对来自不同集合的请求进行重新排序",
|
||||
"duplicated": "重复的请求",
|
||||
"duration": "持续时间",
|
||||
"enter_curl": "输入 cURL",
|
||||
"generate_code": "生成代码",
|
||||
"generated_code": "已生成代码",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "请求头列表",
|
||||
"invalid_name": "请提供请求名称",
|
||||
"method": "方法",
|
||||
@@ -472,7 +548,7 @@
|
||||
"payload": "负载",
|
||||
"query": "查询",
|
||||
"raw_body": "原始请求体",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重命名请求",
|
||||
"renamed": "请求重命名",
|
||||
"run": "运行",
|
||||
"save": "保存",
|
||||
@@ -480,12 +556,14 @@
|
||||
"saved": "请求已保存",
|
||||
"share": "分享",
|
||||
"share_description": "分享 Hoppscotch 给你的朋友",
|
||||
"stop": "Stop",
|
||||
"share_request": "Share Request",
|
||||
"stop": "停止",
|
||||
"title": "请求",
|
||||
"type": "请求类型",
|
||||
"url": "URL",
|
||||
"variables": "变量",
|
||||
"view_my_links": "查看我的链接"
|
||||
"view_my_links": "查看我的链接",
|
||||
"copy_link": "复制链接"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +591,7 @@
|
||||
"account_description": "自定义您的帐户设置。",
|
||||
"account_email_description": "您的主要电子邮箱地址。",
|
||||
"account_name_description": "这是您的显示名称。",
|
||||
"additional": "Additional Settings",
|
||||
"background": "背景",
|
||||
"black_mode": "黑色",
|
||||
"choose_language": "选择语言",
|
||||
@@ -559,14 +638,31 @@
|
||||
"verified_email": "已验证电子邮件地址",
|
||||
"verify_email": "验证电子邮箱"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "操作",
|
||||
"created_on": "创建于",
|
||||
"deleted": "已刪除快捷键",
|
||||
"method": "方法",
|
||||
"not_found": "找不到快捷键",
|
||||
"short_code": "快捷键",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -592,27 +688,28 @@
|
||||
"title": "导航"
|
||||
},
|
||||
"others": {
|
||||
"prettify": "Prettify Editor's Content",
|
||||
"title": "Others"
|
||||
"prettify": "美化内容",
|
||||
"title": "其他"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "复制请求链接",
|
||||
"delete_method": "选择 DELETE 方法",
|
||||
"get_method": "选择 GET 方法",
|
||||
"head_method": "选择 HEAD 方法",
|
||||
"import_curl": "Import cURL",
|
||||
"import_curl": "导入cURL",
|
||||
"method": "方法",
|
||||
"next_method": "选择下一个方法",
|
||||
"post_method": "选择 POST 方法",
|
||||
"previous_method": "选择上一个方法",
|
||||
"put_method": "选择 PUT 方法",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重命名请求",
|
||||
"reset_request": "重置请求",
|
||||
"save_request": "Save Request",
|
||||
"save_request": "保存请求",
|
||||
"save_to_collections": "保存到集合",
|
||||
"send_request": "发送请求",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "请求"
|
||||
"share_request": "Share Request",
|
||||
"show_code": "生成代码片段",
|
||||
"title": "请求",
|
||||
"copy_request_link": "复制请求链接"
|
||||
},
|
||||
"response": {
|
||||
"copy": "复制响应至剪贴板",
|
||||
@@ -642,82 +739,82 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "更改语言",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"title": "Environments"
|
||||
"delete": "删除当前环境",
|
||||
"duplicate": "复制当前环境",
|
||||
"duplicate_global": "复制全局环境",
|
||||
"edit": "编辑当前环境",
|
||||
"edit_global": "编辑全局环境",
|
||||
"new": "创建新环境",
|
||||
"new_variable": "创建新的环境变量",
|
||||
"title": "环境"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
"chat": "与支持人员聊天",
|
||||
"help_menu": "帮助和支持",
|
||||
"open_docs": "阅读文档",
|
||||
"open_github": "打开 GitHub 存储库",
|
||||
"open_keybindings": "键盘快捷键",
|
||||
"social": "社交媒体",
|
||||
"title": "一般"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "连接到服务器",
|
||||
"disconnect": "与服务器断开连接"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "邀请你的朋友来 Hoppscotch",
|
||||
"title": "杂项"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "另存为新请求",
|
||||
"select_method": "选择方法",
|
||||
"switch_to": "切换到",
|
||||
"tab_authorization": "授权标签页",
|
||||
"tab_body": "请求体标签页",
|
||||
"tab_headers": "请求头标签页",
|
||||
"tab_parameters": "参数标签页",
|
||||
"tab_pre_request_script": "预请求脚本标签页",
|
||||
"tab_query": "查询标签页",
|
||||
"tab_tests": "测试标签页b",
|
||||
"tab_variables": "变量标签页"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "复制响应",
|
||||
"download": "将响应下载为文件",
|
||||
"title": "响应"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"interceptor": "拦截器",
|
||||
"interface": "界面",
|
||||
"theme": "主题",
|
||||
"user": "用户"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "更改拦截器",
|
||||
"change_language": "更改语言",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "黑色",
|
||||
"dark": "暗色",
|
||||
"light": "亮色",
|
||||
"system": "系统"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "关闭当前标签页",
|
||||
"close_others": "关闭所有其他标签页",
|
||||
"duplicate": "复制当前标签页",
|
||||
"new_tab": "打开新的标签页",
|
||||
"title": "标签页"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "删除当前团队",
|
||||
"edit": "编辑当前团队",
|
||||
"invite": "邀请人员加入团队",
|
||||
"new": "创建新团队",
|
||||
"switch_to_personal": "切换到您的个人工作空间",
|
||||
"title": "团队"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -735,6 +832,7 @@
|
||||
"connection_error": "连接错误",
|
||||
"connection_failed": "连接失败",
|
||||
"connection_lost": "连接丢失",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "已复制到剪贴板",
|
||||
"deleted": "已删除",
|
||||
"deprecated": "已弃用",
|
||||
@@ -742,10 +840,12 @@
|
||||
"disconnected": "断开连接",
|
||||
"disconnected_from": "与 {name} 断开连接",
|
||||
"docs_generated": "已生成文档",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "开始下载",
|
||||
"enabled": "启用",
|
||||
"file_imported": "文件已导入",
|
||||
"finished_in": "在 {duration} 毫秒内完成",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "历史记录已删除",
|
||||
"linewrap": "换行",
|
||||
"loading": "正在加载……",
|
||||
@@ -756,6 +856,7 @@
|
||||
"published_error": "将信息:{topic}发布至主题:{message}时发生错误",
|
||||
"published_message": "已将此信息:{message} 发布至主题:{topic}",
|
||||
"reconnection_error": "重连失败",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "无法订阅此主题:{topic}",
|
||||
"subscribed_success": "成功订阅此主题:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主题:{topic}",
|
||||
@@ -791,6 +892,7 @@
|
||||
"queries": "查询",
|
||||
"query": "查询",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "测试",
|
||||
@@ -807,6 +909,7 @@
|
||||
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
|
||||
"exit": "退出团队",
|
||||
"exit_disabled": "团队所有者无法退出团队",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "无效的集合 ID",
|
||||
"invalid_email_format": "电子邮箱格式无效",
|
||||
"invalid_id": "无效的团队 ID,请联系你的团队者。",
|
||||
@@ -848,6 +951,7 @@
|
||||
"same_target_destination": "目标相同",
|
||||
"saved": "团队已保存",
|
||||
"select_a_team": "选择团队",
|
||||
"success_invites": "Success invites",
|
||||
"title": "团队",
|
||||
"we_sent_invite_link": "我们向所有受邀者发送了邀请链接!",
|
||||
"we_sent_invite_link_description": "请所有受邀者检查他们的收件箱,点击链接以加入团队。"
|
||||
@@ -879,5 +983,14 @@
|
||||
"personal": "我的工作空间",
|
||||
"team": "团队工作空间",
|
||||
"title": "工作空间"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "操作",
|
||||
"created_on": "创建于",
|
||||
"deleted": "已刪除短链接",
|
||||
"method": "方法",
|
||||
"not_found": "找不到短链接",
|
||||
"short_code": "短链接",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "zrušení",
|
||||
"choose_file": "Vyberte soubor",
|
||||
@@ -10,6 +11,7 @@
|
||||
"connect": "Připojit",
|
||||
"connecting": "Connecting",
|
||||
"copy": "kopírovat",
|
||||
"create": "Create",
|
||||
"delete": "Vymazat",
|
||||
"disconnect": "Odpojit",
|
||||
"dismiss": "Zavrhnout",
|
||||
@@ -31,6 +33,7 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"properties": "Properties",
|
||||
"remove": "Odstranit",
|
||||
"rename": "Rename",
|
||||
"restore": "Obnovit",
|
||||
@@ -39,6 +42,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Vyhledávání",
|
||||
"send": "Poslat",
|
||||
"share": "Share",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "piš si s námi",
|
||||
"contact_us": "Kontaktujte nás",
|
||||
"cookies": "Cookies",
|
||||
"copy": "kopírovat",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
@@ -73,6 +79,7 @@
|
||||
"keyboard_shortcuts": "Klávesové zkratky",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "Nalezena nová verze. Aktualizujte aktualizací.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "Zásady ochrany osobních údajů proxy",
|
||||
"reload": "Znovu načíst",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generovat token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Zahrnout do adresy URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Zjistěte jak",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Heslo",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Žeton",
|
||||
"type": "Typ autorizace",
|
||||
"username": "Uživatelské jméno"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "Kolekce vytvořena",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Upravit sbírku",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Uveďte prosím platný název kolekce",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -132,6 +157,8 @@
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "Nová kolekce",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "Sbírka přejmenována",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Uložit jako",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "Opravdu chcete tuto složku trvale smazat?",
|
||||
"remove_history": "Opravdu chcete trvale smazat celou historii?",
|
||||
"remove_request": "Opravdu chcete tento požadavek trvale smazat?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Opravdu chcete tento tým smazat?",
|
||||
"remove_telemetry": "Opravdu se chcete odhlásit z telemetrie?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
@@ -162,6 +190,24 @@
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "Záhlaví {count}",
|
||||
"message": "Zpráva {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokoly jsou prázdné",
|
||||
"schema": "Připojte se ke koncovému bodu GraphQL",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Název týmu prázdný",
|
||||
"teams": "Týmy jsou prázdné",
|
||||
"tests": "Pro tento požadavek neexistují žádné testy"
|
||||
"tests": "Pro tento požadavek neexistují žádné testy",
|
||||
"shortcodes": "Shortcodes are empty"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Add to Global",
|
||||
@@ -209,6 +257,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Zadejte platný název prostředí",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -232,8 +281,10 @@
|
||||
"variable_list": "Seznam proměnných"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "Zdá se, že tento prohlížeč nemá podporu událostí odeslaných serverem.",
|
||||
"check_console_details": "Podrobnosti najdete v protokolu konzoly.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL nemá správný formát",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
@@ -249,9 +300,12 @@
|
||||
"json_prettify_invalid_body": "Nelze předtifikovat neplatné tělo, vyřešit chyby syntaxe json a zkusit to znovu",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Žádost nelze odeslat",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "Žádné trvání",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Skript předběžného požadavku nelze spustit",
|
||||
"something_went_wrong": "Něco se pokazilo",
|
||||
@@ -260,6 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "Exportovat jako JSON",
|
||||
"create_secret_gist": "Vytvořte tajnou podstatu",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"gist_created": "Podstata vytvořena",
|
||||
"require_github": "Přihlaste se pomocí GitHub a vytvořte tajný seznam",
|
||||
"title": "Export"
|
||||
@@ -286,6 +341,9 @@
|
||||
"subscriptions": "Předplatné",
|
||||
"switch_connection": "Switch connection"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
@@ -297,6 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "Autorizační hlavička se automaticky vygeneruje při odeslání požadavku.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "Nejprve vytvořte dokumentaci",
|
||||
"network_fail": "Nelze dosáhnout koncového bodu API. Zkontrolujte připojení k síti a zkuste to znovu.",
|
||||
"offline": "Zdá se, že jste offline. Data v tomto pracovním prostoru nemusí být aktuální.",
|
||||
@@ -316,7 +376,10 @@
|
||||
"import": {
|
||||
"collections": "Import sbírek",
|
||||
"curl": "Importovat cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "Import se nezdařil",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "Import z Gist",
|
||||
"from_gist_description": "Import from Gist URL",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
@@ -331,11 +394,17 @@
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Zadejte URL adresy",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "Import"
|
||||
},
|
||||
"inspections": {
|
||||
@@ -373,8 +442,10 @@
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "Sbírky",
|
||||
"confirm": "Potvrdit",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "Upravit požadavek",
|
||||
"import_export": "Import Export"
|
||||
"import_export": "Import Export",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
@@ -449,13 +520,14 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Kopírovat odkaz",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Doba trvání",
|
||||
"enter_curl": "Zadejte cURL",
|
||||
"generate_code": "Vygenerujte kód",
|
||||
"generated_code": "Generovaný kód",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "Seznam záhlaví",
|
||||
"invalid_name": "Uveďte prosím název žádosti",
|
||||
"method": "Metoda",
|
||||
@@ -480,12 +552,14 @@
|
||||
"saved": "Žádost uložena",
|
||||
"share": "Podíl",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Žádost",
|
||||
"type": "Typ požadavku",
|
||||
"url": "URL",
|
||||
"variables": "Proměnné",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "View my links",
|
||||
"copy_link": "Kopírovat odkaz"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +587,7 @@
|
||||
"account_description": "Přizpůsobte si nastavení účtu.",
|
||||
"account_email_description": "Vaše primární e -mailová adresa.",
|
||||
"account_name_description": "Toto je vaše zobrazované jméno.",
|
||||
"additional": "Additional Settings",
|
||||
"background": "Pozadí",
|
||||
"black_mode": "Černá",
|
||||
"choose_language": "Vyber jazyk",
|
||||
@@ -559,14 +634,31 @@
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -596,7 +688,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Kopírovat požadavek na odkaz",
|
||||
"delete_method": "Vyberte metodu ODSTRANIT",
|
||||
"get_method": "Vyberte metodu ZÍSKAT",
|
||||
"head_method": "Vyberte metodu HEAD",
|
||||
@@ -611,8 +702,10 @@
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "Uložit do sbírek",
|
||||
"send_request": "Poslat žádost",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "Žádost"
|
||||
"title": "Žádost",
|
||||
"copy_request_link": "Kopírovat požadavek na odkaz"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
@@ -735,6 +828,7 @@
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "Zkopírováno do schránky",
|
||||
"deleted": "Smazáno",
|
||||
"deprecated": "ZASTARALÉ",
|
||||
@@ -742,10 +836,12 @@
|
||||
"disconnected": "Odpojeno",
|
||||
"disconnected_from": "Odpojeno od {name}",
|
||||
"docs_generated": "Vygenerovaná dokumentace",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "Stahování zahájeno",
|
||||
"enabled": "Povoleno",
|
||||
"file_imported": "Soubor importován",
|
||||
"finished_in": "Hotovo za {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "Historie odstraněna",
|
||||
"linewrap": "Zabalit linky",
|
||||
"loading": "Načítání...",
|
||||
@@ -756,6 +852,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -791,6 +888,7 @@
|
||||
"queries": "Dotazy",
|
||||
"query": "Dotaz",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Testy",
|
||||
@@ -807,6 +905,7 @@
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
||||
"exit": "Ukončete tým",
|
||||
"exit_disabled": "Pouze vlastník nemůže opustit tým",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "Formát e -mailu je neplatný",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
@@ -848,6 +947,7 @@
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "Tým uložen",
|
||||
"select_a_team": "Select a team",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Týmy",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
@@ -879,5 +979,14 @@
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Afbestille",
|
||||
"choose_file": "Vælg en fil",
|
||||
@@ -10,6 +11,7 @@
|
||||
"connect": "Opret forbindelse",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Kopi",
|
||||
"create": "Create",
|
||||
"delete": "Slet",
|
||||
"disconnect": "Koble fra",
|
||||
"dismiss": "Afskedige",
|
||||
@@ -31,6 +33,7 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"properties": "Properties",
|
||||
"remove": "Fjerne",
|
||||
"rename": "Rename",
|
||||
"restore": "Gendan",
|
||||
@@ -39,6 +42,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Søg",
|
||||
"send": "Sende",
|
||||
"share": "Share",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Hold op",
|
||||
@@ -57,7 +61,9 @@
|
||||
"app": {
|
||||
"chat_with_us": "Chat med os",
|
||||
"contact_us": "Kontakt os",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Kopi",
|
||||
"copy_interface_type": "Copy interface type",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
@@ -73,6 +79,7 @@
|
||||
"keyboard_shortcuts": "Tastaturgenveje",
|
||||
"name": "Hoppscotch",
|
||||
"new_version_found": "Ny version fundet. Opdater for at opdatere.",
|
||||
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "Politik til beskyttelse af personlige oplysninger i proxy",
|
||||
"reload": "Genindlæs",
|
||||
@@ -112,10 +119,27 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generer Token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Inkluder i URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Lær hvordan",
|
||||
"oauth": {
|
||||
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||
"redirect_no_client_id": "No Client ID defined",
|
||||
"redirect_no_client_secret": "No Client Secret Defined",
|
||||
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
|
||||
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
|
||||
},
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Adgangskode",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Polet",
|
||||
"type": "Godkendelse Type",
|
||||
"username": "Brugernavn"
|
||||
@@ -124,6 +148,7 @@
|
||||
"created": "Samlingen er oprettet",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Rediger samling",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Angiv et gyldigt navn til samlingen",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -132,6 +157,8 @@
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "Ny kollektion",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties": "Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "Samling omdøbt",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Gem som",
|
||||
@@ -151,6 +178,7 @@
|
||||
"remove_folder": "Er du sikker på, at du vil slette denne mappe permanent?",
|
||||
"remove_history": "Er du sikker på, at du vil slette hele historikken permanent?",
|
||||
"remove_request": "Er du sikker på, at du vil slette denne anmodning permanent?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Er du sikker på, at du vil slette dette hold?",
|
||||
"remove_telemetry": "Er du sikker på, at du vil fravælge telemetri?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
@@ -162,6 +190,24 @@
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
"cookie_expires": "Expires",
|
||||
"cookie_name": "Name",
|
||||
"cookie_path": "Path",
|
||||
"cookie_string": "Cookie string",
|
||||
"cookie_value": "Value",
|
||||
"empty_domain": "Domain is empty",
|
||||
"empty_domains": "Domain list is empty",
|
||||
"enter_cookie_string": "Enter cookie string",
|
||||
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||
"managed_tab": "Managed",
|
||||
"new_domain_name": "New domain name",
|
||||
"no_cookies_in_domain": "No cookies set for this domain",
|
||||
"raw_tab": "Raw",
|
||||
"set": "Set a cookie"
|
||||
}
|
||||
},
|
||||
"count": {
|
||||
"header": "Overskrift {count}",
|
||||
"message": "Besked {count}",
|
||||
@@ -192,11 +238,13 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protokoller er tomme",
|
||||
"schema": "Opret forbindelse til et GraphQL -slutpunkt",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Teamnavn er tomt",
|
||||
"teams": "Hold er tomme",
|
||||
"tests": "Der er ingen test for denne anmodning"
|
||||
"tests": "Der er ingen test for denne anmodning",
|
||||
"shortcodes": "Shortcodes are empty"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Add to Global",
|
||||
@@ -209,6 +257,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Angiv et gyldigt navn på miljøet",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -232,8 +281,10 @@
|
||||
"variable_list": "Variabel liste"
|
||||
},
|
||||
"error": {
|
||||
"authproviders_load_error": "Unable to load auth providers",
|
||||
"browser_support_sse": "Det ser ikke ud til, at denne browser understøtter Server Sent Events.",
|
||||
"check_console_details": "Tjek konsollog for at få flere oplysninger.",
|
||||
"check_how_to_add_origin": "Check how you can add an origin",
|
||||
"curl_invalid_format": "cURL er ikke formateret korrekt",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
@@ -249,9 +300,12 @@
|
||||
"json_prettify_invalid_body": "Kunne ikke pryde et ugyldigt brødtekst, løse json -syntaksfejl og prøve igen",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Anmodningen kunne ikke sendes",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "Ingen varighed",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Kunne ikke udføre pre-request script",
|
||||
"something_went_wrong": "Noget gik galt",
|
||||
@@ -260,6 +314,7 @@
|
||||
"export": {
|
||||
"as_json": "Eksporter som JSON",
|
||||
"create_secret_gist": "Opret hemmelig Gist",
|
||||
"failed": "Something went wrong while exporting",
|
||||
"gist_created": "Gist skabt",
|
||||
"require_github": "Log ind med GitHub for at skabe hemmelig kerne",
|
||||
"title": "Export"
|
||||
@@ -286,6 +341,9 @@
|
||||
"subscriptions": "Abonnementer",
|
||||
"switch_connection": "Switch connection"
|
||||
},
|
||||
"graphql_collections": {
|
||||
"title": "GraphQL Collections"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"url": "URL"
|
||||
@@ -297,6 +355,8 @@
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "Autorisationsoverskriften genereres automatisk, når du sender anmodningen.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"generate_documentation_first": "Generer først dokumentation",
|
||||
"network_fail": "Kunne ikke nå API -slutpunktet. Kontroller din netværksforbindelse, og prøv igen.",
|
||||
"offline": "Du ser ud til at være offline. Data i dette arbejdsområde er muligvis ikke opdaterede.",
|
||||
@@ -316,7 +376,10 @@
|
||||
"import": {
|
||||
"collections": "Importer samlinger",
|
||||
"curl": "Importer cURL",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"failed": "Import mislykkedes",
|
||||
"from_file": "Import from File",
|
||||
"from_gist": "Import fra Gist",
|
||||
"from_gist_description": "Import from Gist URL",
|
||||
"from_insomnia": "Import from Insomnia",
|
||||
@@ -331,11 +394,17 @@
|
||||
"from_postman_description": "Import from Postman collection",
|
||||
"from_url": "Import from URL",
|
||||
"gist_url": "Indtast Gist URL",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||
"import_from_url_invalid_file_format": "Error while importing collections",
|
||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||
"import_from_url_success": "Collections Imported",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"title": "Importere"
|
||||
},
|
||||
"inspections": {
|
||||
@@ -373,8 +442,10 @@
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "Samlinger",
|
||||
"confirm": "Bekræfte",
|
||||
"customize_request": "Customize Request",
|
||||
"edit_request": "Rediger anmodning",
|
||||
"import_export": "Import Eksport"
|
||||
"import_export": "Import Eksport",
|
||||
"share_request": "Share Request"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
@@ -449,13 +520,14 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Kopier link",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Varighed",
|
||||
"enter_curl": "Indtast cURL",
|
||||
"generate_code": "Generer kode",
|
||||
"generated_code": "Genereret kode",
|
||||
"go_to_authorization_tab": "Go to Authorization tab",
|
||||
"go_to_body_tab": "Go to Body tab",
|
||||
"header_list": "Overskriftsliste",
|
||||
"invalid_name": "Angiv et navn på anmodningen",
|
||||
"method": "Metode",
|
||||
@@ -480,12 +552,14 @@
|
||||
"saved": "Anmodning gemt",
|
||||
"share": "Del",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Anmodning",
|
||||
"type": "Anmodningstype",
|
||||
"url": "URL",
|
||||
"variables": "Variabler",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "View my links",
|
||||
"copy_link": "Kopier link"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
@@ -513,6 +587,7 @@
|
||||
"account_description": "Tilpas dine kontoindstillinger.",
|
||||
"account_email_description": "Din primære e -mail -adresse.",
|
||||
"account_name_description": "Dette er dit visningsnavn.",
|
||||
"additional": "Additional Settings",
|
||||
"background": "Baggrund",
|
||||
"black_mode": "Sort",
|
||||
"choose_language": "Vælg sprog",
|
||||
@@ -559,14 +634,31 @@
|
||||
"verified_email": "Verified email",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests": {
|
||||
"button": "Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"creating_widget": "Creating widget",
|
||||
"customize": "Customize",
|
||||
"deleted": "Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed": "Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link": "Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"modified": "Shared request modified",
|
||||
"not_found": "Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview": "Preview",
|
||||
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||
"theme": {
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -596,7 +688,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Kopiér anmodningslink",
|
||||
"delete_method": "Vælg SLET metode",
|
||||
"get_method": "Vælg GET -metode",
|
||||
"head_method": "Vælg HEAD -metode",
|
||||
@@ -611,8 +702,10 @@
|
||||
"save_request": "Save Request",
|
||||
"save_to_collections": "Gem i samlinger",
|
||||
"send_request": "Send anmodning",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "Anmodning"
|
||||
"title": "Anmodning",
|
||||
"copy_request_link": "Kopiér anmodningslink"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
@@ -735,6 +828,7 @@
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||
"copied_to_clipboard": "Kopieret til udklipsholder",
|
||||
"deleted": "Slettet",
|
||||
"deprecated": "DEPRECATED",
|
||||
@@ -742,10 +836,12 @@
|
||||
"disconnected": "Afbrudt",
|
||||
"disconnected_from": "Koblet fra {name}",
|
||||
"docs_generated": "Dokumentation genereret",
|
||||
"download_failed": "Download failed",
|
||||
"download_started": "Downloaden er startet",
|
||||
"enabled": "Aktiveret",
|
||||
"file_imported": "Fil importeret",
|
||||
"finished_in": "Færdig om {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "Historik slettet",
|
||||
"linewrap": "Wrap linjer",
|
||||
"loading": "Indlæser...",
|
||||
@@ -756,6 +852,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -791,6 +888,7 @@
|
||||
"queries": "Forespørgsler",
|
||||
"query": "Forespørgsel",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Test",
|
||||
@@ -807,6 +905,7 @@
|
||||
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
|
||||
"exit": "Afslut Team",
|
||||
"exit_disabled": "Kun ejeren kan ikke forlade teamet",
|
||||
"failed_invites": "Failed invites",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_email_format": "E -mailformatet er ugyldigt",
|
||||
"invalid_id": "Invalid team ID. Contact your team owner.",
|
||||
@@ -848,6 +947,7 @@
|
||||
"same_target_destination": "Same target and destination",
|
||||
"saved": "Hold reddet",
|
||||
"select_a_team": "Select a team",
|
||||
"success_invites": "Success invites",
|
||||
"title": "Hold",
|
||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
|
||||
@@ -879,5 +979,14 @@
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user