Compare commits

..

11 Commits

Author SHA1 Message Date
Liyas Thomas
502da61b8b fix: ui styles 2023-12-07 14:43:22 +05:30
nivedin
f2777a9a75 chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
4bd3e89f89 refactor: update embed customize flow 2023-12-07 14:43:22 +05:30
nivedin
09e9601940 chore: add save option to modify sahred request 2023-12-07 14:43:22 +05:30
nivedin
fd4a5c626f chore: disable method and input 2023-12-07 14:43:22 +05:30
nivedin
67cfef82af chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
aa18249791 chore: use shared request properties for embed 2023-12-07 14:43:22 +05:30
nivedin
9d8fdb4d04 chore: add doc button if response is null 2023-12-07 14:43:22 +05:30
nivedin
fbca9b06c3 chore: add open app link 2023-12-07 14:43:22 +05:30
nivedin
bb0bf35164 feat: added embeds components wip 2023-12-07 14:43:22 +05:30
nivedin
5a35c098ec feat: mutations and small refactor in share req 2023-12-07 14:43:22 +05:30
164 changed files with 5235 additions and 4393 deletions

View File

@@ -66,7 +66,6 @@ services:
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
container_name: hoppscotch-aio
restart: unless-stopped
build:
dockerfile: prod.Dockerfile
context: .

View File

@@ -32,9 +32,6 @@
"lint-staged": "12.4.0"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

24
packages/dioc/.gitignore vendored Normal file
View 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
View 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
View File

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

View 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()
}
}

View File

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

View 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()
}
}

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

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

View 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([])
})
})
})

View 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")
})
})
})

View 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
View File

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

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

View 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'],
}
},
})

View File

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

2
packages/dioc/vue.d.ts vendored Normal file
View File

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

View File

@@ -28,7 +28,6 @@
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1",

View File

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

View File

@@ -209,12 +209,3 @@ enum TeamMemberRole {
VIEWER
EDITOR
}
model InfraConfig {
id String @id @default(cuid())
name String @unique
value String?
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}

View File

