Compare commits

..

86 Commits

Author SHA1 Message Date
James George
4bd54b12cd fix(persistence-service): add fallbacks for environments related schemas (#3832) 2024-02-15 23:38:56 +05:30
Andrew Bastin
ed6e9b6954 chore: bump version to 2023.12.5 2024-02-15 21:47:58 +05:30
James George
dfdd44b4ed fix(persistence-service): update global environment variables schema (#3829) 2024-02-15 21:40:31 +05:30
Akash K
fc34871dae fix: accessing undefined property variables (#3831) 2024-02-15 21:32:50 +05:30
Nivedin
45b532747e fix: environment tooltip update bug (#3819) 2024-02-13 17:42:02 +05:30
Akash K
de4635df23 chore: add workspace type property in request run analytics event (#3820)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2024-02-13 17:38:11 +05:30
Nivedin
41bad1f3dc fix: secret environment flow bugs (#3817) 2024-02-10 20:22:10 +05:30
Andrew Bastin
ecca3d2032 chore: correct linting errors 2024-02-09 14:42:12 +05:30
Muhammed Ajmal M
47226be6d0 feat: persist line wrap setting (#3647)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-09 14:05:09 +05:30
Andrew Bastin
6a0e73fdec chore: bump versions 2024-02-08 22:41:59 +05:30
Andrew Bastin
672ee69b2c chore: correct linting errors 2024-02-08 22:33:03 +05:30
James George
c0fae79678 fix(sh-admin): persist active selection in the sidebar (#3812) 2024-02-08 22:16:33 +05:30
James George
5bcc38e36b feat: support secret environment variables in CLI (#3815) 2024-02-08 22:08:18 +05:30
Nivedin
00862eb192 feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-08 21:58:42 +05:30
Akash K
16803acb26 chore: Oauth temporary ux improvements (#3792) 2024-02-06 20:35:29 +05:30
Nivedin
3911c9cd1f refactor: update share request flow (#3805) 2024-02-05 23:50:15 +05:30
Florian Metz
0028f6e878 feat(js-sandbox): expose atob & btoa functions for Node.js (#3724)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-05 23:12:55 +05:30
Anwarul Islam
0ba33ec187 fix: request endpoint heading (#3804) 2024-02-05 23:08:16 +05:30
James George
d7cdeb796a chore(common): analytics on spotlight (#3727)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
2024-02-02 15:32:06 +05:30
Andrew Bastin
aab76f1358 chore: bump version to 2023.12.3 2024-01-30 20:27:25 +05:30
Anwarul Islam
a28a576c41 feat: team environment search and switch (#3700)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-30 19:49:04 +05:30
Liyas Thomas
0d0ad7a2f8 chore: added micro interactions (#3783)
Co-authored-by: Dmitry <mukovkin@yandex.ru>
2024-01-30 18:42:42 +05:30
Balu Babu
1df9de44b7 chore: upgraded prisma version to v5.8.0 (#3787) 2024-01-30 18:16:28 +05:30
Muhammed Ajmal M
4cba03e53f feat(js-sandbox): add pw.env.unset method (#3677)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 22:31:27 +05:30
Nivedin
9e1466a877 fix: bugs in shared request (#3704) 2024-01-23 22:24:18 +05:30
Anwarul Islam
b81ccb4ee3 fix: tab on current input field to focus the next input field (#3754) 2024-01-23 22:21:23 +05:30
Nivedin
27d0a7c437 refactor: persist running requests while switching tabs (#3742) 2024-01-23 22:13:57 +05:30
Nivedin
aca96dd5f2 refactor: add option to disable context menu (#3717) 2024-01-23 22:05:05 +05:30
Anwarul Islam
c0dbcc901f fix: documentation is not being generated on GQL (#3730)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2024-01-23 22:00:41 +05:30
Joel Jacob Stephen
ba52c8cc37 refactor: improvements to the dashboard sidebar (#3709)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:55:42 +05:30
Dmitry
d1f6f40ef8 fix: perform logout if the silent refresh attempt fails (#3705)
Co-authored-by: Dmitry Mukovkin <d.mukovkin@cft.ru>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:53:59 +05:30
Akash K
99f5070f71 fix: unwanted double slashes when importing from openapi (#3745) 2024-01-23 21:49:34 +05:30
Andrew Bastin
cd371fc9d4 chore: bump versions to 2023.12.2 2024-01-03 16:58:51 +05:30
Jordi Been
59fef248c0 build: update node alpine version (#3660) 2024-01-03 16:49:51 +05:30
Andrew Bastin
286fcd2bb0 chore: bump selfhost-desktop version to 23.12.1-1 2023-12-24 13:45:41 +05:30
Andrew Bastin
b2d98f7b66 chore: remove windi from selfhost-desktop 2023-12-24 13:21:12 +05:30
James George
c6c220091a feat(common): support importing environments individually (#3691) 2023-12-24 12:12:02 +05:30
Andrew Bastin
8f503479b6 chore: bump package version to 2023.12.1 2023-12-22 20:49:21 +05:30
Mir Arif Hasan
54d8378ccf fix: improve smtp email validation and fix enableAndDisableSSO mutation (#3689)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-12-22 20:37:15 +05:30
James George
0df194f9c5 fix(cli): environment resolution in the single-entry export format (#3687) 2023-12-22 19:21:33 +05:30
Liyas Thomas
ddf7eb6ad6 chore: minor ui improvements 2023-12-20 18:30:16 +05:30
Nivedin
7db7b9b068 fix: fallback section for embeds if invalid link (#3673) 2023-12-19 18:37:44 +05:30
James George
3d25ef48d1 fix(persistence-service): update schemas found to differ in runtime (#3671) 2023-12-19 18:34:27 +05:30
Mir Arif Hasan
4f138beb8a chore: db migration missing message (#3672) 2023-12-19 18:42:00 +06:00
James George
3d7a76bced chore(common): Gist export flow updates (#3665) 2023-12-19 17:37:35 +05:30
Nivedin
74359ea74e fix: auth-header not inheriting properties (#3668) 2023-12-19 17:04:24 +05:30
Mir Arif Hasan
a694d3f7eb hotfix: added validation on infra config update (#3667)
* feat: added validation on infra config update

* chore: removed async keyword

* fix: feedback
2023-12-19 17:15:46 +06:00
Nivedin
58a9514b67 fix: gql history schema error (#3662) 2023-12-19 16:39:32 +05:30
Akash K
a75bfa9d9e fix: actions not working when sidebar is hidden (#3669) 2023-12-19 16:13:59 +05:30
Andrew Bastin
7374a35b41 fix: broken ui due to accidentally moved postcss config 2023-12-19 12:40:07 +05:30
Andrew Bastin
5ad8f6c2ce chore: move window.open to platform io to handle desktop app 2023-12-19 11:26:37 +05:30
Andrew Bastin
f28298afe7 chore: update desktop app version 2023-12-18 23:43:40 +05:30
Andrew Bastin
56c6e8c643 chore: add devtools for production desktop app builds 2023-12-18 23:43:40 +05:30
Andrew Bastin
1b36de4fa3 fix: handle backspace navigating back on desktop app 2023-12-18 23:43:40 +05:30
Andrew Bastin
2f773bec79 fix: drag not working on windows 2023-12-18 23:43:40 +05:30
Andrew Bastin
d3e04c59cc chore: update tailwind stuff on selfhost-desktop 2023-12-18 23:43:40 +05:30
James George
5179cf59a4 fix(common): ensure the add-environment modal value field is empty when opened via the inspector (#3664) 2023-12-18 20:39:23 +05:30
Andrew Bastin
fad31a47ee chore: bump versions of all relevant packages 2023-12-16 22:27:41 +05:30
James George
72c71ddbd4 chore: remove expand widget from the GQL collections import/export modal (#3661) 2023-12-16 17:06:51 +05:30
Nivedin
a0f5ebee39 fix: embeds system theme (#3659)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-16 17:01:23 +05:30
Anwarul Islam
f93558324f chore: move hoppscotch-ui package out of the monorepo (#3620)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-12-16 16:58:10 +05:30
Liyas Thomas
d80e6c01c8 chore: i18n 2023-12-16 12:29:44 +05:30
Liyas Thomas
06f0f1c91b fix: broken docs link 2023-12-16 10:36:54 +05:30
Joel Jacob Stephen
9b870f876a chore: banner cleanup (#3658) 2023-12-15 17:59:33 +05:30
Joel Jacob Stephen
cf8b5975ac refactor: improvements made to how banners are to be dismissed (#3656) 2023-12-15 17:08:57 +05:30
Nivedin
93082c3816 refactor: embeds preview theme (#3657) 2023-12-15 17:08:02 +05:30
James George
d66537ac34 fix(common): GraphQL query syntax highlighting (#3653)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-12-15 14:37:01 +05:30
Nivedin
fc4c15e52d fix: auth-headers in collection bug (#3652)
* fix: fallback for importers and fix spelling

* chore: update i18n strings
2023-12-15 02:49:35 +05:30
Liyas Thomas
b521604b66 fix: collection properties styles 2023-12-14 18:00:46 +05:30
James George
9bc81a6d67 feat(cli): support collection level authorization and headers (#3636) 2023-12-14 12:43:22 +05:30
Joel Jacob Stephen
c47e2e7767 fix: notify that the user is not an admin when trying to login with a non admin account in admin dashboard (#3651) 2023-12-14 12:36:05 +05:30
Akash K
5209c0a8ca feat: dynamically load enabled auth providers (#3646) 2023-12-13 23:38:21 +05:30
Nivedin
47e009267b feat: collection level headers and authorization (#3505)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-12-13 22:43:18 +05:30
Joel Jacob Stephen
f3edd001d7 feat: introducing server configurations in admin dashboard (#3628)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-13 22:34:59 +05:30
Akash K
a8cc569786 feat: import environments from insomnia (#3625)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-13 19:15:39 +05:30
Liyas Thomas
3ae49ca483 fix: codemirror tooltip margin 2023-12-13 12:13:34 +05:30
Liyas Thomas
37e6497e88 fix: z-index 2023-12-13 11:02:56 +05:30
Liyas Thomas
b522ae9e05 fix: shortcut key size 2023-12-13 10:17:22 +05:30
Liyas Thomas
62b11fcec8 fix: z-index on toast 2023-12-13 09:22:58 +05:30
Liyas Thomas
51ebb57623 fix: z-index 2023-12-13 08:45:30 +05:30
Liyas Thomas
ff5c2ba51c chore: minor ui improvements to modal 2023-12-12 23:32:42 +05:30
Mir Arif Hasan
6abc0e6071 HBE-326 feature: server configuration through GraphQL API (#3591)
* feat: restart cmd added in aio service

* feat: nestjs config package added

* test: fix all broken test case

* feat: infra config module add with get-update-reset functionality

* test: fix test case failure

* feat: update infra configs mutation added

* feat: utilise ConfigService in util functions

* chore: remove saml stuff

* feat: removed saml stuffs

* fix: config service precedence

* fix: mailer module init with right env value

* feat: added mutations and query

* feat: add query infra-configs

* fix: mailer module init issue

* chore: smtp url validation added

* fix: all sso disabling is handled

* fix: pnpm i without db connection

* fix: allowedAuthProviders and enableAndDisableSSO

* fix: validateSMTPUrl check

* feat: get api added for fetch provider list

* feat: feedback resolve

* chore: update code comments

* fix: uppercase issue of VITE_ALLOWED_AUTH_PROVIDERS

* chore: update lockfile

* fix: add validation checks for MAILER_ADDRESS_FROM

* test: fix test case

* chore: feedback resolve

* chore: renamed an enum

* chore: app shutdown way changed

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-12-12 16:42:58 +06:00
Andrew Bastin
957641fb0f chore: move dioc out of monorepo 2023-12-12 15:39:10 +05:30
Liyas Thomas
a55f214102 fix: use base url instead of hardcoded url (#3635) 2023-12-12 15:06:28 +05:30
Liyas Thomas
ebf90207e5 chore: improve placeholder component styles (#3638)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-12 15:02:42 +05:30
Liyas Thomas
4ac8a117ef chore: add protocol logos to realtime page (#3637) 2023-12-12 14:25:56 +05:30
502 changed files with 18667 additions and 14044 deletions

5
.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -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: .

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,141 +0,0 @@
# dioc
A small and lightweight dependency injection / inversion of control system.
### About
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
### Demo
```ts
import { Service, Container } from "dioc"
// Here is a simple service, which you can define by extending the Service class
// and providing an ID static field (of type string)
export class PersistenceService extends Service {
// This should be unique for each container
public static ID = "PERSISTENCE_SERVICE"
public read(key: string): string | undefined {
// ...
}
public write(key: string, value: string) {
// ...
}
}
type TodoServiceEvent =
| { type: "TODO_CREATED"; index: number }
| { type: "TODO_DELETED"; index: number }
// Services have a built in event system
// Define the generic argument to say what are the possible emitted values
export class TodoService extends Service<TodoServiceEvent> {
public static ID = "TODO_SERVICE"
// Inject persistence service into this service
private readonly persistence = this.bind(PersistenceService)
public todos = []
// Service constructors cannot have arguments
constructor() {
super()
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
}
public addTodo(text: string) {
// ...
// You can access services via the bound fields
this.persistence.write("todos", JSON.stringify(this.todos))
// This is how you emit an event
this.emit({
type: "TODO_CREATED",
index,
})
}
public removeTodo(index: number) {
// ...
this.emit({
type: "TODO_DELETED",
index,
})
}
}
// Services need a container to run in
const container = new Container()
// You can initialize and get services using Container#bind
// It will automatically initialize the service (and its dependencies)
const todoService = container.bind(TodoService) // Returns an instance of TodoService
```
### Demo (Unit Test)
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
```ts
import { TestContainer } from "dioc/testing"
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
import { describe, it, expect, vi } from "vitest"
describe("TodoService", () => {
it("addTodo writes to persistence", () => {
const container = new TestContainer()
const writeFn = vi.fn()
// The first parameter is the service to mock and the second parameter
// is the mocked service fields and functions
container.bindMock(PersistenceService, {
read: () => undefined, // Not really important for this test
write: writeFn,
})
// the peristence service bind in TodoService will now use the
// above defined mocked implementation
const todoService = container.bind(TodoService)
todoService.addTodo("sup")
expect(writeFn).toHaveBeenCalledOnce()
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
})
})
```
### Demo (Vue)
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
In the app entry point:
```ts
import { createApp } from "vue"
import { diocPlugin } from "dioc/vue"
const app = createApp()
app.use(diocPlugin, {
container: new Container(), // You can pass in the container you want to provide to the components here
})
```
In your Vue components:
```vue
<script setup>
import { TodoService } from "./demo.ts" // The above demo
import { useService } from "dioc/vue"
const todoService = useService(TodoService) // Returns an instance of the TodoService class
</script>
```

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/main.d.ts"
export * from "./dist/main.d.ts"

View File

@@ -1,147 +0,0 @@
import { Service } from "./service"
import { Observable, Subject } from 'rxjs'
/**
* Stores the current container instance in the current operating context.
*
* NOTE: This should not be used outside of dioc library code
*/
export let currentContainer: Container | null = null
/**
* The events emitted by the container
*
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
* `SERVICE_INIT` - emitted when a service is initialized
*/
export type ContainerEvent =
| {
type: 'SERVICE_BIND';
/** The Service ID of the service being bounded (the dependency) */
boundeeID: string;
/**
* The Service ID of the bounder that is binding the boundee (the dependent)
*
* NOTE: This will be undefined if the service is bound directly to the container
*/
bounderID: string | undefined
}
| {
type: 'SERVICE_INIT';
/** The Service ID of the service being initialized */
serviceID: string
}
/**
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
*/
export class Container {
/** Used during the `bind` operation to detect circular dependencies */
private bindStack: string[] = []
/** The map of bound services to their IDs */
protected boundMap = new Map<string, Service<unknown>>()
/** The RxJS observable representing the event stream */
protected event$ = new Subject<ContainerEvent>()
/**
* Returns whether a container has the given service bound
* @param service The service to check for
*/
public hasBound<
T extends typeof Service<any> & { ID: string }
>(service: T): boolean {
return this.boundMap.has(service.ID)
}
/**
* Returns the service bound to the container with the given ID or if not found, undefined.
*
* NOTE: This is an advanced method and should not be used as much as possible.
*
* @param serviceID The ID of the service to get
*/
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
return this.boundMap.get(serviceID)
}
/**
* Binds a service to the container. This is equivalent to marking a service as a dependency.
* @param service The class reference of a service to bind
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
*/
public bind<T extends typeof Service<any> & { ID: string }>(
service: T,
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
): InstanceType<T> {
// We need to store the current container in a variable so that we can restore it after the bind operation
const oldCurrentContainer = currentContainer;
currentContainer = this;
// If the service is already bound, return the existing instance
if (this.hasBound(service)) {
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
})
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
}
// Detect circular dependency and throw error
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
}
// Push the service ID onto the bind stack to detect circular dependencies
this.bindStack.push(service.ID)
// Initialize the service and emit events
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
const instance: Service<any> = new (service as any)()
this.boundMap.set(service.ID, instance)
this.bindStack.pop()
this.event$.next({
type: 'SERVICE_INIT',
serviceID: service.ID,
})
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID
})
// Restore the current container
currentContainer = oldCurrentContainer;
// We expect the return type to match the service definition
return instance as InstanceType<T>
}
/**
* Returns an iterator of the currently bound service IDs and their instances
*/
public getBoundServices(): IterableIterator<[string, Service<any>]> {
return this.boundMap.entries()
}
/**
* Returns the public container event stream
*/
public getEventStream(): Observable<ContainerEvent> {
return this.event$.asObservable()
}
}

View File

@@ -1,2 +0,0 @@
export * from "./container"
export * from "./service"

View File

@@ -1,65 +0,0 @@
import { Observable, Subject } from 'rxjs'
import { Container, currentContainer } from './container'
/**
* A Dioc service that can bound to a container and can bind dependency services.
*
* NOTE: Services cannot have a constructor that takes arguments.
*
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
*/
export abstract class Service<EventDef = {}> {
/**
* The internal event stream of the service
*/
private event$ = new Subject<EventDef>()
/** The container the service is bound to */
#container: Container
constructor() {
if (!currentContainer) {
throw new Error(
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
)
}
this.#container = currentContainer
}
/**
* Binds a dependency service into this service.
* @param service The class reference of the service to bind
*/
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
if (!currentContainer) {
throw new Error('No currentContainer defined.')
}
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
}
/**
* Returns the container the service is bound to
*/
protected getContainer(): Container {
return this.#container
}
/**
* Emits an event on the service's event stream
* @param event The event to emit
*/
protected emit(event: EventDef) {
this.event$.next(event)
}
/**
* Returns the event stream of the service
*/
public getEventStream(): Observable<EventDef> {
return this.event$.asObservable()
}
}

View File

@@ -1,33 +0,0 @@
import { Container, Service } from "./main";
/**
* A container that can be used for writing tests, contains additional methods
* for binding suitable for writing tests. (see `bindMock`).
*/
export class TestContainer extends Container {
/**
* Binds a mock service to the container.
*
* @param service
* @param mock
*/
public bindMock<
T extends typeof Service<any> & { ID: string },
U extends Partial<InstanceType<T>>
>(service: T, mock: U): U {
if (this.boundMap.has(service.ID)) {
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
}
this.boundMap.set(service.ID, mock as any)
this.event$.next({
type: "SERVICE_BIND",
boundeeID: service.ID,
bounderID: undefined,
})
return mock
}
}

View File

@@ -1,34 +0,0 @@
import { Plugin, inject } from "vue"
import { Container } from "./container"
import { Service } from "./service"
const VUE_CONTAINER_KEY = Symbol()
// TODO: Some Vue version issue with plugin generics is breaking type checking
/**
* The Vue Dioc Plugin, this allows the composables to work and access the container
*
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
*/
export const diocPlugin: Plugin = {
install(app, { container }) {
app.provide(VUE_CONTAINER_KEY, container)
}
}
/**
* A composable that binds a service to a Vue Component
*
* @param service The class reference of the service to bind
*/
export function useService<
T extends typeof Service<any> & { ID: string }
>(service: T): InstanceType<T> {
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
if (!container) {
throw new Error("Container not found, did you forget to install the dioc plugin?")
}
return container.bind(service)
}

View File

@@ -1,54 +0,0 @@
{
"name": "dioc",
"private": true,
"version": "0.1.0",
"type": "module",
"files": [
"dist",
"index.d.ts"
],
"main": "./dist/counter.umd.cjs",
"module": "./dist/counter.js",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./dist/main.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js"
},
"./vue": {
"types": "./dist/vue.d.ts",
"require": "./dist/vue.cjs",
"import": "./dist/vue.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"require": "./dist/testing.cjs",
"import": "./dist/testing.js"
}
},
"scripts": {
"dev": "vite",
"build": "vite build && tsc --emitDeclarationOnly",
"prepare": "pnpm run build",
"test": "vitest run",
"do-test": "pnpm run test",
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vitest": "^0.29.3"
},
"dependencies": {
"rxjs": "^7.8.1"
},
"peerDependencies": {
"vue": "^3.2.25"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
}

View File

@@ -1,262 +0,0 @@
import { it, expect, describe, vi } from "vitest"
import { Service } from "../lib/service"
import { Container, currentContainer, ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
}
describe("Container", () => {
describe("getBoundServiceWithID", () => {
it("returns the service instance if it is bound to the container", () => {
const container = new Container()
const service = container.bind(TestServiceA)
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
})
it("returns undefined if the service is not bound to the container", () => {
const container = new Container()
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
})
})
describe("bind", () => {
it("correctly binds the service to it", () => {
const container = new Container()
const service = container.bind(TestServiceA)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(service.getContainer()).toBe(container)
})
it("after bind, the current container is set back to its previous value", () => {
const originalValue = currentContainer
const container = new Container()
container.bind(TestServiceA)
expect(currentContainer).toBe(originalValue)
})
it("dependent services are registered in the same container", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(serviceB.serviceA.getContainer()).toBe(container)
})
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceA2 = container.bind(TestServiceA)
expect(serviceA).toBe(serviceA2)
})
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceA = container.bind(TestServiceA)
expect(serviceB.serviceA).toBe(serviceA)
})
it("binding an initialized service as a dependency returns the same instance", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
const instance = container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
})
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
// We only care about the bind event of TestServiceA
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
})
})
describe("hasBound", () => {
it("returns true if the given service is bound to the container", () => {
const container = new Container()
container.bind(TestServiceA)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
it("returns false if the given service is not bound to the container", () => {
const container = new Container()
expect(container.hasBound(TestServiceA)).toEqual(false)
})
it("returns true when the service is bound because it is a dependency of another service", () => {
const container = new Container()
container.bind(TestServiceB)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
})
describe("getEventStream", () => {
it("returns an observable which emits events correctly when services are initialized", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceB.ID,
})
})
it("returns an observable which emits events correctly when services are bound", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceB.ID,
bounderID: undefined,
})
})
})
describe("getBoundServices", () => {
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
const container = new Container()
const instanceB = container.bind(TestServiceB)
const instanceA = instanceB.serviceA
expect(Array.from(container.getBoundServices())).toEqual([
[TestServiceA.ID, instanceA],
[TestServiceB.ID, instanceB],
])
})
it("returns an empty iterator if no services are bound", () => {
const container = new Container()
expect(Array.from(container.getBoundServices())).toEqual([])
})
})
})

View File

@@ -1,66 +0,0 @@
import { describe, expect, it, vi } from "vitest"
import { Service, Container } from "../lib/main"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service<"test"> {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
public emitTestEvent() {
this.emit("test")
}
}
describe("Service", () => {
describe("constructor", () => {
it("throws an error if the service is initialized without a container", () => {
expect(() => new TestServiceA()).toThrowError(
"Tried to initialize service with no container (ID: TestServiceA)"
)
})
})
describe("bind", () => {
it("correctly binds the dependency service using the container", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
})
describe("getContainer", () => {
it("returns the container the service is bound to", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
expect(serviceA.getContainer()).toBe(container)
})
})
describe("getEventStream", () => {
it("returns the valid event stream of the service", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceFunc = vi.fn()
serviceB.getEventStream().subscribe(serviceFunc)
serviceB.emitTestEvent()
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith("test")
})
})
})

View File

@@ -1,92 +0,0 @@
import { describe, expect, it, vi } from "vitest"
import { TestContainer } from "../lib/testing"
import { Service } from "../lib/service"
import { ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
public test() {
return "real"
}
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// declared public to help with testing
public readonly serviceA = this.bind(TestServiceA)
public test() {
return this.serviceA.test()
}
}
describe("TestContainer", () => {
describe("bindMock", () => {
it("returns the fake service defined", () => {
const container = new TestContainer()
const fakeService = {
test: () => "fake",
}
const result = container.bindMock(TestServiceA, fakeService)
expect(result).toBe(fakeService)
})
it("new services bound to the container get the mock service", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(fakeServiceA)
})
it("container emits SERVICE_BIND event", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
const serviceFunc = vi.fn<[ContainerEvent, void]>()
container.getEventStream().subscribe((ev) => {
serviceFunc(ev)
})
container.bindMock(TestServiceA, fakeServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("throws if service already bound", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
expect(() => {
container.bindMock(TestServiceA, fakeServiceA)
}).toThrowError(
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
)
})
})
})

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/testing.d.ts"
export * from "./dist/testing.d.ts"

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["lib"]
}

View File

@@ -1,16 +0,0 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: {
index: './lib/main.ts',
vue: './lib/vue.ts',
testing: './lib/testing.ts',
},
},
rollupOptions: {
external: ['vue'],
}
},
})

View File

@@ -1,7 +0,0 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
}
})

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/vue.d.ts"
export * from "./dist/vue.d.ts"

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.8.4-1",
"version": "2023.12.5",
"description": "",
"author": "",
"private": true,
@@ -28,13 +28,14 @@
"@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/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",
@@ -56,7 +57,7 @@
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"prisma": "^4.16.2",
"prisma": "^5.8.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"

View File

@@ -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");

View File

@@ -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)
}

View File

@@ -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],

View File

@@ -16,6 +16,7 @@ import {
USER_ALREADY_INVITED,
} from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -27,6 +28,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 +41,7 @@ const adminService = new AdminService(
mockPrisma as any,
mockMailerService,
mockShortcodeService,
mockConfigService,
);
const invitedUsers: InvitedUsers[] = [

View File

@@ -25,6 +25,7 @@ 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';
@Injectable()
export class AdminService {
@@ -39,6 +40,7 @@ export class AdminService {
private readonly prisma: PrismaService,
private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
private readonly configService: ConfigService,
) {}
/**
@@ -79,7 +81,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) {

View File

@@ -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';
@@ -16,11 +23,21 @@ 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';
@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',
@@ -222,4 +239,76 @@ 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: '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;
}
}

View File

@@ -20,51 +20,69 @@ 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';
@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 +95,7 @@ import { AppController } from './app.controller';
TeamInvitationModule,
UserCollectionModule,
ShortcodeModule,
InfraConfigModule,
],
providers: [GQLComplexityPlugin],
controllers: [AppController],

View File

@@ -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,

View File

@@ -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,
};
}
}

View File

@@ -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',

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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(),
)
: [];

View File

@@ -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,
});
}

View File

@@ -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,
});

View File

@@ -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'),
});
}

View File

@@ -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,
});
}

View File

@@ -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'),
});
}

View File

@@ -28,6 +28,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
*/
@@ -644,3 +651,48 @@ 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';

View File

@@ -0,0 +1,106 @@
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';
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(',');
}

View File

@@ -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',
});

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { InfraConfigService } from './infra-config.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [InfraConfigService],
exports: [InfraConfigService],
})
export class InfraConfigModule {}

View File

@@ -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);
});
});
});

View File

@@ -0,0 +1,380 @@
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, 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();
}
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
},
{
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
value: process.env.GOOGLE_CLIENT_SECRET,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_ID,
value: process.env.GITHUB_CLIENT_ID,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
value: process.env.GITHUB_CLIENT_SECRET,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
value: process.env.MICROSOFT_CLIENT_ID,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
value: process.env.MICROSOFT_CLIENT_SECRET,
},
{
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: getConfiguredSSOProviders(),
},
];
return infraConfigDefaultObjs;
}
/**
* Initialize the 'infra_config' table with values from .env
* @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 = 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
* @returns InfraConfig model
*/
async update(
name: InfraConfigEnumForClient | InfraConfigEnum,
value: string,
) {
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 },
});
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 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(','),
);
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 = this.getDefaultInfraConfigs();
await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
});
await this.prisma.infraConfig.createMany({
data: infraConfigDefaultObjs,
});
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);
}
}

View 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;
}

View File

@@ -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(),
},
}),
],
};
}
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},
});

