Compare commits
1 Commits
2024.3.3-m
...
patch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a3ae231b |
@@ -47,7 +47,6 @@ RATE_LIMIT_MAX=100 # Max requests per IP
|
|||||||
|
|
||||||
|
|
||||||
# Base URLs
|
# Base URLs
|
||||||
VITE_BACKEND_LOGIN_API_URL=http://localhost:5444
|
|
||||||
VITE_BASE_URL=http://localhost:3000
|
VITE_BASE_URL=http://localhost:3000
|
||||||
VITE_SHORTCODE_BASE_URL=http://localhost:3000
|
VITE_SHORTCODE_BASE_URL=http://localhost:3000
|
||||||
VITE_ADMIN_URL=http://localhost:3100
|
VITE_ADMIN_URL=http://localhost:3100
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ describe('AdminService', () => {
|
|||||||
NOT: {
|
NOT: {
|
||||||
inviteeEmail: {
|
inviteeEmail: {
|
||||||
in: [dbAdminUsers[0].email],
|
in: [dbAdminUsers[0].email],
|
||||||
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -229,7 +230,10 @@ describe('AdminService', () => {
|
|||||||
|
|
||||||
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
|
inviteeEmail: {
|
||||||
|
in: [invitedUsers[0].inviteeEmail],
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(result).toEqualRight(true);
|
expect(result).toEqualRight(true);
|
||||||
|
|||||||
@@ -89,12 +89,17 @@ export class AdminService {
|
|||||||
adminEmail: string,
|
adminEmail: string,
|
||||||
inviteeEmail: string,
|
inviteeEmail: string,
|
||||||
) {
|
) {
|
||||||
if (inviteeEmail == adminEmail) return E.left(DUPLICATE_EMAIL);
|
if (inviteeEmail.toLowerCase() == adminEmail.toLowerCase()) {
|
||||||
|
return E.left(DUPLICATE_EMAIL);
|
||||||
|
}
|
||||||
if (!validateEmail(inviteeEmail)) return E.left(INVALID_EMAIL);
|
if (!validateEmail(inviteeEmail)) return E.left(INVALID_EMAIL);
|
||||||
|
|
||||||
const alreadyInvitedUser = await this.prisma.invitedUsers.findFirst({
|
const alreadyInvitedUser = await this.prisma.invitedUsers.findFirst({
|
||||||
where: {
|
where: {
|
||||||
inviteeEmail: inviteeEmail,
|
inviteeEmail: {
|
||||||
|
equals: inviteeEmail,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
|
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
|
||||||
@@ -159,7 +164,7 @@ export class AdminService {
|
|||||||
try {
|
try {
|
||||||
await this.prisma.invitedUsers.deleteMany({
|
await this.prisma.invitedUsers.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
inviteeEmail: { in: inviteeEmails },
|
inviteeEmail: { in: inviteeEmails, mode: 'insensitive' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -189,6 +194,7 @@ export class AdminService {
|
|||||||
NOT: {
|
NOT: {
|
||||||
inviteeEmail: {
|
inviteeEmail: {
|
||||||
in: userEmailObjs.map((user) => user.email),
|
in: userEmailObjs.map((user) => user.email),
|
||||||
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -299,7 +299,10 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
where: userEmail
|
where: userEmail
|
||||||
? {
|
? {
|
||||||
User: {
|
User: {
|
||||||
email: userEmail,
|
email: {
|
||||||
|
equals: userEmail,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -75,12 +75,13 @@ export class TeamInvitationService {
|
|||||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
|
const teamInvite = await this.prisma.teamInvitation.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
teamID_inviteeEmail: {
|
inviteeEmail: {
|
||||||
inviteeEmail: inviteeEmail,
|
equals: inviteeEmail,
|
||||||
teamID: teamID,
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
|
teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ beforeEach(() => {
|
|||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
describe('findUserByEmail', () => {
|
describe('findUserByEmail', () => {
|
||||||
test('should successfully return a valid user given a valid email', async () => {
|
test('should successfully return a valid user given a valid email', async () => {
|
||||||
mockPrisma.user.findUniqueOrThrow.mockResolvedValueOnce(user);
|
mockPrisma.user.findFirst.mockResolvedValueOnce(user);
|
||||||
|
|
||||||
const result = await userService.findUserByEmail(
|
const result = await userService.findUserByEmail(
|
||||||
'dwight@dundermifflin.com',
|
'dwight@dundermifflin.com',
|
||||||
@@ -158,7 +158,7 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return a null user given a invalid email', async () => {
|
test('should return a null user given a invalid email', async () => {
|
||||||
mockPrisma.user.findUniqueOrThrow.mockRejectedValueOnce('NotFoundError');
|
mockPrisma.user.findFirst.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
const result = await userService.findUserByEmail('jim@dundermifflin.com');
|
const result = await userService.findUserByEmail('jim@dundermifflin.com');
|
||||||
expect(result).resolves.toBeNone;
|
expect(result).resolves.toBeNone;
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ export class UserService {
|
|||||||
* @returns Option of found User
|
* @returns Option of found User
|
||||||
*/
|
*/
|
||||||
async findUserByEmail(email: string): Promise<O.None | O.Some<AuthUser>> {
|
async findUserByEmail(email: string): Promise<O.None | O.Some<AuthUser>> {
|
||||||
try {
|
const user = await this.prisma.user.findFirst({
|
||||||
const user = await this.prisma.user.findUniqueOrThrow({
|
|
||||||
where: {
|
where: {
|
||||||
email: email,
|
email: {
|
||||||
|
equals: email,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!user) return O.none;
|
||||||
return O.some(user);
|
return O.some(user);
|
||||||
} catch (error) {
|
|
||||||
return O.none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,20 +14,3 @@ Since TypeScript cannot handle type information for `.vue` imports, they are shi
|
|||||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||||
|
|
||||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
### Prequisites
|
|
||||||
- Install Rust: `curl https://sh.rustup.rs -sSf | sh`
|
|
||||||
- libsoup2.4-dev installed if Linux: `sudo apt install libsoup2.4-dev`
|
|
||||||
- Node v18.20 installed and currently active if you're using nvm
|
|
||||||
|
|
||||||
### Build Instruction
|
|
||||||
|
|
||||||
1. Install latest pnpm `curl -fsSL https://get.pnpm.io/install.sh | sh -` or upgrade `pnpm add -g pnpm`
|
|
||||||
2. Setup the .env of the root project folder, you should deploy the self hosted backend first
|
|
||||||
3. Run `pnpm install` on root project folder
|
|
||||||
4. Run `pnpm dev:gql-codegen` on this folder
|
|
||||||
5. Run `pnpm tauri dev` to run debug mode (optional)
|
|
||||||
6. Run `pnpm tauri build` to build release mode
|
|
||||||
- `pnpm tauri build --target aarch64-apple-darwin` for Apple Sillicon
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
"@fontsource-variable/material-symbols-rounded": "5.0.16",
|
"@fontsource-variable/material-symbols-rounded": "5.0.16",
|
||||||
"@fontsource-variable/roboto-mono": "5.0.16",
|
"@fontsource-variable/roboto-mono": "5.0.16",
|
||||||
"@hoppscotch/common": "workspace:^",
|
"@hoppscotch/common": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
|
||||||
"@platform/auth": "0.1.106",
|
"@platform/auth": "0.1.106",
|
||||||
"@tauri-apps/api": "1.5.1",
|
"@tauri-apps/api": "1.5.1",
|
||||||
"@tauri-apps/cli": "1.5.6",
|
"@tauri-apps/cli": "1.5.6",
|
||||||
@@ -37,13 +36,7 @@
|
|||||||
"tauri-plugin-store-api": "0.0.0",
|
"tauri-plugin-store-api": "0.0.0",
|
||||||
"util": "0.12.5",
|
"util": "0.12.5",
|
||||||
"vue": "3.3.9",
|
"vue": "3.3.9",
|
||||||
"workbox-window": "6.6.0",
|
"workbox-window": "6.6.0"
|
||||||
"zod": "3.22.4",
|
|
||||||
"@urql/core": "^4.1.1",
|
|
||||||
"cookie": "^0.5.0",
|
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
|
||||||
"tauri-plugin-websocket-api": "github:tauri-apps/tauri-plugin-websocket#v1",
|
|
||||||
"wonka": "^6.3.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/add": "5.0.0",
|
"@graphql-codegen/add": "5.0.0",
|
||||||
@@ -83,8 +76,6 @@
|
|||||||
"vite-plugin-pwa": "0.13.1",
|
"vite-plugin-pwa": "0.13.1",
|
||||||
"vite-plugin-static-copy": "0.12.0",
|
"vite-plugin-static-copy": "0.12.0",
|
||||||
"vite-plugin-vue-layouts": "0.7.0",
|
"vite-plugin-vue-layouts": "0.7.0",
|
||||||
"vue-tsc": "1.8.8",
|
"vue-tsc": "1.8.8"
|
||||||
"@types/cookie": "^0.5.1"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ tauri = { version = "1.5.3", features = [
|
|||||||
] }
|
] }
|
||||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tauri-plugin-deep-link = { git = "https://github.com/FabianLars/tauri-plugin-deep-link", branch = "main" }
|
tauri-plugin-deep-link = { git = "https://github.com/FabianLars/tauri-plugin-deep-link", branch = "main" }
|
||||||
tauri-plugin-websocket = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
|
||||||
tauri-plugin-window-state = "0.1.0"
|
tauri-plugin-window-state = "0.1.0"
|
||||||
reqwest = "0.11.22"
|
reqwest = "0.11.22"
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ fn main() {
|
|||||||
tauri_plugin_deep_link::prepare("io.hoppscotch.desktop");
|
tauri_plugin_deep_link::prepare("io.hoppscotch.desktop");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_websocket::init())
|
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
.plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"targets": "all"
|
"targets": "all"
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": "none"
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": false
|
"active": false
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import { ref } from "vue"
|
|
||||||
import { makeResult, makeErrorResult, Exchange, Operation } from "@urql/core"
|
|
||||||
import { makeFetchBody, makeFetchOptions } from "@urql/core/internal"
|
|
||||||
import { filter, make, merge, mergeMap, pipe, takeUntil, map } from "wonka"
|
|
||||||
import { gqlClientError$ } from "@hoppscotch/common/helpers/backend/GQLClient"
|
|
||||||
import { Store } from "tauri-plugin-store-api"
|
|
||||||
import { Body, getClient } from "@tauri-apps/api/http"
|
|
||||||
import { parse, serialize } from "cookie"
|
|
||||||
import { SubscriptionClient } from "subscriptions-transport-ws"
|
|
||||||
import { platform } from "@hoppscotch/common/platform"
|
|
||||||
import WSWrapper from "./ws_wrapper"
|
|
||||||
|
|
||||||
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
|
||||||
|
|
||||||
export async function addCookieToFetchHeaders(
|
|
||||||
store: Store,
|
|
||||||
headers: HeadersInit = {}
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const accessToken = await store.get<{ value: string }>("access_token")
|
|
||||||
const refreshToken = await store.get<{ value: string }>("refresh_token")
|
|
||||||
|
|
||||||
if (accessToken?.value && refreshToken?.value) {
|
|
||||||
// Assert headers as an indexable type
|
|
||||||
const headersIndexable = headers as { [key: string]: string }
|
|
||||||
const existingCookies = parse(headersIndexable["Cookie"] || "")
|
|
||||||
|
|
||||||
if (!existingCookies.access_token) {
|
|
||||||
existingCookies.access_token = accessToken.value
|
|
||||||
}
|
|
||||||
if (!existingCookies.refresh_token) {
|
|
||||||
existingCookies.refresh_token = refreshToken.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the cookies back into the headers
|
|
||||||
const serializedCookies = Object.entries(existingCookies)
|
|
||||||
.map(([name, value]) => serialize(name, value))
|
|
||||||
.join("; ")
|
|
||||||
headersIndexable["Cookie"] = serializedCookies
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
} catch (error) {
|
|
||||||
console.error("error while injecting cookie")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHttpSource(operation: Operation, store: Store) {
|
|
||||||
return make(({ next, complete }) => {
|
|
||||||
getClient().then(async (httpClient) => {
|
|
||||||
const fetchOptions = makeFetchOptions(operation)
|
|
||||||
let headers = fetchOptions.headers
|
|
||||||
headers = await addCookieToFetchHeaders(store, headers)
|
|
||||||
|
|
||||||
const fetchBody = makeFetchBody(operation)
|
|
||||||
httpClient
|
|
||||||
.post(operation.context.url, Body.json(fetchBody), {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
next(result.data)
|
|
||||||
complete()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
next(makeErrorResult(operation, error))
|
|
||||||
complete()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return () => {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tauriGQLFetchExchange =
|
|
||||||
(store: Store): Exchange =>
|
|
||||||
({ forward }) =>
|
|
||||||
(ops$) => {
|
|
||||||
const subscriptionResults$ = pipe(
|
|
||||||
ops$,
|
|
||||||
filter((op) => op.kind === "query" || op.kind === "mutation"),
|
|
||||||
mergeMap((operation) => {
|
|
||||||
const { key, context } = operation
|
|
||||||
|
|
||||||
const teardown$ = pipe(
|
|
||||||
ops$,
|
|
||||||
filter((op: Operation) => op.kind === "teardown" && op.key === key)
|
|
||||||
)
|
|
||||||
|
|
||||||
const source = createHttpSource(operation, store)
|
|
||||||
|
|
||||||
return pipe(
|
|
||||||
source,
|
|
||||||
takeUntil(teardown$),
|
|
||||||
map((result) => makeResult(operation, result as any))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const forward$ = pipe(
|
|
||||||
ops$,
|
|
||||||
filter(
|
|
||||||
(op: Operation) => op.kind === "teardown" || op.kind != "subscription"
|
|
||||||
),
|
|
||||||
forward
|
|
||||||
)
|
|
||||||
|
|
||||||
return merge([subscriptionResults$, forward$])
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSubscriptionClient = () => {
|
|
||||||
return new SubscriptionClient(
|
|
||||||
import.meta.env.VITE_BACKEND_WS_URL,
|
|
||||||
{
|
|
||||||
reconnect: true,
|
|
||||||
connectionParams: () => platform.auth.getBackendHeaders(),
|
|
||||||
connectionCallback(error) {
|
|
||||||
if (error?.length > 0) {
|
|
||||||
gqlClientError$.next({
|
|
||||||
type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT",
|
|
||||||
errors: error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wsOptionArguments: [
|
|
||||||
{
|
|
||||||
store: new Store(APP_DATA_PATH),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
WSWrapper
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let subscriptionClient: SubscriptionClient | null
|
|
||||||
|
|
||||||
const isBackendGQLEventAdded = ref(false)
|
|
||||||
|
|
||||||
const resetSubscriptionClient = () => {
|
|
||||||
if (subscriptionClient) {
|
|
||||||
subscriptionClient.close()
|
|
||||||
}
|
|
||||||
subscriptionClient = createSubscriptionClient()
|
|
||||||
if (!isBackendGQLEventAdded.value) {
|
|
||||||
subscriptionClient.onConnected(() => {
|
|
||||||
platform.auth.onBackendGQLClientShouldReconnect(() => {
|
|
||||||
const currentUser = platform.auth.getCurrentUser()
|
|
||||||
|
|
||||||
if (currentUser && subscriptionClient) {
|
|
||||||
subscriptionClient?.client?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUser && !subscriptionClient) {
|
|
||||||
resetSubscriptionClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentUser && subscriptionClient) {
|
|
||||||
subscriptionClient.close()
|
|
||||||
resetSubscriptionClient()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
isBackendGQLEventAdded.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSubscriptionClient = () => {
|
|
||||||
if (!subscriptionClient) resetSubscriptionClient()
|
|
||||||
return subscriptionClient
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { Store } from "tauri-plugin-store-api"
|
|
||||||
import TauriWebSocket, {
|
|
||||||
Message,
|
|
||||||
ConnectionConfig,
|
|
||||||
} from "tauri-plugin-websocket-api"
|
|
||||||
import { addCookieToFetchHeaders } from "./GQLClient"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a wrapper around tauri-plugin-websocket-api with cookie injection support. This is required because
|
|
||||||
* subscriptions-transport-ws client expects a custom websocket implementation in the shape of native browser WebSocket.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class WebSocketWrapper extends EventTarget implements WebSocket {
|
|
||||||
public client: TauriWebSocket | undefined
|
|
||||||
private tauriWebSocketConfig:
|
|
||||||
| (ConnectionConfig & { store: Store })
|
|
||||||
| undefined
|
|
||||||
private isConnected: boolean = false
|
|
||||||
binaryType: BinaryType = "blob"
|
|
||||||
extensions = ""
|
|
||||||
onclose: ((this: WebSocket, ev: CloseEvent) => any) | null = null
|
|
||||||
onerror: ((this: WebSocket, ev: Event) => any) | null = null
|
|
||||||
onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null = null
|
|
||||||
onopen: ((this: WebSocket, ev: Event) => any) | null = null
|
|
||||||
protocol = ""
|
|
||||||
url: string
|
|
||||||
|
|
||||||
public static readonly CONNECTING = 0
|
|
||||||
public static readonly OPEN = 1
|
|
||||||
public static readonly CLOSING = 2
|
|
||||||
public static readonly CLOSED = 3
|
|
||||||
|
|
||||||
readonly CONNECTING = 0
|
|
||||||
readonly OPEN = 1
|
|
||||||
readonly CLOSING = 2
|
|
||||||
readonly CLOSED = 3
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
url: string,
|
|
||||||
protocols?: string | string[],
|
|
||||||
config?: ConnectionConfig & { store: Store }
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.url = url
|
|
||||||
this.tauriWebSocketConfig = config
|
|
||||||
this.setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setup() {
|
|
||||||
if (this.tauriWebSocketConfig?.store) {
|
|
||||||
const headersStringified =
|
|
||||||
this.tauriWebSocketConfig.headers || ("{}" as any)
|
|
||||||
let headers = JSON.parse(headersStringified)
|
|
||||||
headers = await addCookieToFetchHeaders(
|
|
||||||
this.tauriWebSocketConfig.store,
|
|
||||||
headers
|
|
||||||
)
|
|
||||||
this.tauriWebSocketConfig = {
|
|
||||||
...this.tauriWebSocketConfig,
|
|
||||||
headers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.client = await TauriWebSocket.connect(this.url, {
|
|
||||||
headers: {
|
|
||||||
"sec-websocket-protocol": "graphql-ws",
|
|
||||||
...this.tauriWebSocketConfig?.headers,
|
|
||||||
},
|
|
||||||
}).catch((error) => {
|
|
||||||
this.isConnected = false
|
|
||||||
if (this.onerror) {
|
|
||||||
this.onerror(new Event("error"))
|
|
||||||
}
|
|
||||||
throw new Error(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.isConnected = true
|
|
||||||
|
|
||||||
this.client.addListener(this.handleMessage.bind(this))
|
|
||||||
if (this.onopen) {
|
|
||||||
this.onopen(new Event("open"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get readyState(): number {
|
|
||||||
return this.client
|
|
||||||
? this.isConnected
|
|
||||||
? this.OPEN
|
|
||||||
: this.CLOSED
|
|
||||||
: this.CONNECTING
|
|
||||||
}
|
|
||||||
|
|
||||||
get bufferedAmount(): number {
|
|
||||||
// TODO implement
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
close(code?: number, reason?: string): void {
|
|
||||||
this.client?.disconnect().then(() => {
|
|
||||||
if (this.onclose) {
|
|
||||||
this.onclose(new CloseEvent("close"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
|
|
||||||
if (
|
|
||||||
typeof data === "string" ||
|
|
||||||
data instanceof ArrayBuffer ||
|
|
||||||
data instanceof Blob
|
|
||||||
) {
|
|
||||||
this.client?.send(data as string).catch((error) => {
|
|
||||||
console.error("error while sending data", data)
|
|
||||||
if (this.onerror) {
|
|
||||||
this.onerror(new Event("error"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// TODO implement, drop the record for now
|
|
||||||
console.warn(
|
|
||||||
"WebSocketWrapper.send() not implemented for non-string data"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMessage(message: Message): void {
|
|
||||||
switch (message.type) {
|
|
||||||
case "Close": {
|
|
||||||
if (this.onclose) {
|
|
||||||
this.onclose(new CloseEvent("close"))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "Ping": {
|
|
||||||
this.client?.send("Pong").catch((error) => {
|
|
||||||
console.error("error while sending Pong data", message)
|
|
||||||
if (this.onerror) {
|
|
||||||
this.onerror(new Event("error"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (this.onmessage) {
|
|
||||||
this.onmessage(
|
|
||||||
new MessageEvent("message", {
|
|
||||||
data: message.data,
|
|
||||||
origin: this.url,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getService } from "@hoppscotch/common/modules/dioc"
|
import { getService } from "@hoppscotch/common/modules/dioc"
|
||||||
import axios from "axios"
|
|
||||||
import {
|
import {
|
||||||
AuthEvent,
|
AuthEvent,
|
||||||
AuthPlatformDef,
|
AuthPlatformDef,
|
||||||
@@ -14,50 +13,15 @@ import { open } from '@tauri-apps/api/shell'
|
|||||||
import { BehaviorSubject, Subject } from "rxjs"
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
import { Store } from "tauri-plugin-store-api"
|
import { Store } from "tauri-plugin-store-api"
|
||||||
import { Ref, ref, watch } from "vue"
|
import { Ref, ref, watch } from "vue"
|
||||||
import { z } from "zod"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { subscriptionExchange } from "@urql/core"
|
|
||||||
import {
|
|
||||||
getSubscriptionClient,
|
|
||||||
tauriGQLFetchExchange,
|
|
||||||
} from "../helpers/GQLClient"
|
|
||||||
|
|
||||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||||
|
|
||||||
export const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
||||||
|
|
||||||
const persistenceService = getService(PersistenceService)
|
const persistenceService = getService(PersistenceService)
|
||||||
|
|
||||||
const expectedAllowedProvidersSchema = z.object({
|
|
||||||
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
|
|
||||||
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
|
|
||||||
providers: z.array(z.string()),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const getAllowedAuthProviders = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(
|
|
||||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/providers`,
|
|
||||||
{
|
|
||||||
withCredentials: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const parseResult = expectedAllowedProvidersSchema.safeParse(res.data)
|
|
||||||
|
|
||||||
if (!parseResult.success) {
|
|
||||||
return E.left("SOMETHING_WENT_WRONG")
|
|
||||||
}
|
|
||||||
|
|
||||||
return E.right(parseResult.data.providers)
|
|
||||||
} catch (_) {
|
|
||||||
return E.left("SOMETHING_WENT_WRONG")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
let client = await getClient();
|
let client = await getClient();
|
||||||
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
|
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
|
||||||
@@ -73,7 +37,7 @@ async function signInUserWithGithubFB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signInUserWithGoogleFB() {
|
async function signInUserWithGoogleFB() {
|
||||||
await open(`${import.meta.env.VITE_BACKEND_LOGIN_API_URL}/authenticate`);
|
await open(`${import.meta.env.VITE_BACKEND_API_URL}/auth/google?redirect_uri=desktop`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signInUserWithMicrosoftFB() {
|
async function signInUserWithMicrosoftFB() {
|
||||||
@@ -260,15 +224,6 @@ export const def: AuthPlatformDef = {
|
|||||||
},
|
},
|
||||||
getGQLClientOptions() {
|
getGQLClientOptions() {
|
||||||
return {
|
return {
|
||||||
exchanges: [
|
|
||||||
subscriptionExchange({
|
|
||||||
forwardSubscription(fetchBody) {
|
|
||||||
const subscriptionClient = getSubscriptionClient()
|
|
||||||
return subscriptionClient!.request(fetchBody)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
tauriGQLFetchExchange(new Store(APP_DATA_PATH)),
|
|
||||||
],
|
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
},
|
},
|
||||||
@@ -416,5 +371,4 @@ export const def: AuthPlatformDef = {
|
|||||||
event: "logout",
|
event: "logout",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getAllowedAuthProviders,
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user