@@ -4,6 +4,7 @@ import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module';
import { MailerModule } from '../mailer/mailer.module';
import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
@@ -11,20 +12,19 @@ import { TeamCollectionModule } from '../team-collection/team-collection.module'
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({
imports: [
PrismaModule,
PubSubModule,
UserModule,
MailerModule,
TeamModule,
TeamInvitationModule,
TeamEnvironmentsModule,
TeamCollectionModule,
TeamRequestModule,
ShortcodeModule,
InfraConfigModule,
],
providers: [InfraResolver, AdminResolver, AdminService],
exports: [AdminService],

View File

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

View File

@@ -25,7 +25,6 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AdminService {
@@ -40,7 +39,6 @@ export class AdminService {
private readonly prisma: PrismaService,
private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
private readonly configService: ConfigService,
) {}
/**
@@ -81,7 +79,7 @@ export class AdminService {
template: 'user-invitation',
variables: {
inviteeEmail: inviteeEmail,
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
magicLink: `${process.env.VITE_BASE_URL}`,
},
});
} catch (e) {

View File

@@ -1,12 +1,5 @@
import { UseGuards } from '@nestjs/common';
import {
Args,
ID,
Mutation,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model';
import { AdminService } from './admin.service';
@@ -23,21 +16,11 @@ import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
import { InfraConfig } from 'src/infra-config/infra-config.model';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
import {
EnableAndDisableSSOArgs,
InfraConfigArgs,
} from 'src/infra-config/input-args';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra)
export class InfraResolver {
constructor(
private adminService: AdminService,
private infraConfigService: InfraConfigService,
) {}
constructor(private adminService: AdminService) {}
@Query(() => Infra, {
description: 'Fetch details of the Infrastructure',
@@ -239,76 +222,4 @@ export class InfraResolver {
userEmail,
);
}
@Query(() => [InfraConfig], {
description: 'Retrieve configuration details for the instance',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async infraConfigs(
@Args({
name: 'configNames',
type: () => [InfraConfigEnumForClient],
description: 'Configs to fetch',
})
names: InfraConfigEnumForClient[],
) {
const infraConfigs = await this.infraConfigService.getMany(names);
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
return infraConfigs.right;
}
@Query(() => [String], {
description: 'Allowed Auth Provider list',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
allowedAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
/* Mutations */
@Mutation(() => [InfraConfig], {
description: 'Update Infra Configs',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async updateInfraConfigs(
@Args({
name: 'infraConfigs',
type: () => [InfraConfigArgs],
description: 'InfraConfigs to update',
})
infraConfigs: InfraConfigArgs[],
) {
const updatedRes = await this.infraConfigService.updateMany(infraConfigs);
if (E.isLeft(updatedRes)) throwErr(updatedRes.left);
return updatedRes.right;
}
@Mutation(() => Boolean, {
description: 'Reset Infra Configs with default values (.env)',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async resetInfraConfigs() {
const resetRes = await this.infraConfigService.reset();
if (E.isLeft(resetRes)) throwErr(resetRes.left);
return true;
}
@Mutation(() => Boolean, {
description: 'Enable or Disable SSO for login/signup',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async enableAndDisableSSO(
@Args({
name: 'providerInfo',
type: () => [EnableAndDisableSSOArgs],
description: 'SSO provider and status',
})
providerInfo: EnableAndDisableSSOArgs[],
) {
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;
}
}

View File

@@ -20,69 +20,51 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
import { COOKIES_NOT_FOUND } from './errors';
import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { InfraConfigModule } from './infra-config/infra-config.module';
import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [async () => loadInfraConfiguration()],
}),
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
playground: configService.get('PRODUCTION') !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (_, websocket) => {
try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
},
},
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
};
GraphQLModule.forRoot<ApolloDriverConfig>({
buildSchemaOptions: {
numberScalarMode: 'integer',
},
}),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => [
{
ttl: +configService.get('RATE_LIMIT_TTL'),
limit: +configService.get('RATE_LIMIT_MAX'),
playground: process.env.PRODUCTION !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (_, websocket) => {
try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
},
],
},
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
driver: ApolloDriver,
}),
MailerModule.register(),
ThrottlerModule.forRoot([
{
ttl: +process.env.RATE_LIMIT_TTL,
limit: +process.env.RATE_LIMIT_MAX,
},
]),
UserModule,
AuthModule.register(),
AuthModule,
AdminModule,
UserSettingsModule,
UserEnvironmentsModule,
@@ -95,7 +77,6 @@ import { MailerModule } from './mailer/mailer.module';
TeamInvitationModule,
UserCollectionModule,
ShortcodeModule,
InfraConfigModule,
],
providers: [GQLComplexityPlugin],
controllers: [AppController],

View File

@@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
InternalServerErrorException,
Post,
Query,
Request,
@@ -30,21 +31,11 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })
export class AuthController {
constructor(
private authService: AuthService,
private configService: ConfigService,
) {}
@Get('providers')
async getAuthProviders() {
const providers = await this.authService.getAuthProviders();
return { providers };
}
constructor(private authService: AuthService) {}
/**
** Route to initiate magic-link auth for a users email
@@ -54,14 +45,8 @@ export class AuthController {
@Body() authData: SignInMagicDto,
@Query('origin') origin: string,
) {
if (
!authProviderCheck(
AuthProvider.EMAIL,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
if (!authProviderCheck(AuthProvider.EMAIL))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
const deviceIdToken = await this.authService.signInMagicLink(
authData.email,

View File

@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
@@ -11,47 +12,25 @@ import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
import { AuthProvider, authProviderCheck } from './helper';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({
imports: [
PrismaModule,
UserModule,
MailerModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
}),
JwtModule.register({
secret: process.env.JWT_SECRET,
}),
InfraConfigModule,
],
providers: [AuthService, JwtStrategy, RTJwtStrategy],
providers: [
AuthService,
JwtStrategy,
RTJwtStrategy,
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
],
controllers: [AuthController],
})
export class AuthModule {
static async register() {
const env = await loadInfraConfiguration();
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
const providers = [
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
? [GoogleStrategy]
: []),
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
? [GithubStrategy]
: []),
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
? [MicrosoftStrategy]
: []),
];
return {
module: AuthModule,
providers,
};
}
}
export class AuthModule {}

View File

@@ -21,26 +21,15 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
import { DateTime } from 'luxon';
import * as argon2 from 'argon2';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
const mockPrisma = mockDeep<PrismaService>();
const mockUser = mockDeep<UserService>();
const mockJWT = mockDeep<JwtService>();
const mockMailer = mockDeep<MailerService>();
const mockConfigService = mockDeep<ConfigService>();
const mockInfraConfigService = mockDeep<InfraConfigService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const authService = new AuthService(
mockUser,
mockPrisma,
mockJWT,
mockMailer,
mockConfigService,
mockInfraConfigService,
);
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
const currentTime = new Date();
@@ -102,8 +91,6 @@ describe('signInMagicLink', () => {
mockUser.createUserViaMagicLink.mockResolvedValue(user);
// create new entry in VerificationToken table
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
mockConfigService.get.mockReturnValue('3');
const result = await authService.signInMagicLink(
'dwight@dundermifflin.com',

View File

@@ -28,8 +28,6 @@ import { AuthError } from 'src/types/AuthError';
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
import { VerificationToken } from '@prisma/client';
import { Origin } from './helper';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
@Injectable()
export class AuthService {
@@ -38,8 +36,6 @@ export class AuthService {
private prismaService: PrismaService,
private jwtService: JwtService,
private readonly mailerService: MailerService,
private readonly configService: ConfigService,
private infraConfigService: InfraConfigService,
) {}
/**
@@ -50,12 +46,10 @@ export class AuthService {
*/
private async generateMagicLinkTokens(user: AuthUser) {
const salt = await bcrypt.genSalt(
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
);
const expiresOn = DateTime.now()
.plus({
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
})
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
.toISO()
.toString();
@@ -101,13 +95,13 @@ export class AuthService {
*/
private async generateRefreshToken(userUid: string) {
const refreshTokenPayload: RefreshTokenPayload = {
iss: this.configService.get('VITE_BASE_URL'),
iss: process.env.VITE_BASE_URL,
sub: userUid,
aud: [this.configService.get('VITE_BASE_URL')],
aud: [process.env.VITE_BASE_URL],
};
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
});
const refreshTokenHash = await argon2.hash(refreshToken);
@@ -133,9 +127,9 @@ export class AuthService {
*/
async generateAuthTokens(userUid: string) {
const accessTokenPayload: AccessTokenPayload = {
iss: this.configService.get('VITE_BASE_URL'),
iss: process.env.VITE_BASE_URL,
sub: userUid,
aud: [this.configService.get('VITE_BASE_URL')],
aud: [process.env.VITE_BASE_URL],
};
const refreshToken = await this.generateRefreshToken(userUid);
@@ -143,7 +137,7 @@ export class AuthService {
return E.right(<AuthTokens>{
access_token: await this.jwtService.sign(accessTokenPayload, {
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
}),
refresh_token: refreshToken.right,
});
@@ -224,14 +218,14 @@ export class AuthService {
let url: string;
switch (origin) {
case Origin.ADMIN:
url = this.configService.get('VITE_ADMIN_URL');
url = process.env.VITE_ADMIN_URL;
break;
case Origin.APP:
url = this.configService.get('VITE_BASE_URL');
url = process.env.VITE_BASE_URL;
break;
default:
// if origin is invalid by default set URL to Hoppscotch-App
url = this.configService.get('VITE_BASE_URL');
url = process.env.VITE_BASE_URL;
}
await this.mailerService.sendEmail(email, {
@@ -383,8 +377,4 @@ export class AuthService {
return E.right(<IsAdmin>{ isAdmin: false });
}
getAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
}

View File

@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (
!authProviderCheck(
AuthProvider.GITHUB,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
if (!authProviderCheck(AuthProvider.GITHUB))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context);
}

View File

@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (
!authProviderCheck(
AuthProvider.GOOGLE,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
if (!authProviderCheck(AuthProvider.GOOGLE))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context);
}

View File

@@ -3,31 +3,20 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MicrosoftSSOGuard
extends AuthGuard('microsoft')
implements CanActivate
{
constructor(private readonly configService: ConfigService) {
super();
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (
!authProviderCheck(
AuthProvider.MICROSOFT,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
if (!authProviderCheck(AuthProvider.MICROSOFT))
throwHTTPErr({
message: AUTH_PROVIDER_NOT_SPECIFIED,
statusCode: 404,
});
}
return super.canActivate(context);
}

View File

@@ -6,7 +6,6 @@ import { Response } from 'express';
import * as cookie from 'cookie';
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
import { throwErr } from 'src/utils';
import { ConfigService } from '@nestjs/config';
enum AuthTokenType {
ACCESS_TOKEN = 'access_token',
@@ -46,17 +45,15 @@ export const authCookieHandler = (
redirect: boolean,
redirectUrl: string | null,
) => {
const configService = new ConfigService();
const currentTime = DateTime.now();
const accessTokenValidity = currentTime
.plus({
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
})
.toMillis();
const refreshTokenValidity = currentTime
.plus({
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
})
.toMillis();
@@ -78,12 +75,10 @@ export const authCookieHandler = (
}
// check to see if redirectUrl is a whitelisted url
const whitelistedOrigins = configService
.get('WHITELISTED_ORIGINS')
.split(',');
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
if (!whitelistedOrigins.includes(redirectUrl))
// if it is not redirect by default to REDIRECT_URL
redirectUrl = configService.get('REDIRECT_URL');
redirectUrl = process.env.REDIRECT_URL;
return res.status(HttpStatus.OK).redirect(redirectUrl);
};
@@ -117,16 +112,13 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
* @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not
*/
export function authProviderCheck(
provider: string,
VITE_ALLOWED_AUTH_PROVIDERS: string,
) {
export function authProviderCheck(provider: string) {
if (!provider) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
}
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(),
)
: [];

View File

@@ -5,20 +5,18 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy) {
constructor(
private authService: AuthService,
private usersService: UserService,
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
scope: [configService.get('GITHUB_SCOPE')],
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL,
scope: [process.env.GITHUB_SCOPE],
store: true,
});
}

View File

@@ -5,20 +5,18 @@ import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option';
import { AuthService } from '../auth.service';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor(
private usersService: UserService,
private authService: AuthService,
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
scope: configService.get('GOOGLE_SCOPE').split(','),
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: process.env.GOOGLE_SCOPE.split(','),
passReqToCallback: true,
store: true,
});

View File

@@ -15,14 +15,10 @@ import {
INVALID_ACCESS_TOKEN,
USER_NOT_FOUND,
} from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private usersService: UserService,
private configService: ConfigService,
) {
constructor(private usersService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
@@ -33,7 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return ATCookie;
},
]),
secretOrKey: configService.get('JWT_SECRET'),
secretOrKey: process.env.JWT_SECRET,
});
}

View File

@@ -5,21 +5,19 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
constructor(
private authService: AuthService,
private usersService: UserService,
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
scope: [configService.get('MICROSOFT_SCOPE')],
tenant: configService.get('MICROSOFT_TENANT'),
clientID: process.env.MICROSOFT_CLIENT_ID,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
scope: [process.env.MICROSOFT_SCOPE],
tenant: process.env.MICROSOFT_TENANT,
store: true,
});
}