View File

@@ -0,0 +1,29 @@
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',
}
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',
}

View File

@@ -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(),
);

View File

@@ -1,6 +1,6 @@
{
"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",
"main": "dist/index.js",
@@ -60,6 +60,7 @@
"ts-jest": "^29.1.1",
"tsup": "^7.2.0",
"typescript": "^5.2.2",
"verzod": "^0.2.2",
"zod": "^3.22.4"
}
}

View File

@@ -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();
});

View File

@@ -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"
}
}
]

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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
}
]
}
]

View File

@@ -0,0 +1,16 @@
{
"id": 123,
"v": "1",
"name": "secret-envs",
"values": [
{
"key": "secretVar",
"secret": true
},
{
"key": "regularVar",
"secret": false,
"value": "regular-variable"
}
]
}

View File

@@ -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>>"
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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;
};

View File

@@ -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 = {

View File

@@ -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.

View File

@@ -1,21 +1,23 @@
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { bold } from "chalk";
import { log } from "console";
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
import round from "lodash/round";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
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,
@@ -69,13 +70,13 @@ export const collectionsRunner =
// Request processing initiated message.
log(WARN(`\nRunning: ${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(

View File

@@ -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}`);
},

View File

@@ -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[];
}

View File

@@ -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 },
});
}

