Compare commits
1 Commits
feat/serve
...
add-realti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7517446f79 |
@@ -66,7 +66,6 @@ services:
|
|||||||
# The service that spins up all 3 services at once in one container
|
# The service that spins up all 3 services at once in one container
|
||||||
hoppscotch-aio:
|
hoppscotch-aio:
|
||||||
container_name: hoppscotch-aio
|
container_name: hoppscotch-aio
|
||||||
restart: unless-stopped
|
|
||||||
build:
|
build:
|
||||||
dockerfile: prod.Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
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-modules/mailer": "^1.9.1",
|
||||||
"@nestjs/apollo": "^12.0.9",
|
"@nestjs/apollo": "^12.0.9",
|
||||||
"@nestjs/common": "^10.2.6",
|
"@nestjs/common": "^10.2.6",
|
||||||
"@nestjs/config": "^3.1.1",
|
|
||||||
"@nestjs/core": "^10.2.6",
|
"@nestjs/core": "^10.2.6",
|
||||||
"@nestjs/graphql": "^12.0.9",
|
"@nestjs/graphql": "^12.0.9",
|
||||||
"@nestjs/jwt": "^10.1.1",
|
"@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
|
VIEWER
|
||||||
EDITOR
|
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 { PrismaModule } from '../prisma/prisma.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { MailerModule } from '../mailer/mailer.module';
|
||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
|
||||||
import { TeamEnvironmentsModule } from '../team-environments/team-environments.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 { TeamRequestModule } from '../team-request/team-request.module';
|
||||||
import { InfraResolver } from './infra.resolver';
|
import { InfraResolver } from './infra.resolver';
|
||||||
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PubSubModule,
|
PubSubModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
MailerModule,
|
||||||
TeamModule,
|
TeamModule,
|
||||||
TeamInvitationModule,
|
TeamInvitationModule,
|
||||||
TeamEnvironmentsModule,
|
TeamEnvironmentsModule,
|
||||||
TeamCollectionModule,
|
TeamCollectionModule,
|
||||||
TeamRequestModule,
|
TeamRequestModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
|
||||||
],
|
],
|
||||||
providers: [InfraResolver, AdminResolver, AdminService],
|
providers: [InfraResolver, AdminResolver, AdminService],
|
||||||
exports: [AdminService],
|
exports: [AdminService],
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -28,7 +27,6 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
|||||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||||
const mockMailerService = mockDeep<MailerService>();
|
const mockMailerService = mockDeep<MailerService>();
|
||||||
const mockShortcodeService = mockDeep<ShortcodeService>();
|
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
|
||||||
|
|
||||||
const adminService = new AdminService(
|
const adminService = new AdminService(
|
||||||
mockUserService,
|
mockUserService,
|
||||||
@@ -41,7 +39,6 @@ const adminService = new AdminService(
|
|||||||
mockPrisma as any,
|
mockPrisma as any,
|
||||||
mockMailerService,
|
mockMailerService,
|
||||||
mockShortcodeService,
|
mockShortcodeService,
|
||||||
mockConfigService,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const invitedUsers: InvitedUsers[] = [
|
const invitedUsers: InvitedUsers[] = [
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
|
|||||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||||
import { TeamMemberRole } from '../team/team.model';
|
import { TeamMemberRole } from '../team/team.model';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -40,7 +39,6 @@ export class AdminService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
private readonly shortcodeService: ShortcodeService,
|
private readonly shortcodeService: ShortcodeService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +79,7 @@ export class AdminService {
|
|||||||
template: 'user-invitation',
|
template: 'user-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: inviteeEmail,
|
inviteeEmail: inviteeEmail,
|
||||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import {
|
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
Args,
|
|
||||||
ID,
|
|
||||||
Mutation,
|
|
||||||
Query,
|
|
||||||
ResolveField,
|
|
||||||
Resolver,
|
|
||||||
} from '@nestjs/graphql';
|
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { Infra } from './infra.model';
|
import { Infra } from './infra.model';
|
||||||
import { AdminService } from './admin.service';
|
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 { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||||
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
|
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)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Infra)
|
@Resolver(() => Infra)
|
||||||
export class InfraResolver {
|
export class InfraResolver {
|
||||||
constructor(
|
constructor(private adminService: AdminService) {}
|
||||||
private adminService: AdminService,
|
|
||||||
private infraConfigService: InfraConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Query(() => Infra, {
|
@Query(() => Infra, {
|
||||||
description: 'Fetch details of the Infrastructure',
|
description: 'Fetch details of the Infrastructure',
|
||||||
@@ -239,76 +222,4 @@ export class InfraResolver {
|
|||||||
userEmail,
|
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 { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||||
isGlobal: true,
|
buildSchemaOptions: {
|
||||||
load: [async () => loadInfraConfiguration()],
|
numberScalarMode: 'integer',
|
||||||
}),
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}),
|
playground: process.env.PRODUCTION !== 'true',
|
||||||
ThrottlerModule.forRootAsync({
|
autoSchemaFile: true,
|
||||||
imports: [ConfigModule],
|
installSubscriptionHandlers: true,
|
||||||
inject: [ConfigService],
|
subscriptions: {
|
||||||
useFactory: async (configService: ConfigService) => [
|
'subscriptions-transport-ws': {
|
||||||
{
|
path: '/graphql',
|
||||||
ttl: +configService.get('RATE_LIMIT_TTL'),
|
onConnect: (_, websocket) => {
|
||||||
limit: +configService.get('RATE_LIMIT_MAX'),
|
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,
|
UserModule,
|
||||||
AuthModule.register(),
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
UserSettingsModule,
|
UserSettingsModule,
|
||||||
UserEnvironmentsModule,
|
UserEnvironmentsModule,
|
||||||
@@ -95,7 +77,6 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
TeamInvitationModule,
|
TeamInvitationModule,
|
||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
@@ -30,21 +31,11 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
|||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(private authService: AuthService) {}
|
||||||
private authService: AuthService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get('providers')
|
|
||||||
async getAuthProviders() {
|
|
||||||
const providers = await this.authService.getAuthProviders();
|
|
||||||
return { providers };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Route to initiate magic-link auth for a users email
|
** Route to initiate magic-link auth for a users email
|
||||||
@@ -54,14 +45,8 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.EMAIL,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { UserModule } from 'src/user/user.module';
|
import { UserModule } from 'src/user/user.module';
|
||||||
|
import { MailerModule } from 'src/mailer/mailer.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@@ -11,47 +12,25 @@ import { GoogleStrategy } from './strategies/google.strategy';
|
|||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
MailerModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.register({
|
||||||
imports: [ConfigModule],
|
secret: process.env.JWT_SECRET,
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
secret: configService.get('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],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {}
|
||||||
static async register() {
|
|
||||||
const env = await loadInfraConfiguration();
|
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
|
||||||
|
|
||||||
const providers = [
|
|
||||||
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
|
|
||||||
? [GoogleStrategy]
|
|
||||||
: []),
|
|
||||||
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
|
|
||||||
? [GithubStrategy]
|
|
||||||
: []),
|
|
||||||
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
|
|
||||||
? [MicrosoftStrategy]
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: AuthModule,
|
|
||||||
providers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,26 +21,15 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import * as E from 'fp-ts/Either';
|
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 mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockUser = mockDeep<UserService>();
|
const mockUser = mockDeep<UserService>();
|
||||||
const mockJWT = mockDeep<JwtService>();
|
const mockJWT = mockDeep<JwtService>();
|
||||||
const mockMailer = mockDeep<MailerService>();
|
const mockMailer = mockDeep<MailerService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
|
||||||
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const authService = new AuthService(
|
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
|
||||||
mockUser,
|
|
||||||
mockPrisma,
|
|
||||||
mockJWT,
|
|
||||||
mockMailer,
|
|
||||||
mockConfigService,
|
|
||||||
mockInfraConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
@@ -102,8 +91,6 @@ describe('signInMagicLink', () => {
|
|||||||
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
||||||
// create new entry in VerificationToken table
|
// create new entry in VerificationToken table
|
||||||
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
||||||
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
|
|
||||||
mockConfigService.get.mockReturnValue('3');
|
|
||||||
|
|
||||||
const result = await authService.signInMagicLink(
|
const result = await authService.signInMagicLink(
|
||||||
'dwight@dundermifflin.com',
|
'dwight@dundermifflin.com',
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { AuthError } from 'src/types/AuthError';
|
|||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -38,8 +36,6 @@ export class AuthService {
|
|||||||
private prismaService: PrismaService,
|
private prismaService: PrismaService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private infraConfigService: InfraConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,12 +46,10 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private async generateMagicLinkTokens(user: AuthUser) {
|
private async generateMagicLinkTokens(user: AuthUser) {
|
||||||
const salt = await bcrypt.genSalt(
|
const salt = await bcrypt.genSalt(
|
||||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
|
||||||
);
|
);
|
||||||
const expiresOn = DateTime.now()
|
const expiresOn = DateTime.now()
|
||||||
.plus({
|
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
|
||||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
|
||||||
})
|
|
||||||
.toISO()
|
.toISO()
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
@@ -101,13 +95,13 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private async generateRefreshToken(userUid: string) {
|
private async generateRefreshToken(userUid: string) {
|
||||||
const refreshTokenPayload: RefreshTokenPayload = {
|
const refreshTokenPayload: RefreshTokenPayload = {
|
||||||
iss: this.configService.get('VITE_BASE_URL'),
|
iss: process.env.VITE_BASE_URL,
|
||||||
sub: userUid,
|
sub: userUid,
|
||||||
aud: [this.configService.get('VITE_BASE_URL')],
|
aud: [process.env.VITE_BASE_URL],
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
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);
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
@@ -133,9 +127,9 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async generateAuthTokens(userUid: string) {
|
async generateAuthTokens(userUid: string) {
|
||||||
const accessTokenPayload: AccessTokenPayload = {
|
const accessTokenPayload: AccessTokenPayload = {
|
||||||
iss: this.configService.get('VITE_BASE_URL'),
|
iss: process.env.VITE_BASE_URL,
|
||||||
sub: userUid,
|
sub: userUid,
|
||||||
aud: [this.configService.get('VITE_BASE_URL')],
|
aud: [process.env.VITE_BASE_URL],
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshToken = await this.generateRefreshToken(userUid);
|
const refreshToken = await this.generateRefreshToken(userUid);
|
||||||
@@ -143,7 +137,7 @@ export class AuthService {
|
|||||||
|
|
||||||
return E.right(<AuthTokens>{
|
return E.right(<AuthTokens>{
|
||||||
access_token: await this.jwtService.sign(accessTokenPayload, {
|
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,
|
refresh_token: refreshToken.right,
|
||||||
});
|
});
|
||||||
@@ -224,14 +218,14 @@ export class AuthService {
|
|||||||
let url: string;
|
let url: string;
|
||||||
switch (origin) {
|
switch (origin) {
|
||||||
case Origin.ADMIN:
|
case Origin.ADMIN:
|
||||||
url = this.configService.get('VITE_ADMIN_URL');
|
url = process.env.VITE_ADMIN_URL;
|
||||||
break;
|
break;
|
||||||
case Origin.APP:
|
case Origin.APP:
|
||||||
url = this.configService.get('VITE_BASE_URL');
|
url = process.env.VITE_BASE_URL;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// if origin is invalid by default set URL to Hoppscotch-App
|
// 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, {
|
await this.mailerService.sendEmail(email, {
|
||||||
@@ -383,8 +377,4 @@ export class AuthService {
|
|||||||
|
|
||||||
return E.right(<IsAdmin>{ isAdmin: false });
|
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 { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.GITHUB,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.GOOGLE,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,20 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
extends AuthGuard('microsoft')
|
extends AuthGuard('microsoft')
|
||||||
implements CanActivate
|
implements CanActivate
|
||||||
{
|
{
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
canActivate(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (
|
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||||
!authProviderCheck(
|
|
||||||
AuthProvider.MICROSOFT,
|
|
||||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throwHTTPErr({
|
throwHTTPErr({
|
||||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
return super.canActivate(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Response } from 'express';
|
|||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -46,17 +45,15 @@ export const authCookieHandler = (
|
|||||||
redirect: boolean,
|
redirect: boolean,
|
||||||
redirectUrl: string | null,
|
redirectUrl: string | null,
|
||||||
) => {
|
) => {
|
||||||
const configService = new ConfigService();
|
|
||||||
|
|
||||||
const currentTime = DateTime.now();
|
const currentTime = DateTime.now();
|
||||||
const accessTokenValidity = currentTime
|
const accessTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
const refreshTokenValidity = currentTime
|
const refreshTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
|
|
||||||
@@ -78,12 +75,10 @@ export const authCookieHandler = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check to see if redirectUrl is a whitelisted url
|
// check to see if redirectUrl is a whitelisted url
|
||||||
const whitelistedOrigins = configService
|
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
|
||||||
.get('WHITELISTED_ORIGINS')
|
|
||||||
.split(',');
|
|
||||||
if (!whitelistedOrigins.includes(redirectUrl))
|
if (!whitelistedOrigins.includes(redirectUrl))
|
||||||
// if it is not redirect by default to REDIRECT_URL
|
// 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);
|
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
|
* @param provider Provider we want to check the presence of
|
||||||
* @returns Boolean if provider specified is present or not
|
* @returns Boolean if provider specified is present or not
|
||||||
*/
|
*/
|
||||||
export function authProviderCheck(
|
export function authProviderCheck(provider: string) {
|
||||||
provider: string,
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
|
||||||
) {
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||||
provider.trim().toUpperCase(),
|
provider.trim().toUpperCase(),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import { AuthService } from '../auth.service';
|
|||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubStrategy extends PassportStrategy(Strategy) {
|
export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: process.env.GITHUB_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||||
scope: [configService.get('GITHUB_SCOPE')],
|
scope: [process.env.GITHUB_SCOPE],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ import {
|
|||||||
INVALID_ACCESS_TOKEN,
|
INVALID_ACCESS_TOKEN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
constructor(
|
constructor(private usersService: UserService) {
|
||||||
private usersService: UserService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||||
(request: Request) => {
|
(request: Request) => {
|
||||||
@@ -33,7 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
return ATCookie;
|
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 { UserService } from 'src/user/user.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: process.env.MICROSOFT_CLIENT_ID,
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
scope: [process.env.MICROSOFT_SCOPE],
|
||||||
tenant: configService.get('MICROSOFT_TENANT'),
|
tenant: process.env.MICROSOFT_TENANT,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,10 @@ import {
|
|||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||||
constructor(
|
constructor(private usersService: UserService) {
|
||||||
private usersService: UserService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||||
(request: Request) => {
|
(request: Request) => {
|
||||||
@@ -32,7 +28,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
|||||||
return RTCookie;
|
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 =
|
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
||||||
'shortcode/properties_not_found' as const;
|
'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 { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||||
import { MailerService } from './mailer.service';
|
import { MailerService } from './mailer.service';
|
||||||
@@ -7,42 +7,24 @@ import {
|
|||||||
MAILER_FROM_ADDRESS_UNDEFINED,
|
MAILER_FROM_ADDRESS_UNDEFINED,
|
||||||
MAILER_SMTP_URL_UNDEFINED,
|
MAILER_SMTP_URL_UNDEFINED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
@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],
|
providers: [MailerService],
|
||||||
exports: [MailerService],
|
exports: [MailerService],
|
||||||
})
|
})
|
||||||
export class MailerModule {
|
export class MailerModule {}
|
||||||
static async register() {
|
|
||||||
const env = await loadInfraConfiguration();
|
|
||||||
|
|
||||||
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
|
|
||||||
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
|
|
||||||
|
|
||||||
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
|
|
||||||
const config = new ConfigService();
|
|
||||||
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
|
|
||||||
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: MailerModule,
|
|
||||||
imports: [
|
|
||||||
NestMailerModule.forRoot({
|
|
||||||
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
|
||||||
defaults: {
|
|
||||||
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
dir: __dirname + '/templates',
|
|
||||||
adapter: new HandlebarsAdapter(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,23 +6,18 @@ import { VersioningType } from '@nestjs/common';
|
|||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
import { checkEnvironmentAuthProvider } from './utils';
|
import { checkEnvironmentAuthProvider } from './utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
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 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(
|
app.use(
|
||||||
session({
|
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');
|
console.log('Enabling CORS with development settings');
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('Enabling CORS with production settings');
|
console.log('Enabling CORS with production settings');
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,13 +47,7 @@ async function bootstrap() {
|
|||||||
type: VersioningType.URI,
|
type: VersioningType.URI,
|
||||||
});
|
});
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
await app.listen(configService.get('PORT') || 3170);
|
await app.listen(process.env.PORT || 3170);
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
console.info('SIGTERM signal received');
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.GENERATE_GQL_SCHEMA) {
|
if (!process.env.GENERATE_GQL_SCHEMA) {
|
||||||
|
|||||||
@@ -504,24 +504,20 @@ describe('ShortcodeService', () => {
|
|||||||
);
|
);
|
||||||
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||||
{
|
{
|
||||||
id: shortcodesWithUserEmail[0].id,
|
id: shortcodes[0].id,
|
||||||
request: JSON.stringify(shortcodesWithUserEmail[0].request),
|
request: JSON.stringify(shortcodes[0].request),
|
||||||
properties: JSON.stringify(
|
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||||
shortcodesWithUserEmail[0].embedProperties,
|
createdOn: shortcodes[0].createdOn,
|
||||||
),
|
|
||||||
createdOn: shortcodesWithUserEmail[0].createdOn,
|
|
||||||
creator: {
|
creator: {
|
||||||
uid: user.uid,
|
uid: user.uid,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: shortcodesWithUserEmail[1].id,
|
id: shortcodes[1].id,
|
||||||
request: JSON.stringify(shortcodesWithUserEmail[1].request),
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
properties: JSON.stringify(
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
shortcodesWithUserEmail[1].embedProperties,
|
createdOn: shortcodes[1].createdOn,
|
||||||
),
|
|
||||||
createdOn: shortcodesWithUserEmail[1].createdOn,
|
|
||||||
creator: {
|
creator: {
|
||||||
uid: user.uid,
|
uid: user.uid,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MailerModule } from 'src/mailer/mailer.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||||
import { TeamModule } from 'src/team/team.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';
|
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
|
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
|
||||||
providers: [
|
providers: [
|
||||||
TeamInvitationService,
|
TeamInvitationService,
|
||||||
TeamInvitationResolver,
|
TeamInvitationResolver,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { validateEmail } from '../utils';
|
import { validateEmail } from '../utils';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -29,8 +28,8 @@ export class TeamInvitationService {
|
|||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,9 +150,7 @@ export class TeamInvitationService {
|
|||||||
template: 'team-invitation',
|
template: 'team-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||||
dbInvitation.id
|
|
||||||
}`,
|
|
||||||
invite_team_name: team.name,
|
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);
|
).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
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @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
|
* 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.
|
* If not, it throws an error.
|
||||||
*/
|
*/
|
||||||
export function checkEnvironmentAuthProvider(
|
export function checkEnvironmentAuthProvider() {
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||||
) {
|
|
||||||
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
|
|
||||||
throw new Error(ENV_NOT_FOUND_KEY_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);
|
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||||
(provider) => provider.toLocaleUpperCase(),
|
',',
|
||||||
);
|
).map((provider) => provider.toLocaleUpperCase());
|
||||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||||
(provider: string) => provider.toLocaleUpperCase(),
|
(provider: string) => provider.toLocaleUpperCase(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "^1.0.0",
|
||||||
"dioc": "^1.0.1",
|
"dioc": "workspace:^",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
@@ -164,4 +164,4 @@
|
|||||||
"vitest": "^0.34.6",
|
"vitest": "^0.34.6",
|
||||||
"vue-tsc": "^1.8.22"
|
"vue-tsc": "^1.8.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,10 +17,9 @@
|
|||||||
v-if="isEmpty(shortcutsResults)"
|
v-if="isEmpty(shortcutsResults)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
<details
|
<details
|
||||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -49,15 +49,13 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
:text="`${t('state.nothing_found')} ‟${search}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="search = ''"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.clear')"
|
||||||
|
outline
|
||||||
|
@click="search = ''"
|
||||||
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -254,7 +254,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -263,29 +263,27 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<span class="text-center text-secondaryLight">
|
||||||
<span class="text-center text-secondaryLight">
|
{{ t("collection.import_or_create") }}
|
||||||
{{ t("collection.import_or_create") }}
|
</span>
|
||||||
</span>
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:icon="IconImport"
|
||||||
:icon="IconImport"
|
:label="t('import.title')"
|
||||||
:label="t('import.title')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="emit('display-modal-import-export')"
|
||||||
@click="emit('display-modal-import-export')"
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="emit('display-modal-add')"
|
||||||
@click="emit('display-modal-add')"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
@@ -293,20 +291,18 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="
|
||||||
@click="
|
node.data.type === 'collections' &&
|
||||||
node.data.type === 'collections' &&
|
emit('add-folder', {
|
||||||
emit('add-folder', {
|
path: node.id,
|
||||||
path: node.id,
|
folder: node.data.data.data,
|
||||||
folder: node.data.data.data,
|
})
|
||||||
})
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
|
|||||||
@@ -274,37 +274,33 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
@drop.stop
|
@drop.stop
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<span class="text-center text-secondaryLight">
|
||||||
<span class="text-center text-secondaryLight">
|
{{ t("collection.import_or_create") }}
|
||||||
{{ t("collection.import_or_create") }}
|
</span>
|
||||||
</span>
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:icon="IconImport"
|
||||||
:icon="IconImport"
|
:label="t('import.title')"
|
||||||
:label="t('import.title')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
:disabled="hasNoTeamAccess"
|
||||||
:disabled="hasNoTeamAccess"
|
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
@click="
|
||||||
@click="
|
hasNoTeamAccess ? null : emit('display-modal-import-export')
|
||||||
hasNoTeamAccess
|
"
|
||||||
? null
|
/>
|
||||||
: emit('display-modal-import-export')
|
<HoppButtonSecondary
|
||||||
"
|
:icon="IconPlus"
|
||||||
/>
|
:label="t('add.new')"
|
||||||
<HoppButtonSecondary
|
filled
|
||||||
:icon="IconPlus"
|
outline
|
||||||
:label="t('add.new')"
|
:disabled="hasNoTeamAccess"
|
||||||
filled
|
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||||
outline
|
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
||||||
:disabled="hasNoTeamAccess"
|
/>
|
||||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
|
||||||
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
@@ -313,20 +309,18 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
@drop.stop
|
@drop.stop
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="
|
||||||
@click="
|
node.data.type === 'collections' &&
|
||||||
node.data.type === 'collections' &&
|
emit('add-folder', {
|
||||||
emit('add-folder', {
|
path: node.id,
|
||||||
path: node.id,
|
folder: node.data.data.data,
|
||||||
folder: node.data.data.data,
|
})
|
||||||
})
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
|
|||||||
@@ -180,18 +180,16 @@
|
|||||||
:alt="`${t('empty.collection')}`"
|
:alt="`${t('empty.collection')}`"
|
||||||
:text="t('empty.collection')"
|
:text="t('empty.collection')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="
|
||||||
@click="
|
emit('add-folder', {
|
||||||
emit('add-folder', {
|
path: `${collectionIndex}`,
|
||||||
path: `${collectionIndex}`,
|
})
|
||||||
})
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -176,7 +176,8 @@
|
|||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.folder')}`"
|
:alt="`${t('empty.folder')}`"
|
||||||
:text="t('empty.folder')"
|
:text="t('empty.folder')"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="t('action.search')"
|
: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
|
<div
|
||||||
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
||||||
@@ -66,36 +66,34 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<span class="text-center text-secondaryLight">
|
||||||
<span class="text-center text-secondaryLight">
|
{{ t("collection.import_or_create") }}
|
||||||
{{ t("collection.import_or_create") }}
|
</span>
|
||||||
</span>
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:icon="IconImport"
|
||||||
:icon="IconImport"
|
:label="t('import.title')"
|
||||||
:label="t('import.title')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="displayModalImportExport(true)"
|
||||||
@click="displayModalImportExport(true)"
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="displayModalAdd(true)"
|
||||||
@click="displayModalAdd(true)"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<CollectionsGraphqlAdd
|
<CollectionsGraphqlAdd
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
v-model="filterTexts"
|
v-model="filterTexts"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
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')"
|
:placeholder="t('action.search')"
|
||||||
:disabled="collectionsType.type === 'team-collections'"
|
:disabled="collectionsType.type === 'team-collections'"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,9 +11,7 @@
|
|||||||
v-if="!currentInterceptorSupportsCookies"
|
v-if="!currentInterceptorSupportsCookies"
|
||||||
:text="t('cookies.modal.interceptor_no_support')"
|
:text="t('cookies.modal.interceptor_no_support')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-col">
|
<div v-else class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
@@ -40,7 +38,8 @@
|
|||||||
:alt="`${t('cookies.modal.empty_domains')}`"
|
:alt="`${t('cookies.modal.empty_domains')}`"
|
||||||
:text="t('cookies.modal.empty_domains')"
|
:text="t('cookies.modal.empty_domains')"
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-for="[domain, entries] in workingCookieJar.entries()"
|
v-for="[domain, entries] in workingCookieJar.entries()"
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -105,11 +105,9 @@ const requestCancelFunc: Ref<(() => void) | null> = ref(null)
|
|||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const shortcodeBaseURL =
|
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
|
||||||
|
|
||||||
const sharedRequestURL = computed(() => {
|
const sharedRequestURL = computed(() => {
|
||||||
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
return `${baseURL}/r/${props.sharedRequestID}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|||||||
@@ -93,12 +93,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="myEnvironments.length === 0"
|
v-if="myEnvironments.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
>
|
||||||
:text="t('empty.environments')"
|
<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>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'team-environments'"
|
:id="'team-environments'"
|
||||||
@@ -132,12 +140,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="teamEnvironmentList.length === 0"
|
v-if="teamEnvironmentList.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
>
|
||||||
:text="t('empty.environments')"
|
<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>
|
||||||
<div
|
<div
|
||||||
v-if="!teamListLoading && teamAdapterError"
|
v-if="!teamListLoading && teamAdapterError"
|
||||||
|
|||||||
@@ -78,13 +78,11 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
@click="addEnvironmentVariable"
|
||||||
@click="addEnvironmentVariable"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,29 +38,27 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<span class="text-center text-secondaryLight">
|
||||||
<span class="text-center text-secondaryLight">
|
{{ t("environment.import_or_create") }}
|
||||||
{{ t("environment.import_or_create") }}
|
</span>
|
||||||
</span>
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:icon="IconImport"
|
||||||
:icon="IconImport"
|
:label="t('import.title')"
|
||||||
:label="t('import.title')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="displayModalImportExport(true)"
|
||||||
@click="displayModalImportExport(true)"
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="displayModalAdd(true)"
|
||||||
@click="displayModalAdd(true)"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showModalDetails"
|
:show="showModalDetails"
|
||||||
|
|||||||
@@ -81,20 +81,18 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
v-if="isViewer"
|
||||||
v-if="isViewer"
|
disabled
|
||||||
disabled
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
v-else
|
||||||
v-else
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
@click="addEnvironmentVariable"
|
||||||
@click="addEnvironmentVariable"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,33 +49,31 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<span class="text-center text-secondaryLight">
|
||||||
<span class="text-center text-secondaryLight">
|
{{ t("environment.import_or_create") }}
|
||||||
{{ t("environment.import_or_create") }}
|
</span>
|
||||||
</span>
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:icon="IconImport"
|
||||||
:icon="IconImport"
|
:label="t('import.title')"
|
||||||
:label="t('import.title')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
:disabled="isTeamViewer"
|
||||||
:disabled="isTeamViewer"
|
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
||||||
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
:disabled="isTeamViewer"
|
||||||
:disabled="isTeamViewer"
|
@click="isTeamViewer ? null : displayModalAdd(true)"
|
||||||
@click="isTeamViewer ? null : displayModalAdd(true)"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else-if="!loading">
|
<div v-else-if="!loading">
|
||||||
<EnvironmentsTeamsEnvironment
|
<EnvironmentsTeamsEnvironment
|
||||||
|
|||||||
@@ -120,16 +120,14 @@
|
|||||||
:alt="`${t('empty.authorization')}`"
|
:alt="`${t('empty.authorization')}`"
|
||||||
:text="t('empty.authorization')"
|
:text="t('empty.authorization')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
outline
|
:label="t('app.documentation')"
|
||||||
:label="t('app.documentation')"
|
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
blank
|
||||||
blank
|
:icon="IconExternalLink"
|
||||||
:icon="IconExternalLink"
|
reverse
|
||||||
reverse
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
|
|||||||
@@ -162,14 +162,12 @@
|
|||||||
:alt="`${t('empty.headers')}`"
|
:alt="`${t('empty.headers')}`"
|
||||||
:text="t('empty.headers')"
|
:text="t('empty.headers')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="addHeader"
|
||||||
@click="addHeader"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
:src="`/images/states/${colorMode.value}/add_comment.svg`"
|
:src="`/images/states/${colorMode.value}/add_comment.svg`"
|
||||||
:alt="`${t('empty.documentation')}`"
|
:alt="`${t('empty.documentation')}`"
|
||||||
:text="t('empty.documentation')"
|
:text="t('empty.documentation')"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
v-model="graphqlFieldsFilterText"
|
v-model="graphqlFieldsFilterText"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
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')}`"
|
:placeholder="`${t('action.search')}`"
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
|
||||||
>
|
>
|
||||||
<WorkspaceCurrent :section="t('tab.history')" :is-only-personal="true" />
|
<WorkspaceCurrent :section="t('tab.history')" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input
|
<input
|
||||||
v-model="filterText"
|
v-model="filterText"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
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')}`"
|
:placeholder="`${t('action.search')}`"
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -114,7 +114,8 @@
|
|||||||
:src="`/images/states/${colorMode.value}/history.svg`"
|
:src="`/images/states/${colorMode.value}/history.svg`"
|
||||||
:alt="`${t('empty.history')}`"
|
:alt="`${t('empty.history')}`"
|
||||||
:text="t('empty.history')"
|
:text="t('empty.history')"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="
|
v-else-if="
|
||||||
Object.keys(filteredHistoryGroups).length === 0 ||
|
Object.keys(filteredHistoryGroups).length === 0 ||
|
||||||
@@ -123,20 +124,18 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
filterText = ''
|
|
||||||
filterSelection = 'ALL'
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.clear')"
|
||||||
|
outline
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
filterText = ''
|
||||||
|
filterSelection = 'ALL'
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
|
|||||||
@@ -119,16 +119,14 @@
|
|||||||
:alt="`${t('empty.authorization')}`"
|
:alt="`${t('empty.authorization')}`"
|
||||||
:text="t('empty.authorization')"
|
:text="t('empty.authorization')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
outline
|
:label="t('app.documentation')"
|
||||||
:label="t('app.documentation')"
|
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
blank
|
||||||
blank
|
:icon="IconExternalLink"
|
||||||
:icon="IconExternalLink"
|
reverse
|
||||||
reverse
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
|
|||||||
@@ -112,16 +112,14 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
outline
|
:label="`${t('app.documentation')}`"
|
||||||
:label="`${t('app.documentation')}`"
|
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
||||||
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
blank
|
||||||
blank
|
:icon="IconExternalLink"
|
||||||
:icon="IconExternalLink"
|
reverse
|
||||||
reverse
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -158,14 +158,12 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
filled
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="addBodyParam"
|
||||||
@click="addBodyParam"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -209,14 +209,12 @@
|
|||||||
:alt="`${t('empty.headers')}`"
|
:alt="`${t('empty.headers')}`"
|
||||||
:text="t('empty.headers')"
|
:text="t('empty.headers')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
filled
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="addHeader"
|
||||||
@click="addHeader"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,14 +157,12 @@
|
|||||||
:alt="`${t('empty.parameters')}`"
|
:alt="`${t('empty.parameters')}`"
|
||||||
:text="t('empty.parameters')"
|
:text="t('empty.parameters')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
filled
|
||||||
filled
|
@click="addParam"
|
||||||
@click="addParam"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -180,18 +180,6 @@
|
|||||||
class="input mb-2 !bg-primaryContrast"
|
class="input mb-2 !bg-primaryContrast"
|
||||||
@keyup.enter="hide()"
|
@keyup.enter="hide()"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
|
||||||
ref="saveRequestAction"
|
|
||||||
:label="`${t('request.save_as')}`"
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
showSaveRequestModal = true
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="copyRequestAction"
|
ref="copyRequestAction"
|
||||||
:label="t('request.share_request')"
|
:label="t('request.share_request')"
|
||||||
@@ -204,6 +192,18 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="saveRequestAction"
|
||||||
|
:label="`${t('request.save_as')}`"
|
||||||
|
:icon="IconFolderPlus"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
showSaveRequestModal = true
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
|
|||||||
@@ -37,9 +37,7 @@
|
|||||||
:text="t('helpers.network_fail')"
|
:text="t('helpers.network_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<template #body>
|
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="response.type === 'script_fail'"
|
v-if="response.type === 'script_fail'"
|
||||||
@@ -49,14 +47,12 @@
|
|||||||
:text="t('helpers.script_fail')"
|
:text="t('helpers.script_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<template #body>
|
<div
|
||||||
<div
|
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
>
|
||||||
>
|
{{ response.error.name }}: {{ response.error.message }}<br />
|
||||||
{{ response.error.name }}: {{ response.error.message }}<br />
|
{{ response.error.stack }}
|
||||||
{{ response.error.stack }}
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-if="response.type === 'success' || response.type === 'fail'"
|
v-if="response.type === 'success' || response.type === 'fail'"
|
||||||
|
|||||||
@@ -159,7 +159,8 @@
|
|||||||
:alt="`${t('error.test_script_fail')}`"
|
:alt="`${t('error.test_script_fail')}`"
|
||||||
:heading="t('error.test_script_fail')"
|
:heading="t('error.test_script_fail')"
|
||||||
:text="t('helpers.test_script_fail')"
|
:text="t('helpers.test_script_fail')"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/validation.svg`"
|
:src="`/images/states/${colorMode.value}/validation.svg`"
|
||||||
@@ -167,16 +168,15 @@
|
|||||||
:heading="t('empty.tests')"
|
:heading="t('empty.tests')"
|
||||||
:text="t('helpers.tests')"
|
:text="t('helpers.tests')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
outline
|
:label="`${t('action.learn_more')}`"
|
||||||
:label="`${t('action.learn_more')}`"
|
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
|
||||||
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
|
blank
|
||||||
blank
|
:icon="IconExternalLink"
|
||||||
:icon="IconExternalLink"
|
reverse
|
||||||
reverse
|
class="my-4"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showMyEnvironmentDetailsModal"
|
:show="showMyEnvironmentDetailsModal"
|
||||||
|
|||||||
@@ -149,14 +149,12 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
filled
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="addUrlEncodedParam"
|
||||||
@click="addUrlEncodedParam"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,57 +5,55 @@
|
|||||||
:heading="t('error.network_fail')"
|
:heading="t('error.network_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<template #body>
|
<div class="my-1 flex flex-col items-center text-secondaryLight">
|
||||||
<div class="my-1 flex flex-col items-center text-secondaryLight">
|
<span>
|
||||||
<span>
|
{{ t("error.please_install_extension") }}
|
||||||
{{ t("error.please_install_extension") }}
|
</span>
|
||||||
</span>
|
<span>
|
||||||
<span>
|
{{ t("error.check_how_to_add_origin") }}
|
||||||
{{ t("error.check_how_to_add_origin") }}
|
<HoppSmartLink
|
||||||
<HoppSmartLink
|
blank
|
||||||
blank
|
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
|
||||||
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
|
class="text-accent hover:text-accentDark"
|
||||||
class="text-accent hover:text-accentDark"
|
>
|
||||||
>
|
here
|
||||||
here
|
</HoppSmartLink>
|
||||||
</HoppSmartLink>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2 py-4">
|
||||||
|
<span>
|
||||||
|
<HoppSmartItem
|
||||||
|
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
|
blank
|
||||||
|
:icon="IconChrome"
|
||||||
|
label="Chrome"
|
||||||
|
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
|
||||||
|
:active-info-icon="hasChromeExtInstalled"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<HoppSmartItem
|
||||||
|
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
|
blank
|
||||||
|
:icon="IconFirefox"
|
||||||
|
label="Firefox"
|
||||||
|
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
|
||||||
|
:active-info-icon="hasFirefoxExtInstalled"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="extensionEnabled"
|
||||||
|
@change="extensionEnabled = !extensionEnabled"
|
||||||
|
>
|
||||||
|
{{ t("settings.extensions_use_toggle") }}
|
||||||
|
</HoppSmartToggle>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2 py-4">
|
</div>
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
|
||||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
|
||||||
blank
|
|
||||||
:icon="IconChrome"
|
|
||||||
label="Chrome"
|
|
||||||
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
|
|
||||||
:active-info-icon="hasChromeExtInstalled"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
|
||||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
|
||||||
blank
|
|
||||||
:icon="IconFirefox"
|
|
||||||
label="Firefox"
|
|
||||||
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
|
|
||||||
:active-info-icon="hasFirefoxExtInstalled"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-4 py-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<HoppSmartToggle
|
|
||||||
:on="extensionEnabled"
|
|
||||||
@change="extensionEnabled = !extensionEnabled"
|
|
||||||
>
|
|
||||||
{{ t("settings.extensions_use_toggle") }}
|
|
||||||
</HoppSmartToggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -370,11 +370,10 @@ const linkVariants: LinkVariant[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const shortcodeBaseURL =
|
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
|
||||||
|
|
||||||
const copyEmbed = () => {
|
const copyEmbed = () => {
|
||||||
return `<iframe src="${shortcodeBaseURL}/e/${props.request?.id}" title="Hoppscotch Embed" style="width: 100%; height: 480px; border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.1);"></iframe>`
|
return `<iframe src="${baseURL}/e/${props.request?.id}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyButton = (
|
const copyButton = (
|
||||||
@@ -391,18 +390,18 @@ const copyButton = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "markdown") {
|
if (type === "markdown") {
|
||||||
return `[](${shortcodeBaseURL}/r/${props.request?.id})`
|
return `[](${baseURL}/r/${props.request?.id})`
|
||||||
}
|
}
|
||||||
return `<a href="${shortcodeBaseURL}/r/${props.request?.id}"><img src="${shortcodeBaseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyLink = (variationID: string) => {
|
const copyLink = (variationID: string) => {
|
||||||
if (variationID === "link1") {
|
if (variationID === "link1") {
|
||||||
return `${shortcodeBaseURL}/r/${props.request?.id}`
|
return `${baseURL}/r/${props.request?.id}`
|
||||||
} else if (variationID === "link2") {
|
} else if (variationID === "link2") {
|
||||||
return `<a href="${shortcodeBaseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
|
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
|
||||||
}
|
}
|
||||||
return `[Run in Hoppscotch](${shortcodeBaseURL}/r/${props.request?.id})`
|
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyContent = ({
|
const copyContent = ({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
to="https://docs.hoppscotch.io/documentation/features/share-requests"
|
to="https://docs.hoppscotch.io/documentation/features/shared-request"
|
||||||
blank
|
blank
|
||||||
:title="t('app.wiki')"
|
:title="t('app.wiki')"
|
||||||
:icon="IconHelpCircle"
|
:icon="IconHelpCircle"
|
||||||
@@ -32,12 +32,10 @@
|
|||||||
:alt="`${t('empty.shared_requests_logout')}`"
|
:alt="`${t('empty.shared_requests_logout')}`"
|
||||||
:text="`${t('empty.shared_requests_logout')}`"
|
:text="`${t('empty.shared_requests_logout')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:label="t('auth.login')"
|
||||||
:label="t('auth.login')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
<template v-else-if="sharedRequests.length">
|
<template v-else-if="sharedRequests.length">
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ const props = defineProps<{
|
|||||||
label?: string | undefined
|
label?: string | undefined
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const shortcodeBaseURL =
|
|
||||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
|
||||||
|
|
||||||
const text = computed(() => {
|
const text = computed(() => {
|
||||||
return props.label
|
return props.label ? t(props.label) : `hopp.sh/r/${props.link ?? "xxxx"}`
|
||||||
? t(props.label)
|
|
||||||
: `${shortcodeBaseURL}/r/${props.link ?? "xxxx"}`
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons opacity-75" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,17 +45,15 @@
|
|||||||
:alt="`${t('empty.members')}`"
|
:alt="`${t('empty.members')}`"
|
||||||
:text="t('empty.members')"
|
:text="t('empty.members')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:icon="IconUserPlus"
|
||||||
:icon="IconUserPlus"
|
:label="t('team.invite')"
|
||||||
:label="t('team.invite')"
|
@click="
|
||||||
@click="
|
() => {
|
||||||
() => {
|
emit('invite-team')
|
||||||
emit('invite-team')
|
}
|
||||||
}
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="divide-y divide-dividerLight">
|
<div v-else class="divide-y divide-dividerLight">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -125,7 +125,8 @@
|
|||||||
pendingInvites.data.right.team?.teamInvitations.length === 0
|
pendingInvites.data.right.team?.teamInvitations.length === 0
|
||||||
"
|
"
|
||||||
:text="t('empty.pending_invites')"
|
:text="t('empty.pending_invites')"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
||||||
class="flex flex-col items-center p-4"
|
class="flex flex-col items-center p-4"
|
||||||
@@ -244,13 +245,11 @@
|
|||||||
:alt="`${t('empty.invites')}`"
|
:alt="`${t('empty.invites')}`"
|
||||||
:text="`${t('empty.invites')}`"
|
:text="`${t('empty.invites')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('add.new')"
|
||||||
:label="t('add.new')"
|
filled
|
||||||
filled
|
@click="addNewInvitee"
|
||||||
@click="addNewInvitee"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -16,13 +16,11 @@
|
|||||||
:alt="`${t('empty.teams')}`"
|
:alt="`${t('empty.teams')}`"
|
||||||
:text="`${t('empty.teams')}`"
|
:text="`${t('empty.teams')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="`${t('team.create_new')}`"
|
||||||
:label="`${t('team.create_new')}`"
|
filled
|
||||||
filled
|
@click="displayModalAdd(true)"
|
||||||
@click="displayModalAdd(true)"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-else-if="!loading"
|
v-else-if="!loading"
|
||||||
|
|||||||
@@ -21,15 +21,13 @@
|
|||||||
:alt="`${t('empty.teams')}`"
|
:alt="`${t('empty.teams')}`"
|
||||||
:text="`${t('empty.teams')}`"
|
:text="`${t('empty.teams')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('team.create_new')"
|
||||||
:label="t('team.create_new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
:icon="IconPlus"
|
||||||
:icon="IconPlus"
|
@click="displayModalAdd(true)"
|
||||||
@click="displayModalAdd(true)"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else-if="!loading" class="flex flex-col">
|
<div v-else-if="!loading" class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -14,12 +14,10 @@
|
|||||||
:alt="`${t('empty.profile')}`"
|
:alt="`${t('empty.profile')}`"
|
||||||
:text="`${t('empty.profile')}`"
|
:text="`${t('empty.profile')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonPrimary
|
||||||
<HoppButtonPrimary
|
:label="t('auth.login')"
|
||||||
:label="t('auth.login')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-8">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -142,14 +142,12 @@
|
|||||||
:alt="`${t('empty.subscription')}`"
|
:alt="`${t('empty.subscription')}`"
|
||||||
:text="`${t('empty.subscription')}`"
|
:text="`${t('empty.subscription')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:label="t('mqtt.new')"
|
||||||
:label="t('mqtt.new')"
|
filled
|
||||||
filled
|
outline
|
||||||
outline
|
@click="showSubscriptionModal(true)"
|
||||||
@click="showSubscriptionModal(true)"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -197,16 +197,14 @@
|
|||||||
:alt="`${t('socketio.connection_not_authorized')}`"
|
:alt="`${t('socketio.connection_not_authorized')}`"
|
||||||
:text="`${t('socketio.connection_not_authorized')}`"
|
:text="`${t('socketio.connection_not_authorized')}`"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
outline
|
:label="t('app.documentation')"
|
||||||
:label="t('app.documentation')"
|
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
blank
|
||||||
blank
|
:icon="IconExternalLink"
|
||||||
:icon="IconExternalLink"
|
reverse
|
||||||
reverse
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-if="authType === 'Bearer'"
|
v-if="authType === 'Bearer'"
|
||||||
|
|||||||
@@ -163,7 +163,8 @@
|
|||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||||
:alt="`${t('empty.protocols')}`"
|
:alt="`${t('empty.protocols')}`"
|
||||||
:text="`${t('empty.protocols')}`"
|
:text="`${t('empty.protocols')}`"
|
||||||
/>
|
>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from ".."
|
} from ".."
|
||||||
|
|
||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
import MiniSearch from "minisearch"
|
import MiniSearch from "minisearch"
|
||||||
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
@@ -29,7 +30,6 @@ export class InterceptorSpotlightSearcherService
|
|||||||
public searcherSectionTitle = this.t("settings.interceptor")
|
public searcherSectionTitle = this.t("settings.interceptor")
|
||||||
|
|
||||||
private readonly spotlight = this.bind(SpotlightService)
|
private readonly spotlight = this.bind(SpotlightService)
|
||||||
private interceptorService = this.bind(InterceptorService)
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -37,6 +37,8 @@ export class InterceptorSpotlightSearcherService
|
|||||||
this.spotlight.registerSearcher(this)
|
this.spotlight.registerSearcher(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interceptorService = useService(InterceptorService)
|
||||||
|
|
||||||
createSearchSession(
|
createSearchSession(
|
||||||
query: Readonly<Ref<string>>
|
query: Readonly<Ref<string>>
|
||||||
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"dioc": "^1.0.1",
|
"dioc": "workspace:^",
|
||||||
"environments.api": "link:@platform/environments/environments.api",
|
"environments.api": "link:@platform/environments/environments.api",
|
||||||
"event": "link:@tauri-apps/api/event",
|
"event": "link:@tauri-apps/api/event",
|
||||||
"fp-ts": "^2.16.0",
|
"fp-ts": "^2.16.0",
|
||||||
|
|||||||
@@ -117,13 +117,7 @@
|
|||||||
alt="No invites"
|
alt="No invites"
|
||||||
text="No invites"
|
text="No invites"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary label="Add new" filled @click="addNewMember" />
|
||||||
<HoppButtonSecondary
|
|
||||||
label="Add new"
|
|
||||||
filled
|
|
||||||
@click="addNewMember"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,17 +15,15 @@
|
|||||||
v-if="team?.teamMembers?.length === 0"
|
v-if="team?.teamMembers?.length === 0"
|
||||||
text="No members in this team. Add members to this team to collaborate"
|
text="No members in this team. Add members to this team to collaborate"
|
||||||
>
|
>
|
||||||
<template #body>
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
:icon="IconUserPlus"
|
||||||
:icon="IconUserPlus"
|
:label="t('teams.add_members')"
|
||||||
:label="t('teams.add_members')"
|
@click="
|
||||||
@click="
|
() => {
|
||||||
() => {
|
showInvite = !showInvite;
|
||||||
showInvite = !showInvite;
|
}
|
||||||
}
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="divide-y divide-dividerLight">
|
<div v-else class="divide-y divide-dividerLight">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user