View File

@@ -14,14 +14,10 @@ import {
USER_NOT_FOUND,
} from 'src/errors';
import * as O from 'fp-ts/Option';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(
private usersService: UserService,
private configService: ConfigService,
) {
constructor(private usersService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
@@ -32,7 +28,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
return RTCookie;
},
]),
secretOrKey: configService.get('JWT_SECRET'),
secretOrKey: process.env.JWT_SECRET,
});
}

View File

@@ -644,41 +644,3 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
*/
export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const;
/**
* Infra Config not found
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_FOUND = 'infra_config/not_found' as const;
/**
* Infra Config update failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_UPDATE_FAILED = 'infra_config/update_failed' as const;
/**
* Infra Config not listed for onModuleInit creation
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_LISTED =
'infra_config/properly_not_listed' as const;
/**
* Infra Config reset failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
/**
* Infra Config invalid input for Config variable
* (InfraConfigService)
*/
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
/**
* Error message for when the database table does not exist
* (InfraConfigService)
*/
export const DATABASE_TABLE_NOT_EXIST =
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/faq';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Global, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailerService } from './mailer.service';
@@ -7,42 +7,24 @@ import {
MAILER_FROM_ADDRESS_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED,
} from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
@Global()
@Module({
imports: [],
imports: [
NestMailerModule.forRoot({
transport:
process.env.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from:
process.env.MAILER_ADDRESS_FROM ??
throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
providers: [MailerService],
exports: [MailerService],
})
export class MailerModule {
static async register() {
const env = await loadInfraConfiguration();
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
const config = new ConfigService();
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
}
return {
module: MailerModule,
imports: [
NestMailerModule.forRoot({
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
};
}
}
export class MailerModule {}

View File

@@ -6,23 +6,18 @@ import { VersioningType } from '@nestjs/common';
import * as session from 'express-session';
import { emitGQLSchemaFile } from './gql-schema';
import { checkEnvironmentAuthProvider } from './utils';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
checkEnvironmentAuthProvider();
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
console.log(`Port: ${configService.get('PORT')}`);
checkEnvironmentAuthProvider(
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
);
app.use(
session({
secret: configService.get('SESSION_SECRET'),
secret: process.env.SESSION_SECRET,
}),
);
@@ -33,18 +28,18 @@ async function bootstrap() {
}),
);
if (configService.get('PRODUCTION') === 'false') {
if (process.env.PRODUCTION === 'false') {
console.log('Enabling CORS with development settings');
app.enableCors({
origin: configService.get('WHITELISTED_ORIGINS').split(','),
origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true,
});
} else {
console.log('Enabling CORS with production settings');
app.enableCors({
origin: configService.get('WHITELISTED_ORIGINS').split(','),
origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true,
});
}
@@ -52,13 +47,7 @@ async function bootstrap() {
type: VersioningType.URI,
});
app.use(cookieParser());
await app.listen(configService.get('PORT') || 3170);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.info('SIGTERM signal received');
await app.close();
});
await app.listen(process.env.PORT || 3170);
}
if (!process.env.GENERATE_GQL_SCHEMA) {

View File

@@ -504,24 +504,20 @@ describe('ShortcodeService', () => {
);
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{
id: shortcodesWithUserEmail[0].id,
request: JSON.stringify(shortcodesWithUserEmail[0].request),
properties: JSON.stringify(
shortcodesWithUserEmail[0].embedProperties,
),
createdOn: shortcodesWithUserEmail[0].createdOn,
id: shortcodes[0].id,
request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties),
createdOn: shortcodes[0].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
{
id: shortcodesWithUserEmail[1].id,
request: JSON.stringify(shortcodesWithUserEmail[1].request),
properties: JSON.stringify(
shortcodesWithUserEmail[1].embedProperties,
),
createdOn: shortcodesWithUserEmail[1].createdOn,
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
creator: {
uid: user.uid,
email: user.email,

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module';
import { TeamModule } from 'src/team/team.module';
@@ -11,7 +12,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
@Module({
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
providers: [
TeamInvitationService,
TeamInvitationResolver,

View File

@@ -20,7 +20,6 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class TeamInvitationService {
@@ -29,8 +28,8 @@ export class TeamInvitationService {
private readonly userService: UserService,
private readonly teamService: TeamService,
private readonly mailerService: MailerService,
private readonly pubsub: PubSubService,
private readonly configService: ConfigService,
) {}
/**
@@ -151,9 +150,7 @@ export class TeamInvitationService {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
dbInvitation.id
}`,
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
invite_team_name: team.name,
},
});

View File

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

View File

@@ -131,26 +131,6 @@ export const validateEmail = (email: string) => {
).test(email);
};
/**
* Checks to see if the URL is valid or not
* @param url The URL to validate
* @returns boolean
*/
export const validateSMTPUrl = (url: string) => {
// Possible valid formats
// smtp(s)://mail.example.com
// smtp(s)://user:pass@mail.example.com
// smtp(s)://mail.example.com:587
// smtp(s)://user:pass@mail.example.com:587
if (!url || url.length === 0) return false;
const regex =
/^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
if (regex.test(url)) return true;
return false;
};
/**
* String to JSON parser
* @param {str} str The string to parse
@@ -181,23 +161,21 @@ export function isValidLength(title: string, length: number) {
/**
* This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error.
*/
export function checkEnvironmentAuthProvider(
VITE_ALLOWED_AUTH_PROVIDERS: string,
) {
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
}
if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
}
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
(provider) => provider.toLocaleUpperCase(),
);
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
',',
).map((provider) => provider.toLocaleUpperCase());
const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(),
);

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/commands/**/*.*.ts",
"**/src/__tests__/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -19,9 +19,8 @@
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"do-test": "pnpm test"
"test": "pnpm run build && jest && rm -rf dist"
},
"keywords": [
"cli",

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await execAsync(cmd);
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,6 +1,7 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -12,7 +12,7 @@
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,24 +6,23 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
import { flow, pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as O from "fp-ts/Option";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and

View File

@@ -1,19 +1,17 @@
import { HoppRESTRequest } from "@hoppscotch/data";
import { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as RA from "fp-ts/ReadonlyArray";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -38,7 +36,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
runTestScript(testScript, envs, response)
execTestScript(testScript, envs, response)
)
)
),

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>

Before

Width:  |  Height:  |  Size: 610 B

View File

@@ -50,7 +50,7 @@
"axios": "^1.6.2",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "^1.0.1",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
@@ -164,4 +164,4 @@
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
}
}
}