View File

@@ -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 };
}

View File

@@ -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 {
@@ -516,9 +517,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;

View File

@@ -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 {

View File

@@ -78,12 +78,6 @@
"iso": "he-HE",
"name": "עִברִית"
},
{
"code": "hi",
"file": "hi.json",
"iso": "hi-HI",
"name": "हिन्दी"
},
{
"code": "hu",
"file": "hu.json",

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -1,5 +1,6 @@
{
"action": {
"add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Abbrechen",
"choose_file": "Datei auswählen",
@@ -10,6 +11,7 @@
"connect": "Verbinden",
"connecting": "Connecting",
"copy": "Kopieren",
"create": "Create",
"delete": "Löschen",
"disconnect": "Trennen",
"dismiss": "Verwerfen",
@@ -31,6 +33,7 @@
"open_workspace": "Open workspace",
"paste": "Einfügen",
"prettify": "Verschönern",
"properties": "Properties",
"remove": "Entfernen",
"rename": "Rename",
"restore": "Wiederherstellen",
@@ -39,6 +42,7 @@
"scroll_to_top": "Scroll to top",
"search": "Suchen",
"send": "Senden",
"share": "Share",
"start": "Start",
"starting": "Starting",
"stop": "Stopp",
@@ -57,7 +61,9 @@
"app": {
"chat_with_us": "Chatte mit uns",
"contact_us": "Kontaktiere uns",
"cookies": "Cookies",
"copy": "Kopieren",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Kopiere Nutzer-Authentisierungstoken",
"developer_option": "Entwickleroptionen",
"developer_option_description": "Entwicklungswerkzeuge, die helfen Hoppscotch weiter zu entwickeln und zu warten.",
@@ -73,6 +79,7 @@
"keyboard_shortcuts": "Tastaturkürzel",
"name": "Hoppscotch",
"new_version_found": "Neue Version gefunden. Zum Aktualisieren Seite neu laden.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options",
"proxy_privacy_policy": "Proxy-Datenschutzrichtlinie",
"reload": "Neu laden",
@@ -112,10 +119,27 @@
},
"authorization": {
"generate_token": "Token generieren",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "In URL einbinden",
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
"learn": "Dokumentation",
"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": "Übertragungsart",
"password": "Passwort",
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
"token": "Token",
"type": "Autorisierungsverfahren",
"username": "Nutzername"
@@ -124,6 +148,7 @@
"created": "Sammlung erstellt",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Sammlung bearbeiten",
"import_or_create": "Import or create a collection",
"invalid_name": "Bitte gib einen gültigen Namen für die Sammlung an",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
@@ -132,6 +157,8 @@
"name_length_insufficient": "Sammlungsname soll mindestens 3 Zeichen lang sein",
"new": "Neue Sammlung",
"order_changed": "Collection Order Updated",
"properties": "Collection Properties",
"properties_updated": "Collection Properties Updated",
"renamed": "Sammlung umbenannt",
"request_in_use": "Anfrage wird ausgeführt",
"save_as": "Speichern als",
@@ -151,6 +178,7 @@
"remove_folder": "Möchtest Du diesen Ordner wirklich dauerhaft löschen?",
"remove_history": "Möchtest Du wirklich den gesamten Verlauf dauerhaft löschen?",
"remove_request": "Möchtest Du diese Anfrage wirklich dauerhaft löschen?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Möchtest Du dieses Team wirklich löschen?",
"remove_telemetry": "Möchtest Du die Telemetrie wirklich deaktivieren?",
"request_change": "Möchtest Du diese Anfrage verwerfen? Ungespeicherte Änderungen gehen verloren.",
@@ -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": "Header {count}",
"message": "Nachricht {count}",
@@ -192,11 +238,13 @@
"profile": "Einloggen um das Profil anzusehen",
"protocols": "Protokolle sind leer",
"schema": "Verbinden mit einem GraphQL-Endpunkt",
"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": "Teamname leer",
"teams": "Teams sind leer",
"tests": "Es gibt keine Tests für diese Anfrage"
"tests": "Es gibt keine Tests für diese Anfrage",
"shortcodes": "Shortcodes are empty"
},
"environment": {
"add_to_global": "Zur Globalen Umgebung hinzufügen",
@@ -209,6 +257,7 @@
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"import_or_create": "Import or create a environment",
"invalid_name": "Bitte gib einen gültigen Namen für die Umgebung an",
"list": "Environment variables",
"my_environments": "My Environments",
@@ -232,8 +281,10 @@
"variable_list": "Variablenliste"
},
"error": {
"authproviders_load_error": "Unable to load auth providers",
"browser_support_sse": "Dieser Browser scheint keine Unterstützung für die vom Server gesendete Ereignisse zu haben.",
"check_console_details": "Einzelheiten findest Du in der Browser-Konsole.",
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL ist nicht richtig formatiert",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
@@ -249,9 +300,12 @@
"json_prettify_invalid_body": "Ein ungültiger Text konnte nicht verschönert werden, JSON-Syntaxfehler beheben und erneut versuchen",
"network_error": "Netzwerkfehler. Bitte versuche es erneut.",
"network_fail": "Anfrage konnte nicht gesendet werden",
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
"no_duration": "Keine Dauer",
"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": "Pre-Request-Skripte konnte nicht ausgeführt werden",
"something_went_wrong": "Etwas ist schief gelaufen",
@@ -260,6 +314,7 @@
"export": {
"as_json": "Als JSON exportieren",
"create_secret_gist": "Geheimen Github Gist erstellen",
"failed": "Something went wrong while exporting",
"gist_created": "Gist erstellt",
"require_github": "Melde Dich bei GitHub an, um einen geheimen Gist zu erstellen",
"title": "Export"
@@ -286,6 +341,9 @@
"subscriptions": "Abonnements",
"switch_connection": "Switch connection"
},
"graphql_collections": {
"title": "GraphQL Collections"
},
"group": {
"time": "Time",
"url": "URL"
@@ -297,6 +355,8 @@
},
"helpers": {
"authorization": "Der Autorisierungsheader wird automatisch generiert, wenn Du die Anfrage sendest.",
"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": "Zuerst Dokumentation erstellen",
"network_fail": "Der API-Endpunkt kann nicht erreicht werden. Überprüfe Deine Netzwerkverbindung und versuche es erneut.",
"offline": "Du scheinst offline zu sein. Die Daten in diesem Arbeitsbereich sind möglicherweise nicht aktuell.",
@@ -316,7 +376,10 @@
"import": {
"collections": "Sammlungen importieren",
"curl": "cURL importieren",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"failed": "Importieren fehlgeschlagen",
"from_file": "Import from File",
"from_gist": "Von Github Gist importieren",
"from_gist_description": "Von Github Gist URL importieren",
"from_insomnia": "Von Insomnia importieren",
@@ -331,11 +394,17 @@
"from_postman_description": "Postman-Sammlung importieren",
"from_url": "Von URL importieren",
"gist_url": "Gist-URL eingeben",
"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 Sammlungsdatei (JSON) importieren",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment from a JSON file",
"title": "Importieren"
},
"inspections": {
@@ -373,8 +442,10 @@
"close_unsaved_tab": "You have unsaved changes",
"collections": "Sammlungen",
"confirm": "Aktion bestätigen",
"customize_request": "Customize Request",
"edit_request": "Anfrage bearbeiten",
"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": "Link kopieren",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Dauer",
"enter_curl": "cURL eingeben",
"generate_code": "Code generieren",
"generated_code": "Generierter Code",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"header_list": "Header-Liste",
"invalid_name": "Bitte gib einen Namen für die Anfrage an",
"method": "Methode",
@@ -480,12 +552,14 @@
"saved": "Anfrage gespeichert",
"share": "Teilen",
"share_description": "Teile Hoppscotch mit Deinen Freunden",
"share_request": "Share Request",
"stop": "Stop",
"title": "Anfrage",
"type": "Anfragetyp",
"url": "URL",
"variables": "Variablen",
"view_my_links": "View my links"
"view_my_links": "View my links",
"copy_link": "Link kopieren"
},
"response": {
"audio": "Audio",
@@ -513,6 +587,7 @@
"account_description": "Passe Deine Kontoeinstellungen an.",
"account_email_description": "Deine primäre E-Mail-Adresse.",
"account_name_description": "Dies ist Dein Anzeigename.",
"additional": "Additional Settings",
"background": "Hintergrund",
"black_mode": "Schwarz",
"choose_language": "Sprache wählen",
@@ -559,14 +634,31 @@
"verified_email": "Verified email",
"verify_email": "E-Mail-Adresse bestätigen"
},
"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": "Anfragelink kopieren",
"delete_method": "DELETE-Methode auswählen",
"get_method": "GET-Methode auswählen",
"head_method": "HEAD-Methode auswählen",
@@ -611,8 +702,10 @@
"save_request": "Save Request",
"save_to_collections": "In Sammlungen speichern",
"send_request": "Anfrage senden",
"share_request": "Share Request",
"show_code": "Generate code snippet",
"title": "Anfrage"
"title": "Anfrage",
"copy_request_link": "Anfragelink kopieren"
},
"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": "In die Zwischenablage kopiert",
"deleted": "Gelöscht",
"deprecated": "VERALTET",
@@ -742,10 +836,12 @@
"disconnected": "Getrennt",
"disconnected_from": "Verbindung zu {name} getrennt",
"docs_generated": "Dokumentation erstellt",
"download_failed": "Download failed",
"download_started": "Download gestartet",
"enabled": "Aktiviert",
"file_imported": "Datei importiert",
"finished_in": "Fertig in {duration} ms",
"hide": "Hide",
"history_deleted": "Verlauf gelöscht",
"linewrap": "Zeilen umbrechen",
"loading": "Wird geladen...",
@@ -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": "Anfragen",
"query": "Anfrage",
"schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",
@@ -807,6 +905,7 @@
"email_do_not_match": "E-Mail-Adresse stimmt nicht mit Deinen Kontodaten überein, bitte kontaktiere den Teameigentümer.",
"exit": "Team verlassen",
"exit_disabled": "Eigentümer können das Team nicht verlassen",
"failed_invites": "Failed invites",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "E-Mail-Format ist ungültig",
"invalid_id": "Ungültige Team-ID, bitte kontaktiere den Teameigentümer.",
@@ -848,6 +947,7 @@
"same_target_destination": "Same target and destination",
"saved": "Team gespeichert",
"select_a_team": "Team auswählen",
"success_invites": "Success invites",
"title": "Team",
"we_sent_invite_link": "Einladungen wurden an alle E-Mails verschickt!",
"we_sent_invite_link_description": "Bitte alle eingeladenen Personen, ihren Posteingang zu überprüfen. Klicke auf den Link, um dem Team beizutreten."
@@ -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"
}
}

View File

@@ -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": "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": "Εργαλεία για προγραμματιστές που βοηθάνε στην ανάπτυξη και συντήρηση του Hoppscotch.",
@@ -73,6 +79,7 @@
"keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου",
"name": "Hoppscotch",
"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": "Collection name should be at least 3 characters long",
"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 είναι κενά",
"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 είναι κενά"
},
"environment": {
"add_to_global": "Προσθήκη στο 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": "Τα Περιβάλλοντα μου",
@@ -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": "Από ότι φαίνεται υπάρχει ένα σφάλμα δικτύου. Παρακαλούμε προσπαθήστε ξανά.",
"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": "Αυτή η σελίδα δεν βρέθηκε",
"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": "Εισαγωγή από 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": "Δεν μπορέσαμε να πάρουμε δεδομένα από το url",
"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": "Εισαγωγή συλλογών αρχείο JSON Hoppscotch Collections",
"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": "Κοινοποίηση Hoppscotch στους φίλους σου",
"share_request": "Share Request",
"stop": "Stop",
"title": "Αίτηση",
"type": "Τύπος αιτήματος",
"url": "URL",
"variables": "Μεταβλητές",
"view_my_links": "Προβολή των links μου"
"view_my_links": "Προβολή των links μου",
"copy_link": "Αντιγραφή συνδέσμου"
},
"response": {
"audio": "Audio",
@@ -513,6 +587,7 @@
"account_description": "Προσαρμόστε τις ρυθμίσεις του λογαριασμού σας.",
"account_email_description": "Η κύρια διεύθυνση email σας.",
"account_name_description": "Αυτό είναι το εμφανιζόμενο όνομά σας.",
"additional": "Additional Settings",
"background": "Ιστορικό",
"black_mode": "Μαύρος",
"choose_language": "Διάλεξε γλώσσα",
@@ -559,14 +634,31 @@
"verified_email": "Επαληθευμένο email",
"verify_email": "Επαλήθευση email"
},
"shortcodes": {
"actions": "Δράσεις",
"created_on": "Created on",
"deleted": "Το Shortcode διαγράφηκε",
"method": "Method",
"not_found": "Το 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": "Αντιγραφή συνδέσμου αιτήματος",
"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": "Αντιγραφή response στο πρόχειρο",
@@ -735,6 +828,7 @@
"connection_error": "Η Σύνδεση απέτυχε",
"connection_failed": "Αποτυχής Σύνδεση",
"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} ms",
"hide": "Hide",
"history_deleted": "Το ιστορικό διαγράφηκε",
"linewrap": "Τυλίξτε γραμμές",
"loading": "Φόρτωση...",
@@ -756,6 +852,7 @@
"published_error": "Κάτι πήγε στραβά κατα την αποστολή του μηνύματος: {topic} με θέμα: {message}",
"published_message": "Δημοσιευμένο Μηνυμα: {message} με θέμα: {topic}",
"reconnection_error": "Αποτυχία επανασύνδεσης",
"show": "Show",
"subscribed_failed": "Αποτυχία εγγραφής στο Θέμα: {topic}",
"subscribed_success": "Επιτυχία εγγραφής στο Θέμα: {topic}",
"unsubscribed_failed": "Αποτυχία απεγγραφής στο Θέμα: {topic}",
@@ -791,6 +888,7 @@
"queries": "Ερωτήματα",
"query": "Ερώτηση",
"schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Υποδοχή.IO",
"sse": "SSE",
"tests": "Δοκιμές",
@@ -807,6 +905,7 @@
"email_do_not_match": "Το Email δεν ταιριάζει με τις λεπτομέριες του προφιλ σας. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"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": "Ζητήστε από όλους όσους στείλατε πρόσκληση να ελέγξουν τα email τους. Click στον σύνδεσμο για εισαγωγή στην ομάδα."
@@ -879,5 +979,14 @@
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
},
"shortcodes": {
"actions": "Δράσεις",
"created_on": "Created on",
"deleted": "Το Shortcode διαγράφηκε",
"method": "Method",
"not_found": "Το Shortcode δεν βρέθηκε",
"short_code": "Short code",
"url": "URL"
}
}

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