Compare commits
1 Commits
fix/auth-h
...
add-realti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7517446f79 |
@@ -66,7 +66,6 @@ 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: .
|
||||
|
||||
24
packages/dioc/.gitignore
vendored
Normal file
24
packages/dioc/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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?
|
||||
141
packages/dioc/README.md
Normal file
141
packages/dioc/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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>
|
||||
```
|
||||
2
packages/dioc/index.d.ts
vendored
Normal file
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/main.d.ts"
|
||||
export * from "./dist/main.d.ts"
|
||||
147
packages/dioc/lib/container.ts
Normal file
147
packages/dioc/lib/container.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
2
packages/dioc/lib/main.ts
Normal file
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./container"
|
||||
export * from "./service"
|
||||
65
packages/dioc/lib/service.ts
Normal file
65
packages/dioc/lib/service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
33
packages/dioc/lib/testing.ts
Normal file
33
packages/dioc/lib/testing.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
34
packages/dioc/lib/vue.ts
Normal file
34
packages/dioc/lib/vue.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
54
packages/dioc/package.json
Normal file
54
packages/dioc/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
262
packages/dioc/test/container.spec.ts
Normal file
262
packages/dioc/test/container.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
66
packages/dioc/test/service.spec.ts
Normal file
66
packages/dioc/test/service.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
92
packages/dioc/test/test-container.spec.ts
Normal file
92
packages/dioc/test/test-container.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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 ?"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
2
packages/dioc/testing.d.ts
vendored
Normal file
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/testing.d.ts"
|
||||
export * from "./dist/testing.d.ts"
|
||||
21
packages/dioc/tsconfig.json
Normal file
21
packages/dioc/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
16
packages/dioc/vite.config.ts
Normal file
16
packages/dioc/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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'],
|
||||
}
|
||||
},
|
||||
})
|
||||
7
packages/dioc/vitest.config.ts
Normal file
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
}
|
||||
})
|
||||
2
packages/dioc/vue.d.ts
vendored
Normal file
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/vue.d.ts"
|
||||
export * from "./dist/vue.d.ts"
|
||||
@@ -28,7 +28,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "InfraConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"value" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedOn" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "InfraConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InfraConfig_name_key" ON "InfraConfig"("name");
|
||||
@@ -209,12 +209,3 @@ enum TeamMemberRole {
|
||||
VIEWER
|
||||
EDITOR
|
||||
}
|
||||
|
||||
model InfraConfig {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
value String?
|
||||
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -11,20 +12,19 @@ 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],
|
||||
|
||||
@@ -16,7 +16,6 @@ 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>();
|
||||
@@ -28,7 +27,6 @@ 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,
|
||||
@@ -41,7 +39,6 @@ const adminService = new AdminService(
|
||||
mockPrisma as any,
|
||||
mockMailerService,
|
||||
mockShortcodeService,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
const invitedUsers: InvitedUsers[] = [
|
||||
|
||||
@@ -25,7 +25,6 @@ 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 {
|
||||
@@ -40,7 +39,6 @@ export class AdminService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -81,7 +79,7 @@ export class AdminService {
|
||||
template: 'user-invitation',
|
||||
variables: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
||||
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
ID,
|
||||
Mutation,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { Infra } from './infra.model';
|
||||
import { AdminService } from './admin.service';
|
||||
@@ -23,21 +16,11 @@ 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,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
constructor(private adminService: AdminService) {}
|
||||
|
||||
@Query(() => Infra, {
|
||||
description: 'Fetch details of the Infrastructure',
|
||||
@@ -239,76 +222,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,69 +20,51 @@ 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: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [async () => loadInfraConfiguration()],
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
};
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
}),
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => [
|
||||
{
|
||||
ttl: +configService.get('RATE_LIMIT_TTL'),
|
||||
limit: +configService.get('RATE_LIMIT_MAX'),
|
||||
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,
|
||||
}),
|
||||
MailerModule.register(),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
},
|
||||
]),
|
||||
UserModule,
|
||||
AuthModule.register(),
|
||||
AuthModule,
|
||||
AdminModule,
|
||||
UserSettingsModule,
|
||||
UserEnvironmentsModule,
|
||||
@@ -95,7 +77,6 @@ import { MailerModule } from './mailer/mailer.module';
|
||||
TeamInvitationModule,
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
@@ -30,21 +31,11 @@ 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,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('providers')
|
||||
async getAuthProviders() {
|
||||
const providers = await this.authService.getAuthProviders();
|
||||
return { providers };
|
||||
}
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
/**
|
||||
** Route to initiate magic-link auth for a users email
|
||||
@@ -54,14 +45,8 @@ export class AuthController {
|
||||
@Body() authData: SignInMagicDto,
|
||||
@Query('origin') origin: string,
|
||||
) {
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.EMAIL,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
const deviceIdToken = await this.authService.signInMagicLink(
|
||||
authData.email,
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -11,47 +12,25 @@ 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.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
}),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
}),
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [AuthService, JwtStrategy, RTJwtStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
RTJwtStrategy,
|
||||
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -21,26 +21,15 @@ 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,
|
||||
mockConfigService,
|
||||
mockInfraConfigService,
|
||||
);
|
||||
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
@@ -102,8 +91,6 @@ describe('signInMagicLink', () => {
|
||||
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
||||
// create new entry in VerificationToken table
|
||||
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
||||
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
|
||||
mockConfigService.get.mockReturnValue('3');
|
||||
|
||||
const result = await authService.signInMagicLink(
|
||||
'dwight@dundermifflin.com',
|
||||
|
||||
@@ -28,8 +28,6 @@ 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 {
|
||||
@@ -38,8 +36,6 @@ export class AuthService {
|
||||
private prismaService: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly configService: ConfigService,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -50,12 +46,10 @@ export class AuthService {
|
||||
*/
|
||||
private async generateMagicLinkTokens(user: AuthUser) {
|
||||
const salt = await bcrypt.genSalt(
|
||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
||||
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
|
||||
);
|
||||
const expiresOn = DateTime.now()
|
||||
.plus({
|
||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
||||
})
|
||||
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
|
||||
.toISO()
|
||||
.toString();
|
||||
|
||||
@@ -101,13 +95,13 @@ export class AuthService {
|
||||
*/
|
||||
private async generateRefreshToken(userUid: string) {
|
||||
const refreshTokenPayload: RefreshTokenPayload = {
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
sub: userUid,
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
};
|
||||
|
||||
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
||||
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
|
||||
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
|
||||
});
|
||||
|
||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||
@@ -133,9 +127,9 @@ export class AuthService {
|
||||
*/
|
||||
async generateAuthTokens(userUid: string) {
|
||||
const accessTokenPayload: AccessTokenPayload = {
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
sub: userUid,
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
};
|
||||
|
||||
const refreshToken = await this.generateRefreshToken(userUid);
|
||||
@@ -143,7 +137,7 @@ export class AuthService {
|
||||
|
||||
return E.right(<AuthTokens>{
|
||||
access_token: await this.jwtService.sign(accessTokenPayload, {
|
||||
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
|
||||
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
|
||||
}),
|
||||
refresh_token: refreshToken.right,
|
||||
});
|
||||
@@ -224,14 +218,14 @@ export class AuthService {
|
||||
let url: string;
|
||||
switch (origin) {
|
||||
case Origin.ADMIN:
|
||||
url = this.configService.get('VITE_ADMIN_URL');
|
||||
url = process.env.VITE_ADMIN_URL;
|
||||
break;
|
||||
case Origin.APP:
|
||||
url = this.configService.get('VITE_BASE_URL');
|
||||
url = process.env.VITE_BASE_URL;
|
||||
break;
|
||||
default:
|
||||
// if origin is invalid by default set URL to Hoppscotch-App
|
||||
url = this.configService.get('VITE_BASE_URL');
|
||||
url = process.env.VITE_BASE_URL;
|
||||
}
|
||||
|
||||
await this.mailerService.sendEmail(email, {
|
||||
@@ -383,8 +377,4 @@ export class AuthService {
|
||||
|
||||
return E.right(<IsAdmin>{ isAdmin: false });
|
||||
}
|
||||
|
||||
getAuthProviders() {
|
||||
return this.infraConfigService.getAllowedAuthProviders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,14 @@ 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,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,14 @@ 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,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,31 +3,20 @@ 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,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||
throwHTTPErr({
|
||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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',
|
||||
@@ -46,17 +45,15 @@ export const authCookieHandler = (
|
||||
redirect: boolean,
|
||||
redirectUrl: string | null,
|
||||
) => {
|
||||
const configService = new ConfigService();
|
||||
|
||||
const currentTime = DateTime.now();
|
||||
const accessTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
||||
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
|
||||
})
|
||||
.toMillis();
|
||||
const refreshTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
||||
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
|
||||
})
|
||||
.toMillis();
|
||||
|
||||
@@ -78,12 +75,10 @@ export const authCookieHandler = (
|
||||
}
|
||||
|
||||
// check to see if redirectUrl is a whitelisted url
|
||||
const whitelistedOrigins = configService
|
||||
.get('WHITELISTED_ORIGINS')
|
||||
.split(',');
|
||||
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
|
||||
if (!whitelistedOrigins.includes(redirectUrl))
|
||||
// if it is not redirect by default to REDIRECT_URL
|
||||
redirectUrl = configService.get('REDIRECT_URL');
|
||||
redirectUrl = process.env.REDIRECT_URL;
|
||||
|
||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
};
|
||||
@@ -117,16 +112,13 @@ 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,
|
||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
||||
) {
|
||||
export function authProviderCheck(provider: string) {
|
||||
if (!provider) {
|
||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
provider.trim().toUpperCase(),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -5,20 +5,18 @@ 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: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
||||
scope: [configService.get('GITHUB_SCOPE')],
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||
scope: [process.env.GITHUB_SCOPE],
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,20 +5,18 @@ 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: 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(','),
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||
passReqToCallback: true,
|
||||
store: true,
|
||||
});
|
||||
|
||||
@@ -15,14 +15,10 @@ 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,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
constructor(private usersService: UserService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -33,7 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
return ATCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,19 @@ 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: 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'),
|
||||
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,
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,14 +14,10 @@ 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,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
constructor(private usersService: UserService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -32,7 +28,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
return RTCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -644,41 +644,3 @@ 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;
|
||||
|
||||
/**
|
||||
* Error message for when the database table does not exist
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const DATABASE_TABLE_NOT_EXIST =
|
||||
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/faq';
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
|
||||
export enum ServiceStatus {
|
||||
ENABLE = 'ENABLE',
|
||||
DISABLE = 'DISABLE',
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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',
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
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 {}
|
||||
@@ -1,109 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,312 +0,0 @@
|
||||
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,
|
||||
} from 'src/errors';
|
||||
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ServiceStatus, stopApp } from './helper';
|
||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||
|
||||
@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: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(),
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
providerInfo.forEach(({ provider, status }) => {
|
||||
if (status === ServiceStatus.ENABLE) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 = validateEmail(infraConfigs[i].value);
|
||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from './helper';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@InputType()
|
||||
export class InfraConfigArgs {
|
||||
@Field(() => InfraConfigEnumForClient, {
|
||||
description: 'Infra Config Name',
|
||||
})
|
||||
name: InfraConfigEnumForClient;
|
||||
|
||||
@Field({
|
||||
description: 'Infra Config Value',
|
||||
})
|
||||
value: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class EnableAndDisableSSOArgs {
|
||||
@Field(() => AuthProvider, {
|
||||
description: 'Auth Provider',
|
||||
})
|
||||
provider: AuthProvider;
|
||||
|
||||
@Field(() => ServiceStatus, {
|
||||
description: 'Auth Provider Status',
|
||||
})
|
||||
status: ServiceStatus;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { 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,42 +7,24 @@ 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: [],
|
||||
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(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [MailerService],
|
||||
exports: [MailerService],
|
||||
})
|
||||
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(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
export class MailerModule {}
|
||||
|
||||
@@ -6,23 +6,18 @@ 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('VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
);
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: configService.get('SESSION_SECRET'),
|
||||
secret: process.env.SESSION_SECRET,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -33,18 +28,18 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
if (configService.get('PRODUCTION') === 'false') {
|
||||
if (process.env.PRODUCTION === 'false') {
|
||||
console.log('Enabling CORS with development settings');
|
||||
|
||||
app.enableCors({
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
credentials: true,
|
||||
});
|
||||
} else {
|
||||
console.log('Enabling CORS with production settings');
|
||||
|
||||
app.enableCors({
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
credentials: true,
|
||||
});
|
||||
}
|
||||
@@ -52,13 +47,7 @@ async function bootstrap() {
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
app.use(cookieParser());
|
||||
await app.listen(configService.get('PORT') || 3170);
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.info('SIGTERM signal received');
|
||||
await app.close();
|
||||
});
|
||||
await app.listen(process.env.PORT || 3170);
|
||||
}
|
||||
|
||||
if (!process.env.GENERATE_GQL_SCHEMA) {
|
||||
|
||||
@@ -504,24 +504,20 @@ describe('ShortcodeService', () => {
|
||||
);
|
||||
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||
{
|
||||
id: shortcodesWithUserEmail[0].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[0].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[0].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[0].createdOn,
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: shortcodesWithUserEmail[1].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[1].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[1].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[1].createdOn,
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||
createdOn: shortcodes[1].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -11,7 +12,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
|
||||
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
|
||||
providers: [
|
||||
TeamInvitationService,
|
||||
TeamInvitationResolver,
|
||||
|
||||
@@ -20,7 +20,6 @@ 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 {
|
||||
@@ -29,8 +28,8 @@ export class TeamInvitationService {
|
||||
private readonly userService: UserService,
|
||||
private readonly teamService: TeamService,
|
||||
private readonly mailerService: MailerService,
|
||||
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -151,9 +150,7 @@ export class TeamInvitationService {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
|
||||
dbInvitation.id
|
||||
}`,
|
||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||
invite_team_name: team.name,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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',
|
||||
}
|
||||
@@ -131,26 +131,6 @@ export const validateEmail = (email: string) => {
|
||||
).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
|
||||
@@ -181,23 +161,21 @@ 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(
|
||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
||||
) {
|
||||
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
|
||||
export function checkEnvironmentAuthProvider() {
|
||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||
(provider) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||
',',
|
||||
).map((provider) => provider.toLocaleUpperCase());
|
||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||
(provider: string) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
import { ExecException } from "child_process";
|
||||
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `node ./bin/hopp test`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `node ./bin/hopp test notfound.json`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `node ./bin/hopp invalid-arg`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Some errors occured (exit code 1).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("fails.json")}`;
|
||||
const { error } = await execAsync(cmd);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
@@ -67,83 +66,75 @@ describe("Test 'hopp test <file>' command:", () => {
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("passes.json")}`;
|
||||
const { error } = await execAsync(cmd);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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 args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("ENV file not found.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env notfound.json`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
const { error, stdout } = await execAsync(cmd);
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
|
||||
test("No value passed to delay flag.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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 args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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 args = `${VALID_TEST_ARGS} --delay 1`;
|
||||
const { error } = await runCLI(args);
|
||||
const cmd = `${VALID_TEST_CMD} --delay 1`;
|
||||
const { error } = await execAsync(cmd);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,17 +1,10 @@
|
||||
import { exec } from "child_process";
|
||||
import { resolve } from "path";
|
||||
|
||||
import { ExecResponse } from "./types";
|
||||
|
||||
export const runCLI = (args: string): Promise<ExecResponse> =>
|
||||
{
|
||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||
const command = `node ${CLI_PATH} ${args}`
|
||||
|
||||
return new Promise((resolve) =>
|
||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
}
|
||||
export const execAsync = (command: string): Promise<ExecResponse> =>
|
||||
new Promise((resolve) =>
|
||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
|
||||
export const trimAnsi = (target: string) => {
|
||||
const ansiRegex =
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HoppCollection } from "@hoppscotch/data";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { HoppEnvs } from "./request";
|
||||
|
||||
export type CollectionRunnerParam = {
|
||||
collections: HoppCollection[];
|
||||
collections: HoppCollection<HoppRESTRequest>[];
|
||||
envs: HoppEnvs;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export type HoppEnvs = {
|
||||
|
||||
export type CollectionStack = {
|
||||
path: string;
|
||||
collection: HoppCollection;
|
||||
collection: HoppCollection<HoppRESTRequest>;
|
||||
};
|
||||
|
||||
export type RequestReport = {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
isHoppRESTRequest,
|
||||
} from "@hoppscotch/data";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { CommanderError } from "commander";
|
||||
import { HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
@@ -20,7 +24,9 @@ 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 => {
|
||||
export const isRESTCollection = (
|
||||
param: unknown
|
||||
): param is HoppCollection<HoppRESTRequest> => {
|
||||
if (!!param && typeof param === "object") {
|
||||
if (!hasProperty(param, "v") || typeof param.v !== "number") {
|
||||
return false;
|
||||
@@ -56,6 +62,7 @@ export const isRESTCollection = (param: unknown): param is HoppCollection => {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if given error data is of type HoppCLIError, based on existence
|
||||
* of code property.
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
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 { bold } from "chalk";
|
||||
import { log } from "console";
|
||||
import round from "lodash/round";
|
||||
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import {
|
||||
CollectionStack,
|
||||
HoppEnvs,
|
||||
ProcessRequestParams,
|
||||
CollectionStack,
|
||||
RequestReport,
|
||||
ProcessRequestParams,
|
||||
} from "../types/request";
|
||||
import {
|
||||
PreRequestMetrics,
|
||||
RequestMetrics,
|
||||
TestMetrics,
|
||||
} from "../types/response";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
getRequestMetrics,
|
||||
preProcessRequest,
|
||||
processRequest,
|
||||
} from "./request";
|
||||
import { exceptionColors } from "./getters";
|
||||
import {
|
||||
printErrorsReport,
|
||||
printFailedTestsReport,
|
||||
@@ -25,14 +23,15 @@ import {
|
||||
printRequestsMetrics,
|
||||
printTestsMetrics,
|
||||
} from "./display";
|
||||
import { exceptionColors } from "./getters";
|
||||
import { getPreRequestMetrics } from "./pre-request";
|
||||
import {
|
||||
getRequestMetrics,
|
||||
preProcessRequest,
|
||||
processRequest,
|
||||
} from "./request";
|
||||
PreRequestMetrics,
|
||||
RequestMetrics,
|
||||
TestMetrics,
|
||||
} from "../types/response";
|
||||
import { getTestMetrics } from "./test";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
import { getPreRequestMetrics } from "./pre-request";
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
|
||||
const { WARN, FAIL } = exceptionColors;
|
||||
|
||||
@@ -42,23 +41,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 as HoppRESTRequest, collection);
|
||||
const _request = preProcessRequest(request);
|
||||
const requestPath = `${path}/${_request.name}`;
|
||||
const processRequestParams: ProcessRequestParams = {
|
||||
path: requestPath,
|
||||
@@ -70,13 +69,13 @@ export const collectionsRunner = async (
|
||||
// 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;
|
||||
@@ -85,30 +84,15 @@ export const collectionsRunner = async (
|
||||
|
||||
// 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}/${updatedFolder.name}`,
|
||||
collection: updatedFolder,
|
||||
path: `${path}/${folder.name}`,
|
||||
collection: folder,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestsReport;
|
||||
};
|
||||
return requestsReport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms collections to generate collection-stack which describes each collection's
|
||||
@@ -116,7 +100,9 @@ export const collectionsRunner = async (
|
||||
* @param collections Hopp-collection objects to be mapped to collection-stack type.
|
||||
* @returns Mapped collections to collection-stack.
|
||||
*/
|
||||
const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
|
||||
const getCollectionStack = (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
): CollectionStack[] =>
|
||||
pipe(
|
||||
collections,
|
||||
A.map(
|
||||
|
||||
@@ -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 } from "@hoppscotch/data";
|
||||
import { HoppCollection, HoppRESTRequest } 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,24 +56,22 @@ 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,
|
||||
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
|
||||
*/
|
||||
export async function parseCollectionData(
|
||||
path: string
|
||||
): Promise<HoppCollection[]> {
|
||||
let contents = await readJsonFile(path);
|
||||
): Promise<HoppCollection<HoppRESTRequest>[]> {
|
||||
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[];
|
||||
}
|
||||
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
|
||||
};
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import axios, { Method } from "axios";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
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 * 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 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 { RequestRunnerResponse } from "../interfaces/response";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { preRequestScriptRunner } from "./pre-request";
|
||||
import {
|
||||
HoppEnvs,
|
||||
ProcessRequestParams,
|
||||
RequestReport,
|
||||
} from "../types/request";
|
||||
import { RequestMetrics } from "../types/response";
|
||||
import { responseErrors } from "./constants";
|
||||
import {
|
||||
printPreRequestRunner,
|
||||
printRequestRunner,
|
||||
printTestRunner,
|
||||
} from "./display";
|
||||
import { getDurationInSeconds, getMetaDataPairs } from "./getters";
|
||||
import { preRequestScriptRunner } from "./pre-request";
|
||||
import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { hrtime } from "process";
|
||||
import { RequestMetrics } from "../types/response";
|
||||
import { pipe } from "fp-ts/function";
|
||||
|
||||
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
|
||||
|
||||
@@ -309,12 +309,9 @@ export const processRequest =
|
||||
* @returns Updated request object free of invalid/missing data.
|
||||
*/
|
||||
export const preProcessRequest = (
|
||||
request: HoppRESTRequest,
|
||||
collection: HoppCollection,
|
||||
request: HoppRESTRequest
|
||||
): HoppRESTRequest => {
|
||||
const tempRequest = Object.assign({}, request);
|
||||
const { headers: parentHeaders, auth: parentAuth } = collection;
|
||||
|
||||
if (!tempRequest.v) {
|
||||
tempRequest.v = "1";
|
||||
}
|
||||
@@ -330,31 +327,18 @@ export const preProcessRequest = (
|
||||
if (!tempRequest.params) {
|
||||
tempRequest.params = [];
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!tempRequest.headers) {
|
||||
tempRequest.headers = [];
|
||||
}
|
||||
|
||||
if (!tempRequest.preRequestScript) {
|
||||
tempRequest.preRequestScript = "";
|
||||
}
|
||||
if (!tempRequest.testScript) {
|
||||
tempRequest.testScript = "";
|
||||
}
|
||||
|
||||
if (tempRequest.auth?.authType === "inherit") {
|
||||
tempRequest.auth = parentAuth;
|
||||
} else if (!tempRequest.auth) {
|
||||
if (!tempRequest.auth) {
|
||||
tempRequest.auth = { authActive: false, authType: "none" };
|
||||
}
|
||||
|
||||
if (!tempRequest.body) {
|
||||
tempRequest.body = { contentType: null, body: null };
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ a {
|
||||
@apply shadow-none #{!important};
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-7;
|
||||
@apply -mt-8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,6 @@ pre.ace_editor {
|
||||
|
||||
.toasted-container {
|
||||
@apply max-w-md;
|
||||
@apply z-[10000];
|
||||
|
||||
.toasted {
|
||||
&.toasted-primary {
|
||||
@@ -517,10 +516,9 @@ pre.ace_editor {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-0.5;
|
||||
@apply min-w-[1rem];
|
||||
@apply min-h-[1rem];
|
||||
@apply leading-none;
|
||||
@apply px-1;
|
||||
@apply min-w-[1.25rem];
|
||||
@apply min-h-[1.25rem];
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply border border-dividerDark;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
--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 {
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"properties":"Properties",
|
||||
"remove": "Remove",
|
||||
"rename": "Rename",
|
||||
"restore": "Restore",
|
||||
@@ -139,11 +138,9 @@
|
||||
"generate_token": "Generate Token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Include in URL",
|
||||
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
|
||||
"learn": "Learn how",
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Password",
|
||||
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
|
||||
"token": "Token",
|
||||
"type": "Authorization Type",
|
||||
"username": "Username",
|
||||
@@ -175,8 +172,6 @@
|
||||
"name_length_insufficient": "Collection name should be at least 3 characters long",
|
||||
"new": "New Collection",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"properties":"Collection Properties",
|
||||
"properties_updated": "Collection Properties Updated",
|
||||
"renamed": "Collection renamed",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Save as",
|
||||
@@ -310,8 +305,7 @@
|
||||
"proxy_error": "Proxy error",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script",
|
||||
"authproviders_load_error": "Unable to load auth providers"
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
},
|
||||
"export": {
|
||||
"as_json": "Export as JSON",
|
||||
@@ -360,8 +354,6 @@
|
||||
"offline_short": "You're using Hoppscotch offline.",
|
||||
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
||||
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
||||
"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.",
|
||||
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
||||
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
||||
"tests": "Write a test script to automate debugging."
|
||||
@@ -400,8 +392,7 @@
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
|
||||
"postman_environment_description": "Import Postman Environment JSON file",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"axios": "^1.6.2",
|
||||
"buffer": "^6.0.3",
|
||||
"cookie-es": "^1.0.0",
|
||||
"dioc": "^1.0.1",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.16.1",
|
||||
@@ -164,4 +164,4 @@
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ declare module 'vue' {
|
||||
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
||||
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
||||
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
||||
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin v-if="showLogin" @hide-modal="showLogin = false" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -17,10 +17,9 @@
|
||||
v-if="isEmpty(shortcutsResults)"
|
||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<details
|
||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||
v-else
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { computed } from "vue"
|
||||
import { graphqlCollectionStore } from "~/newstore/collections"
|
||||
|
||||
@@ -28,7 +28,7 @@ const pathFolders = computed(() => {
|
||||
.slice(0, -1)
|
||||
.map((x) => parseInt(x))
|
||||
|
||||
const pathItems: HoppCollection[] = []
|
||||
const pathItems: HoppCollection<HoppGQLRequest>[] = []
|
||||
|
||||
let currentFolder =
|
||||
graphqlCollectionStore.value.state[folderIndicies.shift()!]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed } from "vue"
|
||||
import { restCollectionStore } from "~/newstore/collections"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
@@ -36,7 +36,7 @@ const pathFolders = computed(() => {
|
||||
.slice(0, -1)
|
||||
.map((x) => parseInt(x))
|
||||
|
||||
const pathItems: HoppCollection[] = []
|
||||
const pathItems: HoppCollection<HoppRESTRequest>[] = []
|
||||
|
||||
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
|
||||
pathItems.push(currentFolder)
|
||||
|
||||
@@ -49,15 +49,13 @@
|
||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear')"
|
||||
outline
|
||||
@click="search = ''"
|
||||
/>
|
||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear')"
|
||||
outline
|
||||
@click="search = ''"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
@keyup.e="edit?.$el.click()"
|
||||
@keyup.delete="deleteAction?.$el.click()"
|
||||
@keyup.x="exportAction?.$el.click()"
|
||||
@keyup.p="propertiesAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
@@ -160,18 +159,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="propertiesAction"
|
||||
:icon="IconSettings2"
|
||||
:label="t('action.properties')"
|
||||
:shortcut="['P']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-properties')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
@@ -206,9 +193,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import IconSettings2 from "~icons/lucide/settings-2"
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
@@ -227,7 +213,7 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string
|
||||
parentID?: string | null
|
||||
data: HoppCollection | TeamCollection
|
||||
data: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
/**
|
||||
* Collection component can be used for both collections and folders.
|
||||
* folderType is used to determine which one it is.
|
||||
@@ -259,7 +245,6 @@ const emit = defineEmits<{
|
||||
(event: "add-request"): void
|
||||
(event: "add-folder"): void
|
||||
(event: "edit-collection"): void
|
||||
(event: "edit-properties"): void
|
||||
(event: "export-data"): void
|
||||
(event: "remove-collection"): void
|
||||
(event: "drop-event", payload: DataTransfer): void
|
||||
@@ -276,7 +261,6 @@ const edit = ref<HTMLButtonElement | null>(null)
|
||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||
const exportAction = ref<HTMLButtonElement | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const propertiesAction = ref<TippyComponent | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
@@ -310,8 +294,8 @@ const collectionIcon = computed(() => {
|
||||
})
|
||||
|
||||
const collectionName = computed(() => {
|
||||
if ((props.data as HoppCollection).name)
|
||||
return (props.data as HoppCollection).name
|
||||
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||
return (props.data as TeamCollection).title
|
||||
})
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { PropType, computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
@@ -87,7 +88,9 @@ const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
const handleImportToStore = async (collections: HoppCollection[]) => {
|
||||
const handleImportToStore = async (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
const importResult =
|
||||
props.collectionsType.type === "my-collections"
|
||||
? await importToPersonalWorkspace(collections)
|
||||
@@ -101,47 +104,26 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const importToPersonalWorkspace = (collections: HoppCollection[]) => {
|
||||
const importToPersonalWorkspace = (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
appendRESTCollections(collections)
|
||||
return E.right({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
function translateToTeamCollectionFormat(x: HoppCollection) {
|
||||
const folders: HoppCollection[] = (x.folders ?? []).map(
|
||||
translateToTeamCollectionFormat
|
||||
)
|
||||
|
||||
const data = {
|
||||
auth: x.auth,
|
||||
headers: x.headers,
|
||||
}
|
||||
|
||||
const obj = {
|
||||
...x,
|
||||
folders,
|
||||
data,
|
||||
}
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const importToTeamsWorkspace = async (collections: HoppCollection[]) => {
|
||||
const importToTeamsWorkspace = async (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
|
||||
return E.left({
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
|
||||
const transformedCollection = collections.map((collection) =>
|
||||
translateToTeamCollectionFormat(collection)
|
||||
)
|
||||
|
||||
const res = await toTeamsImporter(
|
||||
JSON.stringify(transformedCollection),
|
||||
JSON.stringify(collections),
|
||||
selectedTeamID.value
|
||||
)()
|
||||
|
||||
@@ -425,6 +407,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
||||
},
|
||||
action: async () => {
|
||||
isHoppTeamCollectionExporterInProgress.value = true
|
||||
|
||||
if (
|
||||
props.collectionsType.type === "team-collections" &&
|
||||
props.collectionsType.selectedTeam
|
||||
|
||||
@@ -71,13 +71,6 @@
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@edit-properties="
|
||||
node.data.type === 'collections' &&
|
||||
emit('edit-properties', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@export-data="
|
||||
node.data.type === 'collections' &&
|
||||
emit('export-data', node.data.data.data)
|
||||
@@ -146,13 +139,6 @@
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@edit-properties="
|
||||
node.data.type === 'folders' &&
|
||||
emit('edit-properties', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@export-data="
|
||||
node.data.type === 'folders' &&
|
||||
emit('export-data', node.data.data.data)
|
||||
@@ -268,7 +254,7 @@
|
||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
@@ -277,29 +263,27 @@
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
@@ -307,20 +291,18 @@
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
@@ -358,7 +340,7 @@ export type Collection = {
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: null
|
||||
data: HoppCollection
|
||||
data: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +349,7 @@ type Folder = {
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: string
|
||||
data: HoppCollection
|
||||
data: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +376,7 @@ type CollectionType =
|
||||
|
||||
const props = defineProps({
|
||||
filteredCollections: {
|
||||
type: Array as PropType<HoppCollection[]>,
|
||||
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
@@ -426,35 +408,28 @@ const emit = defineEmits<{
|
||||
event: "add-request",
|
||||
payload: {
|
||||
path: string
|
||||
folder: HoppCollection
|
||||
folder: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "add-folder",
|
||||
payload: {
|
||||
path: string
|
||||
folder: HoppCollection
|
||||
folder: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-collection",
|
||||
payload: {
|
||||
collectionIndex: string
|
||||
collection: HoppCollection
|
||||
collection: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-folder",
|
||||
payload: {
|
||||
folderPath: string
|
||||
folder: HoppCollection
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-properties",
|
||||
payload: {
|
||||
collectionIndex: string
|
||||
collection: HoppCollection
|
||||
folder: HoppCollection<HoppRESTRequest>
|
||||
}
|
||||
): void
|
||||
(
|
||||
@@ -472,7 +447,7 @@ const emit = defineEmits<{
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
(event: "export-data", payload: HoppCollection): void
|
||||
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void
|
||||
(event: "remove-collection", payload: string): void
|
||||
(event: "remove-folder", payload: string): void
|
||||
(
|
||||
@@ -686,10 +661,10 @@ const updateCollectionOrder = (
|
||||
type MyCollectionNode = Collection | Folder | Requests
|
||||
|
||||
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
constructor(public data: Ref<HoppCollection[]>) {}
|
||||
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {}
|
||||
|
||||
navigateToFolderWithIndexPath(
|
||||
collections: HoppCollection[],
|
||||
collections: HoppCollection<HoppRESTRequest>[],
|
||||
indexPaths: number[]
|
||||
) {
|
||||
if (indexPaths.length === 0) return null
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('collection.properties')"
|
||||
:full-width-body="true"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab :id="'headers'" :label="`${t('tab.headers')}`">
|
||||
<HttpHeaders
|
||||
v-model="editableCollection"
|
||||
:is-collection-property="true"
|
||||
@change-tab="changeOptionTab"
|
||||
/>
|
||||
<div
|
||||
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
|
||||
>
|
||||
<icon-lucide-info class="svg-icons mr-2" />
|
||||
{{ t("helpers.collection_properties_header") }}
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'authorization'"
|
||||
:label="`${t('tab.authorization')}`"
|
||||
>
|
||||
<HttpAuthorization
|
||||
v-model="editableCollection.auth"
|
||||
:is-collection-property="true"
|
||||
:is-root-collection="editingProperties?.isRootCollection"
|
||||
:inherited-properties="editingProperties?.inheritedProperties"
|
||||
/>
|
||||
<div
|
||||
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
|
||||
>
|
||||
<icon-lucide-info class="svg-icons mr-2" />
|
||||
{{ t("helpers.collection_properties_authorization") }}
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:loading="loadingState"
|
||||
outline
|
||||
@click="saveEditedCollection"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { RESTOptionTabs } from "../http/RequestOptions.vue"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import { clone } from "lodash-es"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type EditingProperties = {
|
||||
collection: HoppCollection | TeamCollection | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties: HoppInheritedProperty | undefined
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
loadingState: boolean
|
||||
editingProperties: EditingProperties | null
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
loadingState: false,
|
||||
editingProperties: null,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "set-collection-properties", newCollection: any): void
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const editableCollection = ref({
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: false,
|
||||
},
|
||||
}) as any
|
||||
|
||||
const selectedOptionTab = ref("headers")
|
||||
|
||||
const changeOptionTab = (tab: RESTOptionTabs) => {
|
||||
selectedOptionTab.value = tab
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show && props.editingProperties?.collection) {
|
||||
editableCollection.value.auth = clone(
|
||||
props.editingProperties.collection.auth
|
||||
)
|
||||
editableCollection.value.headers = clone(
|
||||
props.editingProperties.collection.headers
|
||||
)
|
||||
} else {
|
||||
editableCollection.value = {
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const saveEditedCollection = () => {
|
||||
if (!props.editingProperties) return
|
||||
const finalCollection = clone(editableCollection.value)
|
||||
delete finalCollection.body
|
||||
const collection = {
|
||||
path: props.editingProperties.path,
|
||||
collection: {
|
||||
...props.editingProperties.collection,
|
||||
...finalCollection,
|
||||
},
|
||||
isRootCollection: props.editingProperties.isRootCollection,
|
||||
}
|
||||
emit("set-collection-properties", collection)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -74,7 +74,6 @@ import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
cascadeParentCollectionForHeaderAuth,
|
||||
editGraphqlRequest,
|
||||
editRESTRequest,
|
||||
saveGraphqlRequestAs,
|
||||
@@ -240,16 +239,6 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
`${picked.value.collectionIndex}`,
|
||||
"rest"
|
||||
)
|
||||
|
||||
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
@@ -277,16 +266,6 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
picked.value.folderPath,
|
||||
"rest"
|
||||
)
|
||||
|
||||
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
@@ -315,16 +294,6 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
picked.value.folderPath,
|
||||
"rest"
|
||||
)
|
||||
|
||||
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
@@ -409,16 +378,6 @@ const saveRequestAs = async () => {
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
picked.value.folderPath,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-folder") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -434,16 +393,6 @@ const saveRequestAs = async () => {
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
picked.value.folderPath,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-collection") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -459,16 +408,6 @@ const saveRequestAs = async () => {
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
`${picked.value.collectionIndex}`,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,13 +88,6 @@
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@edit-properties="
|
||||
node.data.type === 'collections' &&
|
||||
emit('edit-properties', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@export-data="
|
||||
node.data.type === 'collections' &&
|
||||
emit('export-data', node.data.data.data)
|
||||
@@ -166,13 +159,6 @@
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@edit-properties="
|
||||
node.data.type === 'folders' &&
|
||||
emit('edit-properties', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@export-data="
|
||||
node.data.type === 'folders' &&
|
||||
emit('export-data', node.data.data.data)
|
||||
@@ -252,7 +238,6 @@
|
||||
selectRequest({
|
||||
request: node.data.data.data.request,
|
||||
requestIndex: node.data.data.data.id,
|
||||
folderPath: getPath(node.id),
|
||||
})
|
||||
"
|
||||
@share-request="
|
||||
@@ -289,37 +274,33 @@
|
||||
:text="t('empty.collections')"
|
||||
@drop.stop
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="
|
||||
hasNoTeamAccess
|
||||
? null
|
||||
: emit('display-modal-import-export')
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="
|
||||
hasNoTeamAccess ? null : emit('display-modal-import-export')
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
@@ -328,20 +309,18 @@
|
||||
:text="t('empty.collections')"
|
||||
@drop.stop
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
@@ -467,13 +446,6 @@ const emit = defineEmits<{
|
||||
folder: TeamCollection
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-properties",
|
||||
payload: {
|
||||
collectionIndex: string
|
||||
collection: TeamCollection
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-request",
|
||||
payload: {
|
||||
@@ -504,7 +476,7 @@ const emit = defineEmits<{
|
||||
request: HoppRESTRequest
|
||||
requestIndex: string
|
||||
isActive: boolean
|
||||
folderPath: string
|
||||
folderPath?: string | undefined
|
||||
}
|
||||
): void
|
||||
(
|
||||
@@ -552,12 +524,6 @@ const emit = defineEmits<{
|
||||
(event: "display-modal-import-export"): void
|
||||
}>()
|
||||
|
||||
const getPath = (path: string) => {
|
||||
const pathArray = path.split("/")
|
||||
pathArray.pop()
|
||||
return pathArray.join("/")
|
||||
}
|
||||
|
||||
const teamCollectionsList = toRef(props, "teamCollectionList")
|
||||
|
||||
const hasNoTeamAccess = computed(
|
||||
@@ -614,7 +580,6 @@ const isActiveRequest = (requestID: string) => {
|
||||
const selectRequest = (data: {
|
||||
request: HoppRESTRequest
|
||||
requestIndex: string
|
||||
folderPath: string | null
|
||||
}) => {
|
||||
const { request, requestIndex } = data
|
||||
if (props.saveRequest) {
|
||||
@@ -627,7 +592,6 @@ const selectRequest = (data: {
|
||||
request: request,
|
||||
requestIndex: requestIndex,
|
||||
isActive: isActiveRequest(requestIndex),
|
||||
folderPath: data.folderPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,58 +32,58 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { makeCollection } from "@hoppscotch/data"
|
||||
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
|
||||
import { addGraphqlCollection } from "~/newstore/collections"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
setup() {
|
||||
return {
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null as string | null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewCollection() {
|
||||
if (!this.name) {
|
||||
this.toast.error(`${this.t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
addGraphqlCollection(
|
||||
makeCollection<HoppGQLRequest>({
|
||||
name: this.name,
|
||||
folders: [],
|
||||
requests: [],
|
||||
})
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
this.hideModal()
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
|
||||
const addNewCollection = () => {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
addGraphqlCollection(
|
||||
makeCollection({
|
||||
name: name.value,
|
||||
folders: [],
|
||||
requests: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
})
|
||||
)
|
||||
|
||||
hideModal()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: true,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: true,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('folder.new')"
|
||||
@close="hideModal"
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
@@ -32,49 +32,47 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
<script lang="ts">
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
folderPath?: string
|
||||
collectionIndex: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(
|
||||
e: "add-folder",
|
||||
v: {
|
||||
name: string
|
||||
path: string | undefined
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
folderPath: { type: String, default: null },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
},
|
||||
emits: ["hide-modal", "add-folder"],
|
||||
setup() {
|
||||
return {
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFolder() {
|
||||
if (!this.name) {
|
||||
this.toast.error(`${this.t("folder.name_length_insufficient")}`)
|
||||
return
|
||||
}
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
this.$emit("add-folder", {
|
||||
name: this.name,
|
||||
path: this.folderPath || `${this.collectionIndex}`,
|
||||
})
|
||||
|
||||
const addFolder = () => {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("folder.name_length_insufficient")}`)
|
||||
return
|
||||
}
|
||||
|
||||
emit("add-folder", {
|
||||
name: name.value,
|
||||
path: props.folderPath || `${props.collectionIndex}`,
|
||||
})
|
||||
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -128,21 +128,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="propertiesAction"
|
||||
:icon="IconSettings2"
|
||||
:label="t('action.properties')"
|
||||
:shortcut="['P']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-properties', {
|
||||
collectionIndex: String(collectionIndex),
|
||||
collection: collection,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
@@ -170,15 +155,7 @@
|
||||
@edit-folder="$emit('edit-folder', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@edit-properties="
|
||||
$emit('edit-properties', {
|
||||
collectionIndex: `${collectionIndex}/${String(index)}`,
|
||||
collection: folder,
|
||||
})
|
||||
"
|
||||
@select="$emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
@drop-request="$emit('drop-request', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in collection.requests"
|
||||
@@ -194,7 +171,6 @@
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
@@ -204,18 +180,16 @@
|
||||
:alt="`${t('empty.collection')}`"
|
||||
:text="t('empty.collection')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
emit('add-folder', {
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
emit('add-folder', {
|
||||
path: `${collectionIndex}`,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,24 +212,25 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconSettings2 from "~icons/lucide/settings-2"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { removeGraphqlCollection } from "~/newstore/collections"
|
||||
import {
|
||||
removeGraphqlCollection,
|
||||
moveGraphqlRequest,
|
||||
} from "~/newstore/collections"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
|
||||
const props = defineProps<{
|
||||
picked: Picked | null
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
// Whether the viewing context is related to picking (activates 'select' events)
|
||||
saveRequest: boolean
|
||||
collectionIndex: number | null
|
||||
collection: HoppCollection
|
||||
isFiltered: boolean
|
||||
}>()
|
||||
saveRequest: { type: Boolean, default: false },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
collection: { type: Object, default: () => ({}) },
|
||||
isFiltered: Boolean,
|
||||
})
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const toast = useToast()
|
||||
@@ -271,23 +246,7 @@ const emit = defineEmits<{
|
||||
(e: "add-request", i: any): void
|
||||
(e: "add-folder", i: any): void
|
||||
(e: "edit-folder", i: any): void
|
||||
(
|
||||
e: "edit-properties",
|
||||
payload: {
|
||||
collectionIndex: string | null
|
||||
collection: HoppCollection
|
||||
}
|
||||
): void
|
||||
(e: "edit-collection"): void
|
||||
(e: "select-request", i: any): void
|
||||
(
|
||||
e: "drop-request",
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
collectionIndex: number | null
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
// Template refs
|
||||
@@ -363,10 +322,6 @@ const dropEvent = ({ dataTransfer }: any) => {
|
||||
dragging.value = !dragging.value
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
collectionIndex: props.collectionIndex,
|
||||
})
|
||||
moveGraphqlRequest(folderPath, requestIndex, `${props.collectionIndex}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,14 +37,13 @@ import { ref, watch } from "vue"
|
||||
import { editGraphqlCollection } from "~/newstore/collections"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
editingCollectionIndex: number | null
|
||||
editingCollection: HoppCollection | null
|
||||
editingCollectionName: string
|
||||
}>()
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
editingCollection: { type: Object, default: () => ({}) },
|
||||
editingCollectionIndex: { type: Number, default: null },
|
||||
editingCollectionName: { type: String, default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
|
||||
@@ -32,47 +32,52 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { editGraphqlFolder } from "~/newstore/collections"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
folderPath?: string
|
||||
folder: any
|
||||
editingFolderName: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["hide-modal"])
|
||||
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingFolderName,
|
||||
(val) => {
|
||||
name.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const editFolder = () => {
|
||||
if (!name.value) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
editGraphqlFolder(props.folderPath, {
|
||||
...(props.folder as any),
|
||||
name: name.value,
|
||||
})
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
folder: { type: Object, default: () => ({}) },
|
||||
folderPath: { type: String, default: null },
|
||||
editingFolderName: { type: String, default: null },
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
setup() {
|
||||
return {
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editingFolderName(val) {
|
||||
this.name = val
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
editFolder() {
|
||||
if (!this.name) {
|
||||
this.toast.error(`${this.t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
editGraphqlFolder(this.folderPath, {
|
||||
...(this.folder as any),
|
||||
name: this.name,
|
||||
})
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.name = ""
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -32,55 +32,61 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
folderPath: { type: String, default: null },
|
||||
request: { type: Object as PropType<HoppGQLRequest>, default: () => ({}) },
|
||||
requestIndex: { type: Number, default: null },
|
||||
editingRequestName: { type: String, default: null },
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
setup() {
|
||||
return {
|
||||
toast: useToast(),
|
||||
t: useI18n(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
requestUpdateData: {
|
||||
name: null as any | null,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editingRequestName(val) {
|
||||
this.requestUpdateData.name = val
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveRequest() {
|
||||
if (!this.requestUpdateData.name) {
|
||||
this.toast.error(`${this.t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
folderPath?: string
|
||||
requestIndex: number | null
|
||||
request: HoppGQLRequest | null
|
||||
editingRequestName: string
|
||||
}>()
|
||||
// TODO: Type safety goes brrrr. Proper typing plz
|
||||
const requestUpdated = {
|
||||
...this.$props.request,
|
||||
name: this.$data.requestUpdateData.name || this.$props.request.name,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
editGraphqlRequest(this.folderPath, this.requestIndex, requestUpdated)
|
||||
|
||||
const requestUpdateData = ref({ name: null as string | null })
|
||||
|
||||
watch(
|
||||
() => props.editingRequestName,
|
||||
(val) => {
|
||||
requestUpdateData.value.name = val
|
||||
}
|
||||
)
|
||||
|
||||
const saveRequest = () => {
|
||||
if (!requestUpdateData.value.name) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
const requestUpdated = {
|
||||
...(props.request as any),
|
||||
name: requestUpdateData.value.name || (props.request as any).name,
|
||||
}
|
||||
|
||||
editGraphqlRequest(props.folderPath, props.requestIndex, requestUpdated)
|
||||
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
requestUpdateData.value = { name: null }
|
||||
emit("hide-modal")
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
hideModal() {
|
||||
this.requestUpdateData = { name: null }
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -10,25 +10,24 @@
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center justify-center cursor-pointer"
|
||||
<span
|
||||
class="flex cursor-pointer items-center justify-center px-4"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="pointer-events-none flex items-center justify-center px-4">
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||
@click="toggleShowChildren()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ folder.name ? folder.name : folder.title }}
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ folder.name ? folder.name : folder.title }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -121,21 +120,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="propertiesAction"
|
||||
:icon="IconSettings2"
|
||||
:label="t('action.properties')"
|
||||
:shortcut="['P']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-properties', {
|
||||
collectionIndex: collectionIndex,
|
||||
collection: collection,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
@@ -164,14 +148,7 @@
|
||||
@edit-folder="emit('edit-folder', $event)"
|
||||
@edit-request="emit('edit-request', $event)"
|
||||
@duplicate-request="emit('duplicate-request', $event)"
|
||||
@edit-properties="
|
||||
emit('edit-properties', {
|
||||
collectionIndex: `${folderPath}/${String(subFolderIndex)}`,
|
||||
collection: subFolder,
|
||||
})
|
||||
"
|
||||
@select="emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
/>
|
||||
<CollectionsGraphqlRequest
|
||||
v-for="(request, index) in folder.requests"
|
||||
@@ -187,7 +164,6 @@
|
||||
@edit-request="emit('edit-request', $event)"
|
||||
@duplicate-request="emit('duplicate-request', $event)"
|
||||
@select="emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
/>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
@@ -200,7 +176,8 @@
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
:text="t('empty.folder')"
|
||||
/>
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartConfirmModal
|
||||
@@ -221,16 +198,13 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import IconSettings2 from "~icons/lucide/settings-2"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { removeGraphqlFolder } from "~/newstore/collections"
|
||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||
import { computed, ref } from "vue"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -238,16 +212,16 @@ const colorMode = useColorMode()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
picked: Picked
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
// Whether the request is in a selectable mode (activates 'select' event)
|
||||
saveRequest: boolean
|
||||
folder: HoppCollection
|
||||
folderIndex: number
|
||||
collectionIndex: number
|
||||
folderPath: string
|
||||
isFiltered: boolean
|
||||
}>()
|
||||
saveRequest: { type: Boolean, default: false },
|
||||
folder: { type: Object, default: () => ({}) },
|
||||
folderIndex: { type: Number, default: null },
|
||||
collectionIndex: { type: Number, default: null },
|
||||
folderPath: { type: String, default: null },
|
||||
isFiltered: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
"select",
|
||||
@@ -256,9 +230,6 @@ const emit = defineEmits([
|
||||
"add-folder",
|
||||
"edit-folder",
|
||||
"duplicate-request",
|
||||
"edit-properties",
|
||||
"select-request",
|
||||
"drop-request",
|
||||
])
|
||||
|
||||
// Template refs
|
||||
@@ -333,11 +304,6 @@ const dropEvent = ({ dataTransfer }: any) => {
|
||||
dragging.value = !dragging.value
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
collectionIndex: props.folderPath,
|
||||
})
|
||||
moveGraphqlRequest(folderPath, requestIndex, props.folderPath)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||
@@ -25,14 +25,13 @@ import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
appendGraphqlCollections,
|
||||
graphqlCollections$,
|
||||
setGraphqlCollections,
|
||||
} from "~/newstore/collections"
|
||||
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
|
||||
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
|
||||
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
|
||||
import { computed } from "vue"
|
||||
import { hoppGQLImporter } from "~/helpers/import-export/import/hopp"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -61,20 +60,15 @@ const GqlCollectionsHoppImporter: ImporterOrExporter = {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
const validatedCollection = await hoppGQLImporter(
|
||||
JSON.stringify(res.right)
|
||||
)()
|
||||
|
||||
if (E.isRight(validatedCollection)) {
|
||||
handleImportToStore(validatedCollection.right)
|
||||
handleImportToStore(res.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
importer: "json",
|
||||
})
|
||||
}
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
importer: "json",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
@@ -220,9 +214,11 @@ const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
const handleImportToStore = async (gqlCollections: HoppCollection[]) => {
|
||||
appendGraphqlCollections(gqlCollections)
|
||||
toast.success(t("state.file_imported"))
|
||||
const handleImportToStore = async (
|
||||
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||
) => {
|
||||
setGraphqlCollections(gqlCollections)
|
||||
toast.success(t("import.success"))
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -9,41 +9,38 @@
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options.tippy.show()"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
|
||||
<span
|
||||
class="flex w-16 cursor-pointer items-center justify-center truncate px-2"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none flex w-8 items-center justify-center truncate px-6"
|
||||
>
|
||||
<component
|
||||
:is="isSelected ? IconCheckCircle : IconFile"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<component
|
||||
:is="isSelected ? IconCheckCircle : IconFile"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex min-w-0 flex-1 cursor-pointer items-center py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||
v-if="isActive"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
|
||||
:title="`${t('collection.request_in_use')}`"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="isActive"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
|
||||
:title="`${t('collection.request_in_use')}`"
|
||||
>
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
|
||||
></span>
|
||||
</span>
|
||||
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<span>
|
||||
<tippy
|
||||
@@ -137,7 +134,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { PropType, computed, ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
@@ -177,12 +175,7 @@ const isActive = computed(() => {
|
||||
})
|
||||
|
||||
// TODO: Better types please
|
||||
const emit = defineEmits([
|
||||
"select",
|
||||
"edit-request",
|
||||
"duplicate-request",
|
||||
"select-request",
|
||||
])
|
||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||
|
||||
const dragging = ref(false)
|
||||
const confirmRemove = ref(false)
|
||||
@@ -206,11 +199,36 @@ const selectRequest = () => {
|
||||
if (props.saveRequest) {
|
||||
pick()
|
||||
} else {
|
||||
emit("select-request", {
|
||||
request: props.request,
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
})
|
||||
|
||||
// Switch to that request if that request is open
|
||||
if (possibleTab) {
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
return
|
||||
}
|
||||
|
||||
tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
},
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: props.request.name,
|
||||
url: props.request.url,
|
||||
query: props.request.query,
|
||||
headers: props.request.headers,
|
||||
variables: props.request.variables,
|
||||
auth: props.request.auth,
|
||||
})
|
||||
),
|
||||
isDirty: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||
class="!border-0 bg-transparent py-2 pl-4 pr-2"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
||||
@@ -57,10 +57,7 @@
|
||||
@edit-request="editRequest($event)"
|
||||
@duplicate-request="duplicateRequest($event)"
|
||||
@select-collection="$emit('use-collection', collection)"
|
||||
@edit-properties="editProperties($event)"
|
||||
@select="$emit('select', $event)"
|
||||
@select-request="selectRequest($event)"
|
||||
@drop-request="dropRequest($event)"
|
||||
/>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
@@ -69,36 +66,34 @@
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<CollectionsGraphqlAdd
|
||||
@@ -145,27 +140,19 @@
|
||||
v-if="showModalImportExport"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
<CollectionsProperties
|
||||
:show="showModalEditProperties"
|
||||
:editing-properties="editingProperties"
|
||||
@hide-modal="displayModalEditProperties(false)"
|
||||
@set-collection-properties="setCollectionProperties"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from "vue"
|
||||
import { clone, cloneDeep } from "lodash-es"
|
||||
<script lang="ts">
|
||||
// TODO: TypeScript + Script Setup this :)
|
||||
import { defineComponent } from "vue"
|
||||
import { cloneDeep, clone } from "lodash-es"
|
||||
import {
|
||||
graphqlCollections$,
|
||||
addGraphqlFolder,
|
||||
saveGraphqlRequestAs,
|
||||
cascadeParentCollectionForHeaderAuth,
|
||||
editGraphqlCollection,
|
||||
editGraphqlFolder,
|
||||
moveGraphqlRequest,
|
||||
} from "~/newstore/collections"
|
||||
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
@@ -175,448 +162,213 @@ import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { computed } from "vue"
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppGQLRequest,
|
||||
makeGQLRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { getRequestsByPath } from "~/helpers/collection/request"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// Whether to activate the ability to pick items (activates 'select' events)
|
||||
saveRequest: { type: Boolean, default: false },
|
||||
picked: { type: Object, default: null },
|
||||
},
|
||||
emits: ["select", "use-collection"],
|
||||
setup() {
|
||||
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
defineProps<{
|
||||
// Whether to activate the ability to pick items (activates 'select' events)
|
||||
saveRequest: boolean
|
||||
picked: Picked
|
||||
}>()
|
||||
|
||||
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
|
||||
const colorMode = useColorMode()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const showModalAdd = ref(false)
|
||||
const showModalEdit = ref(false)
|
||||
const showModalImportExport = ref(false)
|
||||
const showModalAddRequest = ref(false)
|
||||
const showModalAddFolder = ref(false)
|
||||
const showModalEditFolder = ref(false)
|
||||
const showModalEditRequest = ref(false)
|
||||
const showModalEditProperties = ref(false)
|
||||
|
||||
const editingCollection = ref<HoppCollection | null>(null)
|
||||
const editingCollectionIndex = ref<number | null>(null)
|
||||
const editingFolder = ref<HoppCollection | null>(null)
|
||||
const editingFolderName = ref("")
|
||||
const editingFolderIndex = ref<number | null>(null)
|
||||
const editingFolderPath = ref("")
|
||||
const editingRequest = ref<HoppGQLRequest | null>(null)
|
||||
const editingRequestIndex = ref<number | null>(null)
|
||||
|
||||
const editingProperties = ref<{
|
||||
collection: HoppCollection | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>({
|
||||
collection: null,
|
||||
isRootCollection: false,
|
||||
path: "",
|
||||
inheritedProperties: undefined,
|
||||
})
|
||||
|
||||
const filterText = ref("")
|
||||
|
||||
const filteredCollections = computed(() => {
|
||||
const collectionsClone = clone(collections.value)
|
||||
|
||||
if (!filterText.value) return collectionsClone
|
||||
|
||||
const filterTextLower = filterText.value.toLowerCase()
|
||||
const filteredCollections = []
|
||||
|
||||
for (const collection of collectionsClone) {
|
||||
const filteredRequests = []
|
||||
const filteredFolders = []
|
||||
|
||||
for (const request of collection.requests) {
|
||||
if (request.name.toLowerCase().includes(filterTextLower))
|
||||
filteredRequests.push(request)
|
||||
return {
|
||||
collections,
|
||||
colorMode,
|
||||
t,
|
||||
tabs,
|
||||
IconPlus,
|
||||
IconHelpCircle,
|
||||
IconImport,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModalAdd: false,
|
||||
showModalEdit: false,
|
||||
showModalImportExport: false,
|
||||
showModalAddRequest: false,
|
||||
showModalAddFolder: false,
|
||||
showModalEditFolder: false,
|
||||
showModalEditRequest: false,
|
||||
editingCollection: undefined,
|
||||
editingCollectionIndex: undefined,
|
||||
editingFolder: undefined,
|
||||
editingFolderName: undefined,
|
||||
editingFolderIndex: undefined,
|
||||
editingFolderPath: undefined,
|
||||
editingRequest: undefined,
|
||||
editingRequestIndex: undefined,
|
||||
filterText: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredCollections() {
|
||||
const collections = clone(this.collections)
|
||||
|
||||
for (const folder of collection.folders) {
|
||||
const filteredFolderRequests = []
|
||||
for (const request of folder.requests) {
|
||||
if (request.name.toLowerCase().includes(filterTextLower))
|
||||
filteredFolderRequests.push(request)
|
||||
if (!this.filterText) return collections
|
||||
|
||||
const filterText = this.filterText.toLowerCase()
|
||||
const filteredCollections = []
|
||||
|
||||
for (const collection of collections) {
|
||||
const filteredRequests = []
|
||||
const filteredFolders = []
|
||||
for (const request of collection.requests) {
|
||||
if (request.name.toLowerCase().includes(filterText))
|
||||
filteredRequests.push(request)
|
||||
}
|
||||
for (const folder of collection.folders) {
|
||||
const filteredFolderRequests = []
|
||||
for (const request of folder.requests) {
|
||||
if (request.name.toLowerCase().includes(filterText))
|
||||
filteredFolderRequests.push(request)
|
||||
}
|
||||
if (filteredFolderRequests.length > 0) {
|
||||
const filteredFolder = Object.assign({}, folder)
|
||||
filteredFolder.requests = filteredFolderRequests
|
||||
filteredFolders.push(filteredFolder)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredRequests.length + filteredFolders.length > 0) {
|
||||
const filteredCollection = Object.assign({}, collection)
|
||||
filteredCollection.requests = filteredRequests
|
||||
filteredCollection.folders = filteredFolders
|
||||
filteredCollections.push(filteredCollection)
|
||||
}
|
||||
}
|
||||
if (filteredFolderRequests.length > 0) {
|
||||
const filteredFolder = { ...folder }
|
||||
filteredFolder.requests = filteredFolderRequests
|
||||
filteredFolders.push(filteredFolder)
|
||||
|
||||
return filteredCollections
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
displayModalAdd(shouldDisplay) {
|
||||
this.showModalAdd = shouldDisplay
|
||||
},
|
||||
displayModalEdit(shouldDisplay) {
|
||||
this.showModalEdit = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalImportExport(shouldDisplay) {
|
||||
this.showModalImportExport = shouldDisplay
|
||||
},
|
||||
displayModalAddRequest(shouldDisplay) {
|
||||
this.showModalAddRequest = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalAddFolder(shouldDisplay) {
|
||||
this.showModalAddFolder = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalEditFolder(shouldDisplay) {
|
||||
this.showModalEditFolder = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
displayModalEditRequest(shouldDisplay) {
|
||||
this.showModalEditRequest = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) this.resetSelectedData()
|
||||
},
|
||||
editCollection(collection, collectionIndex) {
|
||||
this.$data.editingCollection = collection
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.displayModalEdit(true)
|
||||
},
|
||||
onAddRequest({ name, path, index }) {
|
||||
const newRequest = {
|
||||
...this.tabs.currentActiveTab.value.document.request,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredRequests.length + filteredFolders.length > 0) {
|
||||
const filteredCollection = { ...collection }
|
||||
filteredCollection.requests = filteredRequests
|
||||
filteredCollection.folders = filteredFolders
|
||||
filteredCollections.push(filteredCollection)
|
||||
}
|
||||
}
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
|
||||
return filteredCollections
|
||||
})
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
showModalAdd.value = shouldDisplay
|
||||
}
|
||||
|
||||
const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
showModalEdit.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalImportExport = (shouldDisplay: boolean) => {
|
||||
showModalImportExport.value = shouldDisplay
|
||||
}
|
||||
|
||||
const displayModalAddRequest = (shouldDisplay: boolean) => {
|
||||
showModalAddRequest.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalAddFolder = (shouldDisplay: boolean) => {
|
||||
showModalAddFolder.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalEditFolder = (shouldDisplay: boolean) => {
|
||||
showModalEditFolder.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalEditRequest = (shouldDisplay: boolean) => {
|
||||
showModalEditRequest.value = shouldDisplay
|
||||
|
||||
if (!shouldDisplay) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalEditProperties = (show: boolean) => {
|
||||
showModalEditProperties.value = show
|
||||
|
||||
if (!show) resetSelectedData()
|
||||
}
|
||||
|
||||
const editCollection = (
|
||||
collection: HoppCollection,
|
||||
collectionIndex: number
|
||||
) => {
|
||||
editingCollection.value = collection
|
||||
editingCollectionIndex.value = collectionIndex
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
const onAddRequest = ({
|
||||
name,
|
||||
path,
|
||||
index,
|
||||
}: {
|
||||
name: string
|
||||
path: string
|
||||
index: number
|
||||
}) => {
|
||||
const newRequest = {
|
||||
...tabs.currentActiveTab.value.document.request,
|
||||
name,
|
||||
}
|
||||
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
path,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: index,
|
||||
},
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "gql",
|
||||
createdNow: true,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
displayModalAddRequest(false)
|
||||
}
|
||||
|
||||
const addRequest = (payload: { path: string }) => {
|
||||
const { path } = payload
|
||||
editingFolderPath.value = path
|
||||
displayModalAddRequest(true)
|
||||
}
|
||||
|
||||
const onAddFolder = ({
|
||||
name,
|
||||
path,
|
||||
}: {
|
||||
name: string
|
||||
path: string | undefined
|
||||
}) => {
|
||||
addGraphqlFolder(name, path ?? "0")
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: false,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
displayModalAddFolder(false)
|
||||
}
|
||||
|
||||
const addFolder = (payload: { path: string }) => {
|
||||
const { path } = payload
|
||||
editingFolderPath.value = path
|
||||
displayModalAddFolder(true)
|
||||
}
|
||||
|
||||
const editFolder = (payload: {
|
||||
folder: HoppCollection
|
||||
folderPath: string
|
||||
}) => {
|
||||
const { folder, folderPath } = payload
|
||||
editingFolder.value = folder
|
||||
editingFolderPath.value = folderPath
|
||||
displayModalEditFolder(true)
|
||||
}
|
||||
|
||||
const editRequest = (payload: {
|
||||
collectionIndex: number
|
||||
folderIndex: number
|
||||
folderName: string
|
||||
request: HoppGQLRequest
|
||||
requestIndex: number
|
||||
folderPath: string
|
||||
}) => {
|
||||
const {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
folderPath,
|
||||
} = payload
|
||||
editingFolderPath.value = folderPath
|
||||
editingCollectionIndex.value = collectionIndex
|
||||
editingFolderIndex.value = folderIndex
|
||||
editingFolderName.value = folderName
|
||||
editingRequest.value = request
|
||||
editingRequestIndex.value = requestIndex
|
||||
displayModalEditRequest(true)
|
||||
}
|
||||
|
||||
const duplicateRequest = ({
|
||||
folderPath,
|
||||
request,
|
||||
}: {
|
||||
folderPath: string
|
||||
request: HoppGQLRequest
|
||||
}) => {
|
||||
saveGraphqlRequestAs(folderPath, {
|
||||
...cloneDeep(request),
|
||||
name: `${request.name} - ${t("action.duplicate")}`,
|
||||
})
|
||||
}
|
||||
|
||||
const selectRequest = ({
|
||||
request,
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: {
|
||||
request: HoppGQLRequest
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
}) => {
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath,
|
||||
requestIndex: requestIndex,
|
||||
})
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
folderPath,
|
||||
"graphql"
|
||||
)
|
||||
// Switch to that request if that request is open
|
||||
if (possibleTab) {
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
return
|
||||
}
|
||||
|
||||
tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath,
|
||||
requestIndex: requestIndex,
|
||||
},
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: request.name,
|
||||
url: request.url,
|
||||
query: request.query,
|
||||
headers: request.headers,
|
||||
variables: request.variables,
|
||||
auth: request.auth,
|
||||
this.tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: index,
|
||||
},
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
})
|
||||
),
|
||||
isDirty: false,
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "gql",
|
||||
createdNow: true,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
this.displayModalAddRequest(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
addRequest(payload) {
|
||||
const { path } = payload
|
||||
this.$data.editingFolderPath = path
|
||||
this.displayModalAddRequest(true)
|
||||
},
|
||||
onAddFolder({ name, path }) {
|
||||
addGraphqlFolder(name, path)
|
||||
|
||||
const dropRequest = ({
|
||||
folderPath,
|
||||
requestIndex,
|
||||
collectionIndex,
|
||||
}: {
|
||||
folderPath: string
|
||||
requestIndex: number
|
||||
collectionIndex: number
|
||||
}) => {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
`${collectionIndex}`,
|
||||
"graphql"
|
||||
)
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: false,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath,
|
||||
requestIndex: Number(requestIndex),
|
||||
})
|
||||
|
||||
if (possibleTab) {
|
||||
possibleTab.value.document.saveContext = {
|
||||
originLocation: "user-collection",
|
||||
folderPath: `${collectionIndex}`,
|
||||
requestIndex: getRequestsByPath(collections.value, `${collectionIndex}`)
|
||||
.length,
|
||||
}
|
||||
|
||||
possibleTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
moveGraphqlRequest(folderPath, requestIndex, `${collectionIndex}`)
|
||||
|
||||
toast.success(`${t("request.moved")}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the collection is already in the root
|
||||
* @param id - path of the collection
|
||||
* @returns boolean - true if the collection is already in the root
|
||||
*/
|
||||
const isAlreadyInRoot = (id: string) => {
|
||||
const indexPath = id.split("/")
|
||||
return indexPath.length === 1
|
||||
}
|
||||
|
||||
const editProperties = ({
|
||||
collectionIndex,
|
||||
collection,
|
||||
}: {
|
||||
collectionIndex: string | null
|
||||
collection: HoppCollection | null
|
||||
}) => {
|
||||
if (collectionIndex === null || collection === null) return
|
||||
|
||||
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||
let inheritedProperties = {}
|
||||
|
||||
if (parentIndex) {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
parentIndex,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
} as HoppInheritedProperty
|
||||
}
|
||||
|
||||
editingProperties.value = {
|
||||
collection,
|
||||
isRootCollection: isAlreadyInRoot(collectionIndex),
|
||||
path: collectionIndex,
|
||||
inheritedProperties,
|
||||
}
|
||||
|
||||
displayModalEditProperties(true)
|
||||
}
|
||||
|
||||
const setCollectionProperties = (newCollection: {
|
||||
collection: HoppCollection
|
||||
path: string
|
||||
isRootCollection: boolean
|
||||
}) => {
|
||||
const { collection, path, isRootCollection } = newCollection
|
||||
if (isRootCollection) {
|
||||
editGraphqlCollection(parseInt(path), collection)
|
||||
} else {
|
||||
editGraphqlFolder(path, collection)
|
||||
}
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
path,
|
||||
"graphql"
|
||||
)
|
||||
|
||||
nextTick(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
{
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
"graphql"
|
||||
)
|
||||
})
|
||||
|
||||
displayModalEditProperties(false)
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingCollection.value = null
|
||||
editingCollectionIndex.value = null
|
||||
editingFolder.value = null
|
||||
editingFolderIndex.value = null
|
||||
editingRequest.value = null
|
||||
editingRequestIndex.value = null
|
||||
}
|
||||
this.displayModalAddFolder(false)
|
||||
},
|
||||
addFolder(payload) {
|
||||
const { path } = payload
|
||||
this.$data.editingFolderPath = path
|
||||
this.displayModalAddFolder(true)
|
||||
},
|
||||
editFolder(payload) {
|
||||
const { folder, folderPath } = payload
|
||||
this.editingFolder = folder
|
||||
this.editingFolderPath = folderPath
|
||||
this.displayModalEditFolder(true)
|
||||
},
|
||||
editRequest(payload) {
|
||||
const {
|
||||
collectionIndex,
|
||||
folderIndex,
|
||||
folderName,
|
||||
request,
|
||||
requestIndex,
|
||||
folderPath,
|
||||
} = payload
|
||||
this.$data.editingFolderPath = folderPath
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.$data.editingFolderIndex = folderIndex
|
||||
this.$data.editingFolderName = folderName
|
||||
this.$data.editingRequest = request
|
||||
this.$data.editingRequestIndex = requestIndex
|
||||
this.displayModalEditRequest(true)
|
||||
},
|
||||
resetSelectedData() {
|
||||
this.$data.editingCollection = undefined
|
||||
this.$data.editingCollectionIndex = undefined
|
||||
this.$data.editingFolder = undefined
|
||||
this.$data.editingFolderIndex = undefined
|
||||
this.$data.editingRequest = undefined
|
||||
this.$data.editingRequestIndex = undefined
|
||||
},
|
||||
duplicateRequest({ folderPath, request }) {
|
||||
saveGraphqlRequestAs(folderPath, {
|
||||
...cloneDeep(request),
|
||||
name: `${request.name} - ${this.t("action.duplicate")}`,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
v-model="filterTexts"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||
class="flex w-full bg-transparent px-4 py-2"
|
||||
:placeholder="t('action.search')"
|
||||
:disabled="collectionsType.type === 'team-collections'"
|
||||
/>
|
||||
@@ -38,7 +38,6 @@
|
||||
@add-request="addRequest"
|
||||
@edit-collection="editCollection"
|
||||
@edit-folder="editFolder"
|
||||
@edit-properties="editProperties"
|
||||
@export-data="exportData"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-folder="removeFolder"
|
||||
@@ -70,7 +69,6 @@
|
||||
@add-folder="addFolder"
|
||||
@edit-collection="editCollection"
|
||||
@edit-folder="editFolder"
|
||||
@edit-properties="editProperties"
|
||||
@export-data="exportData"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-folder="removeFolder"
|
||||
@@ -153,12 +151,6 @@
|
||||
:show="showTeamModalAdd"
|
||||
@hide-modal="displayTeamModalAdd(false)"
|
||||
/>
|
||||
<CollectionsProperties
|
||||
:show="showModalEditProperties"
|
||||
:editing-properties="editingProperties"
|
||||
@hide-modal="displayModalEditProperties(false)"
|
||||
@set-collection-properties="setCollectionProperties"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -189,13 +181,10 @@ import {
|
||||
moveRESTFolder,
|
||||
navigateToFolderWithIndexPath,
|
||||
restCollectionStore,
|
||||
cascadeParentCollectionForHeaderAuth,
|
||||
} from "~/newstore/collections"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeaders,
|
||||
HoppRESTRequest,
|
||||
makeCollection,
|
||||
} from "@hoppscotch/data"
|
||||
@@ -204,10 +193,10 @@ import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
createNewRootCollection,
|
||||
createChildCollection,
|
||||
renameCollection,
|
||||
deleteCollection,
|
||||
moveRESTTeamCollection,
|
||||
updateOrderRESTTeamCollection,
|
||||
updateTeamCollection,
|
||||
} from "~/helpers/backend/mutations/TeamCollection"
|
||||
import {
|
||||
updateTeamRequest,
|
||||
@@ -231,7 +220,6 @@ import {
|
||||
getFoldersByPath,
|
||||
resolveSaveContextOnCollectionReorder,
|
||||
updateSaveContextForAffectedRequests,
|
||||
updateInheritedPropertiesForAffectedRequests,
|
||||
resetTeamRequestsContext,
|
||||
} from "~/helpers/collection/collection"
|
||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
@@ -239,7 +227,6 @@ import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -279,11 +266,15 @@ const collectionsType = ref<CollectionType>({
|
||||
})
|
||||
|
||||
// Collection Data
|
||||
const editingCollection = ref<HoppCollection | TeamCollection | null>(null)
|
||||
const editingCollection = ref<
|
||||
HoppCollection<HoppRESTRequest> | TeamCollection | null
|
||||
>(null)
|
||||
const editingCollectionName = ref<string | null>(null)
|
||||
const editingCollectionIndex = ref<number | null>(null)
|
||||
const editingCollectionID = ref<string | null>(null)
|
||||
const editingFolder = ref<HoppCollection | TeamCollection | null>(null)
|
||||
const editingFolder = ref<
|
||||
HoppCollection<HoppRESTRequest> | TeamCollection | null
|
||||
>(null)
|
||||
const editingFolderName = ref<string | null>(null)
|
||||
const editingFolderPath = ref<string | null>(null)
|
||||
const editingRequest = ref<HoppRESTRequest | null>(null)
|
||||
@@ -291,18 +282,6 @@ const editingRequestName = ref("")
|
||||
const editingRequestIndex = ref<number | null>(null)
|
||||
const editingRequestID = ref<string | null>(null)
|
||||
|
||||
const editingProperties = ref<{
|
||||
collection: Omit<HoppCollection, "v"> | TeamCollection | null
|
||||
isRootCollection: boolean
|
||||
path: string
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>({
|
||||
collection: null,
|
||||
isRootCollection: false,
|
||||
path: "",
|
||||
inheritedProperties: undefined,
|
||||
})
|
||||
|
||||
const confirmModalTitle = ref<string | null>(null)
|
||||
|
||||
const filterTexts = ref("")
|
||||
@@ -541,7 +520,6 @@ const showModalEditCollection = ref(false)
|
||||
const showModalEditFolder = ref(false)
|
||||
const showModalEditRequest = ref(false)
|
||||
const showModalImportExport = ref(false)
|
||||
const showModalEditProperties = ref(false)
|
||||
const showConfirmModal = ref(false)
|
||||
const showTeamModalAdd = ref(false)
|
||||
|
||||
@@ -587,12 +565,6 @@ const displayModalImportExport = (show: boolean) => {
|
||||
if (!show) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayModalEditProperties = (show: boolean) => {
|
||||
showModalEditProperties.value = show
|
||||
|
||||
if (!show) resetSelectedData()
|
||||
}
|
||||
|
||||
const displayConfirmModal = (show: boolean) => {
|
||||
showConfirmModal.value = show
|
||||
|
||||
@@ -612,11 +584,6 @@ const addNewRootCollection = (name: string) => {
|
||||
name,
|
||||
folders: [],
|
||||
requests: [],
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -658,7 +625,7 @@ const addNewRootCollection = (name: string) => {
|
||||
|
||||
const addRequest = (payload: {
|
||||
path: string
|
||||
folder: HoppCollection | TeamCollection
|
||||
folder: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
}) => {
|
||||
const { path, folder } = payload
|
||||
editingFolder.value = folder
|
||||
@@ -672,13 +639,11 @@ const onAddRequest = (requestName: string) => {
|
||||
name: requestName,
|
||||
}
|
||||
|
||||
const path = editingFolderPath.value
|
||||
if (!path) return
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const path = editingFolderPath.value
|
||||
if (!path) return
|
||||
const insertionIndex = saveRESTRequestAs(path, newRequest)
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
|
||||
|
||||
tabs.createNewTab({
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
@@ -687,10 +652,6 @@ const onAddRequest = (requestName: string) => {
|
||||
folderPath: path,
|
||||
requestIndex: insertionIndex,
|
||||
},
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
@@ -731,8 +692,7 @@ const onAddRequest = (requestName: string) => {
|
||||
},
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
|
||||
|
||||
tabs.createNewTab({
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
@@ -742,10 +702,6 @@ const onAddRequest = (requestName: string) => {
|
||||
collectionID: createRequestInCollection.collection.id,
|
||||
teamID: createRequestInCollection.collection.team.id,
|
||||
},
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
})
|
||||
|
||||
modalLoadingState.value = false
|
||||
@@ -758,7 +714,7 @@ const onAddRequest = (requestName: string) => {
|
||||
|
||||
const addFolder = (payload: {
|
||||
path: string
|
||||
folder: HoppCollection | TeamCollection
|
||||
folder: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
}) => {
|
||||
const { path, folder } = payload
|
||||
editingFolder.value = folder
|
||||
@@ -817,13 +773,15 @@ const onAddFolder = (folderName: string) => {
|
||||
|
||||
const editCollection = (payload: {
|
||||
collectionIndex: string
|
||||
collection: HoppCollection | TeamCollection
|
||||
collection: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
}) => {
|
||||
const { collectionIndex, collection } = payload
|
||||
editingCollection.value = collection
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
editingCollectionIndex.value = parseInt(collectionIndex)
|
||||
editingCollectionName.value = (collection as HoppCollection).name
|
||||
editingCollectionName.value = (
|
||||
collection as HoppCollection<HoppRESTRequest>
|
||||
).name
|
||||
} else {
|
||||
editingCollectionName.value = (collection as TeamCollection).title
|
||||
}
|
||||
@@ -858,7 +816,7 @@ const updateEditingCollection = (newName: string) => {
|
||||
modalLoadingState.value = true
|
||||
|
||||
pipe(
|
||||
updateTeamCollection(editingCollection.value.id, undefined, newName),
|
||||
renameCollection(editingCollection.value.id, newName),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
@@ -876,13 +834,13 @@ const updateEditingCollection = (newName: string) => {
|
||||
|
||||
const editFolder = (payload: {
|
||||
folderPath: string | undefined
|
||||
folder: HoppCollection | TeamCollection
|
||||
folder: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
}) => {
|
||||
const { folderPath, folder } = payload
|
||||
editingFolder.value = folder
|
||||
if (collectionsType.value.type === "my-collections" && folderPath) {
|
||||
editingFolderPath.value = folderPath
|
||||
editingFolderName.value = (folder as HoppCollection).name
|
||||
editingFolderName.value = (folder as HoppCollection<HoppRESTRequest>).name
|
||||
} else {
|
||||
editingFolderName.value = (folder as TeamCollection).title
|
||||
}
|
||||
@@ -896,7 +854,7 @@ const updateEditingFolder = (newName: string) => {
|
||||
if (!editingFolderPath.value) return
|
||||
|
||||
editRESTFolder(editingFolderPath.value, {
|
||||
...(editingFolder.value as HoppCollection),
|
||||
...(editingFolder.value as HoppCollection<HoppRESTRequest>),
|
||||
name: newName,
|
||||
})
|
||||
displayModalEditFolder(false)
|
||||
@@ -907,7 +865,7 @@ const updateEditingFolder = (newName: string) => {
|
||||
/* renameCollection can be used to rename both collections and folders
|
||||
since folder is treated as collection in the BE. */
|
||||
pipe(
|
||||
updateTeamCollection(editingFolder.value.id, undefined, newName),
|
||||
renameCollection(editingFolder.value.id, newName),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
if (err.error === "team_coll/short_title") {
|
||||
@@ -1321,18 +1279,16 @@ const selectPicked = (payload: Picked | null) => {
|
||||
*/
|
||||
const selectRequest = (selectedRequest: {
|
||||
request: HoppRESTRequest
|
||||
folderPath: string
|
||||
folderPath: string | undefined
|
||||
requestIndex: string
|
||||
isActive: boolean
|
||||
}) => {
|
||||
const { request, folderPath, requestIndex } = selectedRequest
|
||||
|
||||
// If there is a request with this save context, switch into it
|
||||
let possibleTab = null
|
||||
|
||||
if (collectionsType.value.type === "team-collections") {
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
|
||||
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
@@ -1346,19 +1302,10 @@ const selectRequest = (selectedRequest: {
|
||||
saveContext: {
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
collectionID: folderPath,
|
||||
},
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
folderPath,
|
||||
"rest"
|
||||
)
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
requestIndex: parseInt(requestIndex),
|
||||
@@ -1376,10 +1323,6 @@ const selectRequest = (selectedRequest: {
|
||||
folderPath: folderPath!,
|
||||
requestIndex: parseInt(requestIndex),
|
||||
},
|
||||
inheritedProperties: {
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1406,17 +1349,16 @@ const dropRequest = (payload: {
|
||||
}) => {
|
||||
const { folderPath, requestIndex, destinationCollectionIndex } = payload
|
||||
|
||||
if (!requestIndex || !destinationCollectionIndex || !folderPath) return
|
||||
if (!requestIndex || !destinationCollectionIndex) return
|
||||
|
||||
let possibleTab = null
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
destinationCollectionIndex,
|
||||
"rest"
|
||||
if (collectionsType.value.type === "my-collections" && folderPath) {
|
||||
moveRESTRequest(
|
||||
folderPath,
|
||||
pathToLastIndex(requestIndex),
|
||||
destinationCollectionIndex
|
||||
)
|
||||
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath,
|
||||
requestIndex: pathToLastIndex(requestIndex),
|
||||
@@ -1432,11 +1374,6 @@ const dropRequest = (payload: {
|
||||
destinationCollectionIndex
|
||||
).length,
|
||||
}
|
||||
|
||||
possibleTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
// When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
|
||||
@@ -1446,11 +1383,6 @@ const dropRequest = (payload: {
|
||||
folderPath,
|
||||
length: getRequestsByPath(myCollections.value, folderPath).length,
|
||||
})
|
||||
moveRESTRequest(
|
||||
folderPath,
|
||||
pathToLastIndex(requestIndex),
|
||||
destinationCollectionIndex
|
||||
)
|
||||
|
||||
toast.success(`${t("request.moved")}`)
|
||||
draggingToRoot.value = false
|
||||
@@ -1474,12 +1406,8 @@ const dropRequest = (payload: {
|
||||
requestMoveLoading.value.indexOf(requestIndex),
|
||||
1
|
||||
)
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
|
||||
destinationCollectionIndex
|
||||
)
|
||||
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
})
|
||||
@@ -1489,10 +1417,6 @@ const dropRequest = (payload: {
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
}
|
||||
possibleTab.value.document.inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
toast.success(`${t("request.moved")}`)
|
||||
}
|
||||
@@ -1613,22 +1537,6 @@ const dropCollection = (payload: {
|
||||
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
|
||||
)
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
|
||||
"rest"
|
||||
)
|
||||
|
||||
const inheritedProperty = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
|
||||
inheritedProperty,
|
||||
"rest"
|
||||
)
|
||||
|
||||
draggingToRoot.value = false
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
@@ -1654,22 +1562,6 @@ const dropCollection = (payload: {
|
||||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
|
||||
destinationCollectionIndex
|
||||
)
|
||||
|
||||
const inheritedProperty = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
`${destinationCollectionIndex}`,
|
||||
inheritedProperty,
|
||||
"rest"
|
||||
)
|
||||
}
|
||||
)
|
||||
)()
|
||||
@@ -1954,11 +1846,13 @@ const initializeDownloadCollection = async (
|
||||
* Triggered by the export button in the tippy menu
|
||||
* @param collection - Collection or folder to be exported
|
||||
*/
|
||||
const exportData = async (collection: HoppCollection | TeamCollection) => {
|
||||
const exportData = async (
|
||||
collection: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||
) => {
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const collectionJSON = JSON.stringify(collection)
|
||||
|
||||
const name = (collection as HoppCollection).name
|
||||
const name = (collection as HoppCollection<HoppRESTRequest>).name
|
||||
|
||||
initializeDownloadCollection(collectionJSON, name)
|
||||
} else {
|
||||
@@ -1999,164 +1893,6 @@ const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const editProperties = (payload: {
|
||||
collectionIndex: string
|
||||
collection: HoppCollection | TeamCollection
|
||||
}) => {
|
||||
const { collection, collectionIndex } = payload
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||
|
||||
let inheritedProperties = {
|
||||
auth: {
|
||||
parentID: "",
|
||||
parentName: "",
|
||||
inheritedAuth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
parentID: "",
|
||||
parentName: "",
|
||||
inheritedHeaders: [],
|
||||
},
|
||||
],
|
||||
} as HoppInheritedProperty
|
||||
|
||||
if (parentIndex) {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
|
||||
parentIndex,
|
||||
"rest"
|
||||
)
|
||||
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
editingProperties.value = {
|
||||
collection,
|
||||
isRootCollection: isAlreadyInRoot(collectionIndex),
|
||||
path: collectionIndex,
|
||||
inheritedProperties,
|
||||
}
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||
|
||||
const data = (collection as TeamCollection).data
|
||||
? JSON.parse((collection as TeamCollection).data ?? "")
|
||||
: null
|
||||
|
||||
let inheritedProperties = undefined
|
||||
let coll = {
|
||||
id: collection.id,
|
||||
name: (collection as TeamCollection).title,
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
} as HoppRESTAuth,
|
||||
headers: [] as HoppRESTHeaders,
|
||||
folders: null,
|
||||
requests: null,
|
||||
}
|
||||
|
||||
if (parentIndex) {
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(parentIndex)
|
||||
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
coll = {
|
||||
...coll,
|
||||
auth: data.auth,
|
||||
headers: data.headers as HoppRESTHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
editingProperties.value = {
|
||||
collection: coll,
|
||||
isRootCollection: isAlreadyInRoot(collectionIndex),
|
||||
path: collectionIndex,
|
||||
inheritedProperties,
|
||||
}
|
||||
}
|
||||
|
||||
displayModalEditProperties(true)
|
||||
}
|
||||
|
||||
const setCollectionProperties = (newCollection: {
|
||||
collection: HoppCollection
|
||||
path: string
|
||||
isRootCollection: boolean
|
||||
}) => {
|
||||
const { collection, path, isRootCollection } = newCollection
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
if (isRootCollection) {
|
||||
editRESTCollection(parseInt(path), collection)
|
||||
} else {
|
||||
editRESTFolder(path, collection)
|
||||
}
|
||||
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
|
||||
|
||||
nextTick(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
{
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
"rest"
|
||||
)
|
||||
})
|
||||
toast.success(t("collection.properties_updated"))
|
||||
} else if (hasTeamWriteAccess.value && collection.id) {
|
||||
const data = {
|
||||
auth: collection.auth,
|
||||
headers: collection.headers,
|
||||
}
|
||||
pipe(
|
||||
updateTeamCollection(collection.id, JSON.stringify(data), undefined),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(t("collection.properties_updated"))
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
//This is a hack to update the inherited properties of the requests if there an tab opened
|
||||
// since it takes a little bit of time to update the collection tree
|
||||
setTimeout(() => {
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
{
|
||||
auth,
|
||||
headers,
|
||||
},
|
||||
"rest",
|
||||
"team"
|
||||
)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
displayModalEditProperties(false)
|
||||
}
|
||||
|
||||
const resolveConfirmModal = (title: string | null) => {
|
||||
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
|
||||
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()
|
||||
|
||||
@@ -9,13 +9,9 @@
|
||||
<template #body>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!currentInterceptorSupportsCookies"
|
||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||
:alt="`${t('cookies.modal.interceptor_no_support')}`"
|
||||
:text="t('cookies.modal.interceptor_no_support')"
|
||||
>
|
||||
<template #body>
|
||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||
</template>
|
||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-col">
|
||||
<div
|
||||
@@ -42,7 +38,8 @@
|
||||
:alt="`${t('cookies.modal.empty_domains')}`"
|
||||
:text="t('cookies.modal.empty_domains')"
|
||||
class="mt-6"
|
||||
/>
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-for="[domain, entries] in workingCookieJar.entries()"
|
||||
v-else
|
||||
|
||||
@@ -105,11 +105,9 @@ const requestCancelFunc: Ref<(() => void) | null> = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const shortcodeBaseURL =
|
||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
|
||||
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
const sharedRequestURL = computed(() => {
|
||||
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
||||
return `${baseURL}/r/${props.sharedRequestID}`
|
||||
})
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
@@ -18,22 +18,16 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
|
||||
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
appendEnvironments,
|
||||
addGlobalEnvVariable,
|
||||
environments$,
|
||||
} from "~/newstore/environments"
|
||||
import { appendEnvironments, environments$ } from "~/newstore/environments"
|
||||
|
||||
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
|
||||
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
|
||||
import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv"
|
||||
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||
import { computed } from "vue"
|
||||
@@ -142,51 +136,6 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
|
||||
}),
|
||||
}
|
||||
|
||||
const insomniaEnvironmentsImport: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.from_insomnia",
|
||||
name: "import.from_insomnia",
|
||||
icon: IconInsomnia,
|
||||
title: "import.from_json",
|
||||
applicableTo: ["personal-workspace", "team-workspace"],
|
||||
disabled: false,
|
||||
},
|
||||
component: FileSource({
|
||||
acceptedFileTypes: "application/json",
|
||||
caption: "import.insomnia_environment_description",
|
||||
onImportFromFile: async (environments) => {
|
||||
const res = await insomniaEnvImporter(environments)()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
showImportFailedError()
|
||||
return
|
||||
}
|
||||
|
||||
const globalEnvIndex = res.right.findIndex(
|
||||
(env) => env.name === "Base Environment"
|
||||
)
|
||||
|
||||
const globalEnv =
|
||||
globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
|
||||
|
||||
// remove the global env from the environments array to prevent it from being imported twice
|
||||
if (globalEnvIndex !== -1) {
|
||||
res.right.splice(globalEnvIndex, 1)
|
||||
}
|
||||
|
||||
handleImportToStore(res.right, globalEnv)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||
})
|
||||
|
||||
emit("hide-modal")
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const EnvironmentsImportFromGIST: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "import.environments_from_gist",
|
||||
@@ -306,7 +255,6 @@ const importerModules = [
|
||||
HoppEnvironmentsImport,
|
||||
EnvironmentsImportFromGIST,
|
||||
PostmanEnvironmentsImport,
|
||||
insomniaEnvironmentsImport,
|
||||
]
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
@@ -323,17 +271,7 @@ const showImportFailedError = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
const handleImportToStore = async (
|
||||
environments: Environment[],
|
||||
globalEnv?: Environment
|
||||
) => {
|
||||
// if there's a global env, add them to the store
|
||||
if (globalEnv) {
|
||||
globalEnv.variables.forEach(({ key, value }) => {
|
||||
addGlobalEnvVariable({ key, value })
|
||||
})
|
||||
}
|
||||
|
||||
const handleImportToStore = async (environments: Environment[]) => {
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
appendEnvironments(environments)
|
||||
toast.success(t("state.file_imported"))
|
||||
|
||||
@@ -93,12 +93,20 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
<div
|
||||
v-if="myEnvironments.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
/>
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
@@ -132,12 +140,20 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
<div
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
/>
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
|
||||
@@ -78,13 +78,11 @@
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,29 +38,27 @@
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showModalDetails"
|
||||
|
||||
@@ -81,20 +81,18 @@
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,33 +49,31 @@
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex flex-col items-stretch gap-4">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
<EnvironmentsTeamsEnvironment
|
||||
|
||||
@@ -1,71 +1,64 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="`${t('auth.login_to_hoppscotch')}`"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<template v-if="isLoadingAllowedAuthProviders">
|
||||
<div class="flex justify-center">
|
||||
<HoppSmartSpinner />
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-if="mode === 'email'"
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:label="`${t('auth.send_magic_link')}`"
|
||||
/>
|
||||
</form>
|
||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||
<div class="flex max-w-md flex-col items-center justify-center">
|
||||
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||
<h3 class="my-2 text-center text-lg">
|
||||
{{ t("auth.we_sent_magic_link") }}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
{{
|
||||
t("auth.we_sent_magic_link_description", { email: form.email })
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
v-if="mode === 'email'"
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
:label="`${t('auth.send_magic_link')}`"
|
||||
/>
|
||||
</form>
|
||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||
<div class="flex max-w-md flex-col items-center justify-center">
|
||||
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||
<h3 class="my-2 text-center text-lg">
|
||||
{{ t("auth.we_sent_magic_link") }}
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
{{
|
||||
t("auth.we_sent_magic_link_description", { email: form.email })
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div
|
||||
@@ -116,7 +109,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, onMounted, ref } from "vue"
|
||||
import { Ref, computed, onMounted, ref } from "vue"
|
||||
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useStreamSubscriber } from "@composables/stream"
|
||||
@@ -134,7 +127,9 @@ import { useService } from "dioc/vue"
|
||||
import { LoginItemDef } from "~/platform/auth"
|
||||
import { PersistenceService } from "~/services/persistence"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
@@ -150,8 +145,6 @@ const form = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
const isLoadingAllowedAuthProviders = ref(true)
|
||||
|
||||
const signingInWithGoogle = ref(false)
|
||||
const signingInWithGitHub = ref(false)
|
||||
const signingInWithMicrosoft = ref(false)
|
||||
@@ -169,42 +162,21 @@ type AuthProviderItem = {
|
||||
isLoading: Ref<boolean>
|
||||
}
|
||||
|
||||
let allowedAuthProviders: AuthProviderItem[] = []
|
||||
let additonalLoginItems: LoginItemDef[] = []
|
||||
const additonalLoginItems = computed(
|
||||
() => platform.auth.additionalLoginItems ?? []
|
||||
)
|
||||
|
||||
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
|
||||
await item.onClick()
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
subscribeToStream(currentUser$, (user) => {
|
||||
if (user) hideModal()
|
||||
})
|
||||
|
||||
const res = await platform.auth.getAllowedAuthProviders()
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(`${t("error.authproviders_load_error")}`)
|
||||
isLoadingAllowedAuthProviders.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// setup the normal auth providers
|
||||
const enabledAuthProviders = authProvidersAvailable.filter((provider) =>
|
||||
res.right.includes(provider.id)
|
||||
)
|
||||
allowedAuthProviders = enabledAuthProviders
|
||||
|
||||
// setup the additional login items
|
||||
additonalLoginItems =
|
||||
platform.auth.additionalLoginItems?.filter((item) =>
|
||||
res.right.includes(item.id)
|
||||
) ?? []
|
||||
|
||||
isLoadingAllowedAuthProviders.value = false
|
||||
})
|
||||
|
||||
const showLoginSuccess = () => {
|
||||
@@ -303,7 +275,14 @@ const signInWithEmail = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const authProvidersAvailable: AuthProviderItem[] = [
|
||||
const hideModal = () => {
|
||||
mode.value = "sign-in"
|
||||
toast.clear()
|
||||
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const authProviders: AuthProviderItem[] = [
|
||||
{
|
||||
id: "GITHUB",
|
||||
icon: IconGithub,
|
||||
@@ -336,10 +315,19 @@ const authProvidersAvailable: AuthProviderItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const hideModal = () => {
|
||||
mode.value = "sign-in"
|
||||
toast.clear()
|
||||
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
|
||||
// prettier-ignore
|
||||
const allowedAuthProvidersIDsString =
|
||||
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
|
||||
emit("hide-modal")
|
||||
}
|
||||
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
|
||||
? allowedAuthProvidersIDsString.split(",")
|
||||
: []
|
||||
|
||||
const allowedAuthProviders =
|
||||
allowedAuthProvidersIDs.length > 0
|
||||
? authProviders.filter((provider) =>
|
||||
allowedAuthProvidersIDs.includes(provider.id)
|
||||
)
|
||||
: authProviders
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
|
||||
:class="[
|
||||
isCollectionProperty
|
||||
? 'top-propertiesPrimaryStickyFold'
|
||||
: 'top-sidebarPrimaryStickyFold',
|
||||
]"
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
@@ -42,18 +37,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isRootCollection"
|
||||
label="Inherit"
|
||||
:icon="authName === 'Inherit' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'Inherit'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'inherit'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
@@ -137,16 +120,14 @@
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
:text="t('empty.authorization')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
@@ -166,17 +147,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'inherit'" class="p-4">
|
||||
<span v-if="inheritedProperties?.auth">
|
||||
Inherited
|
||||
{{ getAuthName(inheritedProperties.auth.inheritedAuth.authType) }}
|
||||
from Parent Collection {{ inheritedProperties?.auth.parentName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Please save this request in any collection to inherit the
|
||||
authorization
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
@@ -231,8 +201,6 @@ import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { onMounted } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -240,24 +208,12 @@ const colorMode = useColorMode()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppGQLAuth
|
||||
isCollectionProperty?: boolean
|
||||
isRootCollection?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppGQLAuth): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isRootCollection && auth.value.authType === "inherit") {
|
||||
auth.value = {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const auth = useVModel(props, "modelValue", emit)
|
||||
|
||||
const AUTH_KEY_NAME = {
|
||||
@@ -266,20 +222,12 @@ const AUTH_KEY_NAME = {
|
||||
"oauth-2": "OAuth 2.0",
|
||||
"api-key": "API key",
|
||||
none: "None",
|
||||
inherit: "Inherit",
|
||||
} as const
|
||||
|
||||
const authType = pluckRef(auth, "authType")
|
||||
|
||||
const authName = computed(() =>
|
||||
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
||||
)
|
||||
|
||||
const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
|
||||
if (!type) return "None"
|
||||
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
|
||||
}
|
||||
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
|
||||
const clearContent = () => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
|
||||
:class="[
|
||||
isCollectionProperty
|
||||
? 'top-propertiesPrimaryStickyFold'
|
||||
: 'top-sidebarPrimaryStickyFold',
|
||||
]"
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("tab.headers") }}
|
||||
@@ -82,11 +77,22 @@
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<SmartEnvInput
|
||||
v-model="header.key"
|
||||
<HoppSmartAutoComplete
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
:auto-complete-source="commonHeaders"
|
||||
@change="
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header.key"
|
||||
autofocus
|
||||
styles="
|
||||
bg-transparent
|
||||
flex
|
||||
flex-1
|
||||
py-1
|
||||
px-4
|
||||
truncate
|
||||
"
|
||||
class="!flex flex-1"
|
||||
@input="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: $event,
|
||||
@@ -95,14 +101,17 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-model="header.value"
|
||||
<input
|
||||
class="flex flex-1 bg-transparent px-4 py-2"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:name="`value ${String(index)}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: header.key,
|
||||
value: $event,
|
||||
value: ($event!.target! as HTMLInputElement).value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
@@ -147,133 +156,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<draggable
|
||||
v-model="computedHeaders"
|
||||
item-key="id"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<template #item="{ element: header, index }">
|
||||
<div
|
||||
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
|
||||
>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLock"
|
||||
class="cursor-auto bg-divider text-secondaryLight opacity-25"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<SmartEnvInput
|
||||
v-model="header.header.key"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
readonly
|
||||
/>
|
||||
<SmartEnvInput
|
||||
:model-value="mask(header)"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
readonly
|
||||
/>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-if="header.source === 'auth'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t(masking ? 'state.show' : 'state.hide')"
|
||||
:icon="masking ? IconEye : IconEyeOff"
|
||||
@click="toggleMask()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArrowUpRight"
|
||||
:title="t('request.go_to_authorization_tab')"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArrowUpRight"
|
||||
:title="t('request.go_to_authorization_tab')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<draggable
|
||||
v-model="inheritedProperties"
|
||||
item-key="id"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<template #item="{ element: header, index }">
|
||||
<div
|
||||
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
|
||||
>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLock"
|
||||
class="cursor-auto bg-divider text-secondaryLight opacity-25"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<SmartEnvInput
|
||||
v-model="header.header.key"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
readonly
|
||||
/>
|
||||
<SmartEnvInput
|
||||
:model-value="
|
||||
header.source === 'auth' ? mask(header) : header.header.value
|
||||
"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
readonly
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="header.source === 'auth'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t(masking ? 'state.show' : 'state.hide')"
|
||||
:icon="masking && header.source === 'auth' ? IconEye : IconEyeOff"
|
||||
@click="toggleMask()"
|
||||
/>
|
||||
<span v-else class="aspect-square w-[2.05rem]"></span>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconInfo"
|
||||
:title="`This header is inherited from Parent Collection ${
|
||||
header.inheritedFrom ?? ''
|
||||
}`"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingHeaders.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||
:alt="`${t('empty.headers')}`"
|
||||
:text="t('empty.headers')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
@@ -288,12 +182,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconEyeOff from "~icons/lucide/eye-off"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
@@ -315,20 +204,13 @@ import { commonHeaders } from "~/helpers/headers"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppGQLHeader } from "~/helpers/graphql"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = defineProps<{
|
||||
modelValue: HoppGQLRequest
|
||||
isCollectionProperty?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>()
|
||||
const props = defineProps<{ modelValue: HoppGQLRequest }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppGQLRequest): void
|
||||
@@ -529,11 +411,7 @@ const deleteHeader = (index: number) => {
|
||||
})
|
||||
}
|
||||
|
||||
workingHeaders.value = pipe(
|
||||
workingHeaders.value,
|
||||
A.deleteAt(index),
|
||||
O.getOrElseW(() => throwError("Working Headers Deletion Out of Bounds"))
|
||||
)
|
||||
workingHeaders.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
@@ -549,151 +427,4 @@ const clearContent = () => {
|
||||
|
||||
bulkHeaders.value = ""
|
||||
}
|
||||
|
||||
const getComputedAuthHeaders = (
|
||||
req?: HoppGQLRequest,
|
||||
auth?: HoppGQLRequest["auth"]
|
||||
) => {
|
||||
const request = auth ? { auth: auth ?? { authActive: false } } : req
|
||||
// If Authorization header is also being user-defined, that takes priority
|
||||
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
|
||||
return []
|
||||
|
||||
if (!request) return []
|
||||
|
||||
if (!request.auth || !request.auth.authActive) return []
|
||||
|
||||
const headers: HoppGQLHeader[] = []
|
||||
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (request.auth.authType === "basic") {
|
||||
const username = request.auth.username
|
||||
const password = request.auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
})
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
) {
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${request.auth.token}`,
|
||||
})
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
const { key, addTo } = request.auth
|
||||
|
||||
if (addTo === "Headers" && key) {
|
||||
headers.push({
|
||||
active: true,
|
||||
key,
|
||||
value: request.auth.value ?? "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
const getComputedHeaders = (req: HoppGQLRequest) => {
|
||||
return [
|
||||
...getComputedAuthHeaders(req).map((header) => ({
|
||||
source: "auth" as const,
|
||||
header,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const computedHeaders = computed(() =>
|
||||
getComputedHeaders(request.value).map((header, index) => ({
|
||||
id: `header-${index}`,
|
||||
...header,
|
||||
}))
|
||||
)
|
||||
|
||||
const inheritedProperties = computed(() => {
|
||||
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
|
||||
return []
|
||||
|
||||
//filter out headers that are already in the request headers
|
||||
|
||||
const inheritedHeaders = props.inheritedProperties.headers.filter(
|
||||
(header) =>
|
||||
!request.value.headers.some(
|
||||
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
|
||||
)
|
||||
)
|
||||
|
||||
const headers = inheritedHeaders
|
||||
.filter(
|
||||
(header) =>
|
||||
header.inheritedHeader !== null &&
|
||||
header.inheritedHeader !== undefined &&
|
||||
header.inheritedHeader.active
|
||||
)
|
||||
.map((header, index) => ({
|
||||
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
|
||||
source: "headers",
|
||||
id: `header-${index}`,
|
||||
header: {
|
||||
key: header.inheritedHeader?.key,
|
||||
value: header.inheritedHeader?.value,
|
||||
active: header.inheritedHeader?.active,
|
||||
},
|
||||
}))
|
||||
|
||||
let auth = [] as {
|
||||
inheritedFrom: string
|
||||
source: "auth"
|
||||
id: string
|
||||
header: {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
}[]
|
||||
|
||||
const computedAuthHeader = getComputedAuthHeaders(
|
||||
request.value,
|
||||
props.inheritedProperties.auth.inheritedAuth
|
||||
)[0]
|
||||
|
||||
if (
|
||||
computedAuthHeader &&
|
||||
request.value.auth.authType === "inherit" &&
|
||||
request.value.auth.authActive
|
||||
) {
|
||||
auth = [
|
||||
{
|
||||
inheritedFrom: props.inheritedProperties?.auth.parentName,
|
||||
source: "auth",
|
||||
id: `header-auth`,
|
||||
header: computedAuthHeader,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [...headers, ...auth]
|
||||
})
|
||||
|
||||
const masking = ref(true)
|
||||
|
||||
const toggleMask = () => {
|
||||
masking.value = !masking.value
|
||||
}
|
||||
|
||||
const mask = (header: any) => {
|
||||
if (header.source === "auth" && masking.value)
|
||||
return header.header.value.replace(/\S/gi, "*")
|
||||
return header.header.value
|
||||
}
|
||||
|
||||
// const changeTab = (tab: ComputedHeader["source"]) => {
|
||||
// if (tab === "auth") emit("change-tab", "authorization")
|
||||
// else emit("change-tab", "bodyParams")
|
||||
// }
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user