View File

@@ -164,12 +164,6 @@ declare module 'vue' {
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']

View File

@@ -305,9 +305,10 @@ watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return
}
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
} else {
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
}
})

View File

@@ -17,10 +17,9 @@
v-if="isEmpty(shortcutsResults)"
:text="`${t('state.nothing_found')} ‟${filterText}”`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</HoppSmartPlaceholder>
<details
v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else

View File

@@ -49,15 +49,13 @@
:text="`${t('state.nothing_found')} ‟${search}”`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
<template #body>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</HoppSmartPlaceholder>
</div>
<div
@@ -114,7 +112,6 @@ import {
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n()
@@ -144,10 +141,6 @@ useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)
)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()

View File

@@ -254,7 +254,7 @@
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
@@ -263,29 +263,27 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</template>
</div>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -293,20 +291,18 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -274,37 +274,33 @@
:text="t('empty.collections')"
@drop.stop
>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess
? null
: emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
</div>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess ? null : emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
</div>
</template>
</div>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -313,20 +309,18 @@
:text="t('empty.collections')"
@drop.stop
>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -180,18 +180,16 @@
:alt="`${t('empty.collection')}`"
:text="t('empty.collection')"
>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
</template>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -176,7 +176,8 @@
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
/>
>
</HoppSmartPlaceholder>
</div>
</div>
<HoppSmartConfirmModal

View File

@@ -11,7 +11,7 @@
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="flex w-full bg-transparent px-4 py-2 h-8"
class="!border-0 bg-transparent py-2 pl-4 pr-2"
/>
<div
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
@@ -66,36 +66,34 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</div>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</div>
</template>
</div>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
</HoppSmartPlaceholder>
<CollectionsGraphqlAdd

View File

@@ -22,7 +22,7 @@
v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2 h-8"
class="flex w-full bg-transparent px-4 py-2"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>

View File

@@ -11,9 +11,7 @@
v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')"
>
<template #body>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</template>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</HoppSmartPlaceholder>
<div v-else class="flex flex-col">
<div
@@ -40,7 +38,8 @@
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
/>
>
</HoppSmartPlaceholder>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else

View File

@@ -1,17 +1,17 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<header
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
>
<div class="flex items-center justify-between flex-1 space-x-2">
<div class="flex flex-1 items-center justify-between space-x-2">
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="https://hoppscotch.io"
to="https://hoppscotch.io/"
blank
/>
<div class="flex">
<HoppButtonSecondary
<HoppSmartItem
:label="t('app.open_in_hoppscotch')"
:to="sharedRequestURL"
blank
@@ -19,57 +19,63 @@
</div>
</div>
</header>
<div class="sticky top-0 z-10 flex-1">
<div class="flex-1">
<div
class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
>
<div
class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
>
<span
class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
>
{{ tab.document.request.method }}
</span>
<div class="relative flex">
<span
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
>
{{ tab.document.request.method }}
</span>
</div>
<div
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
>
{{ tab.document.request.endpoint }}
<input
name="method"
:value="tab.document.request.endpoint"
class="flex-1 px-4 bg-primary"
disabled
/>
</div>
</div>
<div class="flex mt-2 space-x-2 sm:mt-0">
<div class="mt-2 flex sm:mt-0">
<HoppButtonPrimary
id="send"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="flex-1 min-w-20"
outline
class="min-w-20 flex-1"
@click="!loading ? newSendRequest() : cancelRequest()"
/>
<div class="flex">
<HoppButtonSecondary
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="t('request.save')"
filled
:icon="IconSave"
class="flex-1 rounded"
blank
outline
:to="sharedRequestURL"
/>
</div>
<HoppButtonSecondary
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="t('request.save')"
filled
:icon="IconSave"
class="flex-1 rounded rounded-r-none"
blank
:to="sharedRequestURL"
/>
</div>
</div>
</div>
<HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="selectedOptionTab"
:properties="properties"
/>
<HttpResponse :document="tab.document" :is-embed="true" />
</div>
</template>
@@ -105,11 +111,9 @@ const requestCancelFunc: Ref<(() => void) | null> = ref(null)
const loading = ref(false)
const shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const sharedRequestURL = computed(() => {
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
return `${baseURL}/r/${props.sharedRequestID}`
})
const { subscribeToStream } = useStreamSubscriber()

View File

@@ -93,12 +93,20 @@
}
"
/>
<HoppSmartPlaceholder
<div
v-if="myEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
@@ -132,12 +140,20 @@
}
"
/>
<HoppSmartPlaceholder
<div
v-if="teamEnvironmentList.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"

View File

@@ -78,13 +78,11 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -38,29 +38,27 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
</div>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
</div>
</template>
</div>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showModalDetails"

View File

@@ -81,20 +81,18 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<template #body>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -49,33 +49,31 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
</div>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
</div>
</template>
</div>
</HoppSmartPlaceholder>
<div v-else-if="!loading">
<EnvironmentsTeamsEnvironment

View File

@@ -120,16 +120,14 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -162,14 +162,12 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</template>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -20,7 +20,8 @@
:src="`/images/states/${colorMode.value}/add_comment.svg`"
:alt="`${t('empty.documentation')}`"
:text="t('empty.documentation')"
/>
>
</HoppSmartPlaceholder>
<div v-else>
<div
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
@@ -29,7 +30,7 @@
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2 h-8"
class="flex w-full bg-transparent px-4 py-2"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">

View File

@@ -3,13 +3,13 @@
<div
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">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2 h-8"
class="flex w-full bg-transparent px-4 py-2"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
@@ -114,7 +114,8 @@
:src="`/images/states/${colorMode.value}/history.svg`"
:alt="`${t('empty.history')}`"
:text="t('empty.history')"
/>
>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="
Object.keys(filteredHistoryGroups).length === 0 ||
@@ -123,20 +124,18 @@
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
<template #body>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="
() => {
filterText = ''
filterSelection = 'ALL'
}
"
/>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="
() => {
filterText = ''
filterSelection = 'ALL'
}
"
/>
</HoppSmartPlaceholder>
<HoppSmartConfirmModal
:show="confirmRemove"

View File

@@ -119,16 +119,14 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -112,16 +112,14 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
</template>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -158,14 +158,12 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
</template>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -61,7 +61,7 @@
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
</HoppSmartPlaceholder>
</div>

View File

@@ -209,14 +209,12 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
</template>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -157,14 +157,12 @@
:alt="`${t('empty.parameters')}`"
:text="t('empty.parameters')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
</template>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -180,18 +180,6 @@
class="input mb-2 !bg-primaryContrast"
@keyup.enter="hide()"
/>
<HoppSmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
:icon="IconFolderPlus"
@click="
() => {
showSaveRequestModal = true
hide()
}
"
/>
<hr />
<HoppSmartItem
ref="copyRequestAction"
: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>
</template>
</tippy>

View File

@@ -37,9 +37,7 @@
:text="t('helpers.network_fail')"
large
>
<template #body>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</template>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="response.type === 'script_fail'"
@@ -49,14 +47,12 @@
:text="t('helpers.script_fail')"
large
>
<template #body>
<div
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.stack }}
</div>
</template>
<div
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.stack }}
</div>
</HoppSmartPlaceholder>
<div
v-if="response.type === 'success' || response.type === 'fail'"

View File

@@ -41,7 +41,7 @@
<div class="divide-y divide-dividerLight">
<div
v-if="noEnvSelected && !globalHasAdditions"
class="flex bg-bannerInfo p-4 text-secondaryDark"
class="flex bg-info p-4 text-secondaryDark"
role="alert"
>
<icon-lucide-alert-triangle class="svg-icons mr-4" />
@@ -159,7 +159,8 @@
:alt="`${t('error.test_script_fail')}`"
:heading="t('error.test_script_fail')"
:text="t('helpers.test_script_fail')"
/>
>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else
:src="`/images/states/${colorMode.value}/validation.svg`"
@@ -167,16 +168,15 @@
:heading="t('empty.tests')"
:text="t('helpers.tests')"
>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
/>
</template>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
class="my-4"
/>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showMyEnvironmentDetailsModal"

View File

@@ -149,14 +149,12 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
</template>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -5,57 +5,55 @@
:heading="t('error.network_fail')"
large
>
<template #body>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</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 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>
</template>
</div>
</HoppSmartPlaceholder>
</template>

View File

@@ -26,7 +26,7 @@
</div>
<div
v-else-if="myTeams.length"
class="bg-bannerInfo flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
>
<h2 class="font-bold text-red-500">
{{ t("error.danger_zone") }}
@@ -45,7 +45,7 @@
</div>
<div v-else>
<div
class="bg-bannerInfo mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
class="bg-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
>
<h2 class="font-bold text-red-500">
{{ t("error.danger_zone") }}

View File

@@ -306,8 +306,9 @@ const embedThemeIcon = computed(() => {
return IconMonitor
} else if (embedOptions.value.theme === "light") {
return IconSun
} else {
return IconMoon
}
return IconMoon
})
const removeEmbedOption = (option: EmbedTabs) => {
@@ -370,11 +371,10 @@ const linkVariants: LinkVariant[] = [
},
]
const shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const 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 = (
@@ -391,18 +391,20 @@ const copyButton = (
}
if (type === "markdown") {
return `[![Run in Hoppscotch](${shortcodeBaseURL}/${badge})](${shortcodeBaseURL}/r/${props.request?.id})`
return `[![Run in Hoppscotch](${baseURL}/${badge})](${baseURL}/r/${props.request?.id})`
} else {
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
}
return `<a href="${shortcodeBaseURL}/r/${props.request?.id}"><img src="${shortcodeBaseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
}
const copyLink = (variationID: string) => {
if (variationID === "link1") {
return `${shortcodeBaseURL}/r/${props.request?.id}`
return `${baseURL}/r/${props.request?.id}`
} 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>`
} else {
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
}
return `[Run in Hoppscotch](${shortcodeBaseURL}/r/${props.request?.id})`
}
const copyContent = ({

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