feat: selfhost auth frontend (#15)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -119,10 +119,11 @@ export type AuthPlatformDef = {
|
||||
onBackendGQLClientShouldReconnect: (func: () => void) => void
|
||||
|
||||
/**
|
||||
* provide the client options for GqlClient
|
||||
* Called by the platform to provide additional/different config options when
|
||||
* setting up the URQL based GQLCLient instance
|
||||
* @returns
|
||||
*/
|
||||
getGQLClientOptions?: () => ClientOptions
|
||||
getGQLClientOptions?: () => Partial<ClientOptions>
|
||||
|
||||
/**
|
||||
* Returns the string content that should be returned when the user selects to
|
||||
|
||||
64
packages/hoppscotch-selfhost-web/.eslintrc.cjs
Normal file
64
packages/hoppscotch-selfhost-web/.eslintrc.cjs
Normal file
@@ -0,0 +1,64 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
},
|
||||
extends: [
|
||||
"@vue/typescript/recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
ignorePatterns: [
|
||||
"static/**/*",
|
||||
"./helpers/backend/graphql.ts",
|
||||
"**/*.d.ts",
|
||||
"types/**/*",
|
||||
],
|
||||
plugins: ["vue", "prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
semi: [2, "never"],
|
||||
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||
"no-console": "off",
|
||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"prettier/prettier":
|
||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-side-effects-in-computed-properties": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"@typescript-eslint/no-unused-vars":
|
||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"import/default": "off",
|
||||
"no-undef": "off",
|
||||
// localStorage block
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "localStorage",
|
||||
message:
|
||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
||||
},
|
||||
],
|
||||
// window.localStorage block
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "CallExpression[callee.object.property.name='localStorage']",
|
||||
message:
|
||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
27
packages/hoppscotch-selfhost-web/.gitignore
vendored
Normal file
27
packages/hoppscotch-selfhost-web/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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?
|
||||
|
||||
# Sitemap Generation Artifacts (see vite.config.ts)
|
||||
.sitemap-gen
|
||||
26
packages/hoppscotch-selfhost-web/index.html
Normal file
26
packages/hoppscotch-selfhost-web/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hoppscotch - Open source API development ecosystem</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" href="/icon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Shims to make swagger-parser package work
|
||||
window.global = window
|
||||
</script>
|
||||
<script type="module">
|
||||
import { Buffer } from "buffer"
|
||||
import process from "process"
|
||||
|
||||
// // Shims to make postman-collection work
|
||||
window.Buffer = Buffer
|
||||
window.process = process
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
118
packages/hoppscotch-selfhost-web/meta.ts
Normal file
118
packages/hoppscotch-selfhost-web/meta.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { IHTMLTag } from "vite-plugin-html-config"
|
||||
|
||||
export const APP_INFO = {
|
||||
name: "Hoppscotch",
|
||||
shortDescription: "Open source API development ecosystem",
|
||||
description:
|
||||
"Helps you create requests faster, saving precious time on development.",
|
||||
keywords:
|
||||
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
|
||||
app: {
|
||||
background: "#202124",
|
||||
},
|
||||
social: {
|
||||
twitter: "@hoppscotch_io",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
|
||||
{
|
||||
name: "keywords",
|
||||
content: APP_INFO.keywords,
|
||||
},
|
||||
{
|
||||
name: "X-UA-Compatible",
|
||||
content: "IE=edge, chrome=1",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Open Graph tags
|
||||
{
|
||||
name: "og:title",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "og:description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "og:image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Twitter tags
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image",
|
||||
},
|
||||
{
|
||||
name: "twitter:site",
|
||||
content: APP_INFO.social.twitter,
|
||||
},
|
||||
{
|
||||
name: "twitter:creator",
|
||||
content: APP_INFO.social.twitter,
|
||||
},
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
},
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: APP_INFO.description,
|
||||
},
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: `${env.VITE_BASE_URL}/banner.png`,
|
||||
},
|
||||
// Add to homescreen for Chrome on Android. Fallback for PWA (handled by nuxt)
|
||||
{
|
||||
name: "application-name",
|
||||
content: APP_INFO.name,
|
||||
},
|
||||
// Windows phone tile icon
|
||||
{
|
||||
name: "msapplication-TileImage",
|
||||
content: `${env.VITE_BASE_URL}/icon.png`,
|
||||
},
|
||||
{
|
||||
name: "msapplication-TileColor",
|
||||
content: APP_INFO.app.background,
|
||||
},
|
||||
{
|
||||
name: "msapplication-tap-highlight",
|
||||
content: "no",
|
||||
},
|
||||
// iOS Safari
|
||||
{
|
||||
name: "apple-mobile-web-app-title",
|
||||
content: APP_INFO.name,
|
||||
},
|
||||
{
|
||||
name: "apple-mobile-web-app-capable",
|
||||
content: "yes",
|
||||
},
|
||||
{
|
||||
name: "apple-mobile-web-app-status-bar-style",
|
||||
content: "black-translucent",
|
||||
},
|
||||
// PWA
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.background,
|
||||
},
|
||||
{
|
||||
name: "mask-icon",
|
||||
content: "/icon.png",
|
||||
color: APP_INFO.app.background,
|
||||
},
|
||||
]
|
||||
61
packages/hoppscotch-selfhost-web/package.json
Normal file
61
packages/hoppscotch-selfhost-web/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@hoppscotch/selfhost-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max_old_space_size=16384 ./node_modules/vite/bin/vite.js build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||
"lint:ts": "vue-tsc --noEmit",
|
||||
"lintfix": "eslint --fix src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||
"prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint",
|
||||
"generate": "pnpm run build",
|
||||
"do-dev": "pnpm run dev",
|
||||
"do-build-prod": "pnpm run build",
|
||||
"do-lint": "pnpm run prod-lint",
|
||||
"do-typecheck": "pnpm run lint",
|
||||
"do-lintfix": "pnpm run lintfix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hoppscotch/common": "workspace:^",
|
||||
"axios": "^0.21.4",
|
||||
"buffer": "^6.0.3",
|
||||
"firebase": "^9.8.4",
|
||||
"process": "^0.11.10",
|
||||
"rxjs": "^7.5.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.4",
|
||||
"vue": "^3.2.41",
|
||||
"workbox-window": "^6.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"typescript": "^4.6.4",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
"unplugin-vue-components": "^0.21.0",
|
||||
"vite": "^3.2.3",
|
||||
"vite-plugin-fonts": "^0.6.0",
|
||||
"vite-plugin-html-config": "^1.0.10",
|
||||
"vite-plugin-inspect": "^0.7.4",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-static-copy": "^0.12.0",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vue-tsc": "^1.0.9",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
}
|
||||
6
packages/hoppscotch-selfhost-web/src/main.ts
Normal file
6
packages/hoppscotch-selfhost-web/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createHoppApp } from "@hoppscotch/common"
|
||||
import { def as authDef } from "./platform/auth"
|
||||
|
||||
createHoppApp("#app", {
|
||||
auth: authDef,
|
||||
})
|
||||
330
packages/hoppscotch-selfhost-web/src/platform/auth.ts
Normal file
330
packages/hoppscotch-selfhost-web/src/platform/auth.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import axios from "axios"
|
||||
import {
|
||||
AuthEvent,
|
||||
AuthPlatformDef,
|
||||
HoppUser,
|
||||
} from "@hoppscotch/common/platform/auth"
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import {
|
||||
getLocalConfig,
|
||||
removeLocalConfig,
|
||||
setLocalConfig,
|
||||
} from "@hoppscotch/common/newstore/localpersistence"
|
||||
import { Ref, ref, watch } from "vue"
|
||||
|
||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
|
||||
async function logout() {
|
||||
await axios.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`, {
|
||||
withCredentials: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function signInUserWithGithubFB() {
|
||||
window.location.href = `${import.meta.env.VITE_BACKEND_API_URL}/auth/github`
|
||||
}
|
||||
|
||||
async function signInUserWithGoogleFB() {
|
||||
window.location.href = `${import.meta.env.VITE_BACKEND_API_URL}/auth/google`
|
||||
}
|
||||
|
||||
async function signInUserWithMicrosoftFB() {
|
||||
window.location.href = `${
|
||||
import.meta.env.VITE_BACKEND_API_URL
|
||||
}/auth/microsoft`
|
||||
}
|
||||
|
||||
async function getInitialUserDetails() {
|
||||
const res = await axios.post<{
|
||||
data?: {
|
||||
me?: {
|
||||
uid: string
|
||||
displayName: string
|
||||
email: string
|
||||
photoURL: string
|
||||
isAdmin: boolean
|
||||
createdOn: string
|
||||
// emailVerified: boolean
|
||||
}
|
||||
}
|
||||
errors?: Array<{
|
||||
message: string
|
||||
}>
|
||||
}>(
|
||||
`${import.meta.env.VITE_BACKEND_GQL_URL}`,
|
||||
{
|
||||
query: `query Me {
|
||||
me {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
photoURL
|
||||
isAdmin
|
||||
createdOn
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
|
||||
return res.data
|
||||
}
|
||||
|
||||
const isGettingInitialUser: Ref<null | boolean> = ref(null)
|
||||
|
||||
function setUser(user: HoppUser | null) {
|
||||
currentUser$.next(user)
|
||||
probableUser$.next(user)
|
||||
|
||||
setLocalConfig("login_state", JSON.stringify(user))
|
||||
}
|
||||
|
||||
async function setInitialUser() {
|
||||
isGettingInitialUser.value = true
|
||||
const res = await getInitialUserDetails()
|
||||
|
||||
const error = res.errors && res.errors[0]
|
||||
|
||||
// no cookies sent. so the user is not logged in
|
||||
if (error && error.message === "auth/cookies_not_found") {
|
||||
setUser(null)
|
||||
isGettingInitialUser.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// cookies sent, but it is expired, we need to refresh the token
|
||||
if (error && error.message === "Unauthorized") {
|
||||
const isRefreshSuccess = await refreshToken()
|
||||
|
||||
if (isRefreshSuccess) {
|
||||
setInitialUser()
|
||||
} else {
|
||||
setUser(null)
|
||||
isGettingInitialUser.value = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// no errors, we have a valid user
|
||||
if (res.data && res.data.me) {
|
||||
const hoppBackendUser = res.data.me
|
||||
|
||||
const hoppUser: HoppUser = {
|
||||
uid: hoppBackendUser.uid,
|
||||
displayName: hoppBackendUser.displayName,
|
||||
email: hoppBackendUser.email,
|
||||
photoURL: hoppBackendUser.photoURL,
|
||||
// all our signin methods currently guarantees the email is verified
|
||||
emailVerified: true,
|
||||
}
|
||||
|
||||
setUser(hoppUser)
|
||||
|
||||
isGettingInitialUser.value = false
|
||||
|
||||
authEvents$.next({
|
||||
event: "login",
|
||||
user: hoppUser,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
const res = await axios.get(
|
||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
|
||||
const isSuccessful = res.status === 200
|
||||
|
||||
if (isSuccessful) {
|
||||
authEvents$.next({
|
||||
event: "token_refresh",
|
||||
})
|
||||
}
|
||||
|
||||
return isSuccessful
|
||||
}
|
||||
|
||||
async function sendMagicLink(email: string) {
|
||||
const res = await axios.post(
|
||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/signin`,
|
||||
{
|
||||
email,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
|
||||
if (res.data && res.data.deviceIdentifier) {
|
||||
setLocalConfig("deviceIdentifier", res.data.deviceIdentifier)
|
||||
} else {
|
||||
throw new Error("test: does not get device identifier")
|
||||
}
|
||||
|
||||
return res.data
|
||||
}
|
||||
|
||||
export const def: AuthPlatformDef = {
|
||||
getCurrentUserStream: () => currentUser$,
|
||||
getAuthEventsStream: () => authEvents$,
|
||||
getProbableUserStream: () => probableUser$,
|
||||
|
||||
getCurrentUser: () => currentUser$.value,
|
||||
getProbableUser: () => probableUser$.value,
|
||||
|
||||
getBackendHeaders() {
|
||||
return {}
|
||||
},
|
||||
getGQLClientOptions() {
|
||||
return {
|
||||
fetchOptions: {
|
||||
credentials: "include",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* it is not possible for us to know if the current cookie is expired because we cannot access http-only cookies from js
|
||||
* hence just returning if the currentUser$ has a value associated with it
|
||||
*/
|
||||
willBackendHaveAuthError() {
|
||||
return !currentUser$.value
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onBackendGQLClientShouldReconnect(func) {
|
||||
authEvents$.subscribe((event) => {
|
||||
if (
|
||||
event.event == "login" ||
|
||||
event.event == "logout" ||
|
||||
event.event == "token_refresh"
|
||||
) {
|
||||
func()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* we cannot access our auth cookies from javascript, so leaving this as null
|
||||
*/
|
||||
getDevOptsBackendIDToken() {
|
||||
return null
|
||||
},
|
||||
async performAuthInit() {
|
||||
const probableUser = JSON.parse(getLocalConfig("login_state") ?? "null")
|
||||
probableUser$.next(probableUser)
|
||||
await setInitialUser()
|
||||
},
|
||||
|
||||
waitProbableLoginToConfirm() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.getCurrentUser()) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (!probableUser$.value) reject(new Error("no_probable_user"))
|
||||
|
||||
const unwatch = watch(isGettingInitialUser, (val) => {
|
||||
if (val === true || val === false) {
|
||||
resolve()
|
||||
unwatch()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async signInWithEmail(email: string) {
|
||||
await sendMagicLink(email)
|
||||
},
|
||||
|
||||
isSignInWithEmailLink(url: string) {
|
||||
const urlObject = new URL(url)
|
||||
const searchParams = new URLSearchParams(urlObject.search)
|
||||
|
||||
return !!searchParams.get("token")
|
||||
},
|
||||
|
||||
async verifyEmailAddress() {
|
||||
return
|
||||
},
|
||||
async signInUserWithGoogle() {
|
||||
await signInUserWithGoogleFB()
|
||||
},
|
||||
async signInUserWithGithub() {
|
||||
await signInUserWithGithubFB()
|
||||
return undefined
|
||||
},
|
||||
async signInUserWithMicrosoft() {
|
||||
await signInUserWithMicrosoftFB()
|
||||
},
|
||||
async signInWithEmailLink(email: string, url: string) {
|
||||
const urlObject = new URL(url)
|
||||
const searchParams = new URLSearchParams(urlObject.search)
|
||||
|
||||
const token = searchParams.get("token")
|
||||
const deviceIdentifier = getLocalConfig("deviceIdentifier")
|
||||
|
||||
await axios.post(
|
||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`,
|
||||
{
|
||||
token: token,
|
||||
deviceIdentifier,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async setEmailAddress(_email: string) {
|
||||
return
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async setDisplayName(name: string) {
|
||||
return
|
||||
},
|
||||
|
||||
async signOutUser() {
|
||||
// if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
await logout()
|
||||
|
||||
probableUser$.next(null)
|
||||
currentUser$.next(null)
|
||||
removeLocalConfig("login_state")
|
||||
|
||||
authEvents$.next({
|
||||
event: "logout",
|
||||
})
|
||||
},
|
||||
|
||||
async processMagicLink() {
|
||||
if (this.isSignInWithEmailLink(window.location.href)) {
|
||||
const deviceIdentifier = getLocalConfig("deviceIdentifier")
|
||||
|
||||
if (!deviceIdentifier) {
|
||||
throw new Error(
|
||||
"Device Identifier not found, you can only signin from the browser you generated the magic link"
|
||||
)
|
||||
}
|
||||
|
||||
await this.signInWithEmailLink(deviceIdentifier, window.location.href)
|
||||
|
||||
removeLocalConfig("deviceIdentifier")
|
||||
window.location.href = "/"
|
||||
}
|
||||
},
|
||||
}
|
||||
7
packages/hoppscotch-selfhost-web/src/vite-env.d.ts
vendored
Normal file
7
packages/hoppscotch-selfhost-web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue"
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
23
packages/hoppscotch-selfhost-web/tsconfig.json
Normal file
23
packages/hoppscotch-selfhost-web/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@hoppscotch/common": [ "../hoppscotch-common/src/index.ts" ],
|
||||
"@hoppscotch/common/*": [ "../hoppscotch-common/src/*" ]
|
||||
}
|
||||
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
packages/hoppscotch-selfhost-web/tsconfig.node.json
Normal file
9
packages/hoppscotch-selfhost-web/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "meta.ts"]
|
||||
}
|
||||
193
packages/hoppscotch-selfhost-web/vite.config.ts
Normal file
193
packages/hoppscotch-selfhost-web/vite.config.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { defineConfig, loadEnv, normalizePath } from "vite"
|
||||
import { APP_INFO, META_TAGS } from "./meta"
|
||||
import { viteStaticCopy as StaticCopy } from "vite-plugin-static-copy"
|
||||
import generateSitemap from "vite-plugin-pages-sitemap"
|
||||
import HtmlConfig from "vite-plugin-html-config"
|
||||
import Vue from "@vitejs/plugin-vue"
|
||||
import VueI18n from "@intlify/vite-plugin-vue-i18n"
|
||||
import Components from "unplugin-vue-components/vite"
|
||||
import Icons from "unplugin-icons/vite"
|
||||
import Inspect from "vite-plugin-inspect"
|
||||
import WindiCSS from "vite-plugin-windicss"
|
||||
import { VitePWA } from "vite-plugin-pwa"
|
||||
import Pages from "vite-plugin-pages"
|
||||
import Layouts from "vite-plugin-vue-layouts"
|
||||
import IconResolver from "unplugin-icons/resolver"
|
||||
import { FileSystemIconLoader } from "unplugin-icons/loaders"
|
||||
import * as path from "path"
|
||||
import { VitePluginFonts } from "vite-plugin-fonts"
|
||||
import legacy from "@vitejs/plugin-legacy"
|
||||
|
||||
const ENV = loadEnv("development", path.resolve(__dirname, "../../"))
|
||||
|
||||
export default defineConfig({
|
||||
envDir: path.resolve(__dirname, "../../"),
|
||||
// TODO: Migrate @hoppscotch/data to full ESM
|
||||
define: {
|
||||
// For 'util' polyfill required by dep of '@apidevtools/swagger-parser'
|
||||
"process.env": {},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
preview: {
|
||||
port: 3000,
|
||||
},
|
||||
publicDir: path.resolve(__dirname, "../hoppscotch-common/public"),
|
||||
build: {
|
||||
sourcemap: true,
|
||||
emptyOutDir: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// TODO: Maybe leave ~ only for individual apps and not use on common
|
||||
"~": path.resolve(__dirname, "../hoppscotch-common/src"),
|
||||
"@hoppscotch/common": "@hoppscotch/common/src",
|
||||
"@composables": path.resolve(
|
||||
__dirname,
|
||||
"../hoppscotch-common/src/composables"
|
||||
),
|
||||
"@modules": path.resolve(__dirname, "../hoppscotch-common/src/modules"),
|
||||
"@components": path.resolve(
|
||||
__dirname,
|
||||
"../hoppscotch-common/src/components"
|
||||
),
|
||||
"@helpers": path.resolve(__dirname, "../hoppscotch-common/src/helpers"),
|
||||
"@functional": path.resolve(
|
||||
__dirname,
|
||||
"../hoppscotch-common/src/helpers/functional"
|
||||
),
|
||||
"@workers": path.resolve(__dirname, "../hoppscotch-common/src/workers"),
|
||||
|
||||
stream: "stream-browserify",
|
||||
util: "util",
|
||||
},
|
||||
dedupe: ["vue"],
|
||||
},
|
||||
plugins: [
|
||||
Inspect(), // go to url -> /__inspect
|
||||
HtmlConfig({
|
||||
metas: META_TAGS(ENV),
|
||||
}),
|
||||
Vue(),
|
||||
Pages({
|
||||
routeStyle: "nuxt",
|
||||
dirs: "../hoppscotch-common/src/pages",
|
||||
importMode: "async",
|
||||
onRoutesGenerated(routes) {
|
||||
// HACK: See: https://github.com/jbaubree/vite-plugin-pages-sitemap/issues/173
|
||||
return ((generateSitemap as any).default as typeof generateSitemap)({
|
||||
routes,
|
||||
nuxtStyle: true,
|
||||
allowRobots: true,
|
||||
dest: ".sitemap-gen",
|
||||
hostname: ENV.VITE_BASE_URL,
|
||||
})
|
||||
},
|
||||
}),
|
||||
StaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: normalizePath(path.resolve(__dirname, "./.sitemap-gen/*")),
|
||||
dest: normalizePath(path.resolve(__dirname, "./dist")),
|
||||
},
|
||||
],
|
||||
}),
|
||||
Layouts({
|
||||
layoutsDirs: "../hoppscotch-common/src/layouts",
|
||||
defaultLayout: "default",
|
||||
}),
|
||||
VueI18n({
|
||||
runtimeOnly: false,
|
||||
compositionOnly: true,
|
||||
include: [path.resolve(__dirname, "locales")],
|
||||
}),
|
||||
WindiCSS({
|
||||
root: path.resolve(__dirname, "../hoppscotch-common"),
|
||||
}),
|
||||
Components({
|
||||
dts: "../hoppscotch-common/src/components.d.ts",
|
||||
dirs: [
|
||||
"../hoppscotch-common/src/components",
|
||||
"../hoppscotch-ui/src/components",
|
||||
],
|
||||
directoryAsNamespace: true,
|
||||
resolvers: [
|
||||
IconResolver({
|
||||
prefix: "icon",
|
||||
customCollections: ["hopp", "auth", "brands"],
|
||||
}),
|
||||
],
|
||||
types: [
|
||||
{
|
||||
from: "vue-tippy",
|
||||
names: ["Tippy"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
Icons({
|
||||
compiler: "vue3",
|
||||
customCollections: {
|
||||
hopp: FileSystemIconLoader("../hoppscotch-common/assets/icons"),
|
||||
auth: FileSystemIconLoader("../hoppscotch-common/assets/icons/auth"),
|
||||
brands: FileSystemIconLoader(
|
||||
"../hoppscotch-common/assets/icons/brands"
|
||||
),
|
||||
},
|
||||
}),
|
||||
VitePWA({
|
||||
manifest: {
|
||||
name: APP_INFO.name,
|
||||
short_name: APP_INFO.name,
|
||||
description: APP_INFO.shortDescription,
|
||||
start_url: "/?source=pwa",
|
||||
background_color: APP_INFO.app.background,
|
||||
theme_color: APP_INFO.app.background,
|
||||
icons: [
|
||||
{
|
||||
src: "/icon.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any maskable",
|
||||
},
|
||||
{
|
||||
src: "/logo.svg",
|
||||
sizes: "48x48 72x72 96x96 128x128 256x256 512x512",
|
||||
type: "image/svg+xml",
|
||||
purpose: "any maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
registerType: "prompt",
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
maximumFileSizeToCacheInBytes: 4194304,
|
||||
navigateFallbackDenylist: [
|
||||
/robots.txt/,
|
||||
/sitemap.xml/,
|
||||
/discord/,
|
||||
/telegram/,
|
||||
/beta/,
|
||||
/careers/,
|
||||
/newsletter/,
|
||||
/twitter/,
|
||||
/github/,
|
||||
/announcements/,
|
||||
],
|
||||
},
|
||||
}),
|
||||
VitePluginFonts({
|
||||
google: {
|
||||
families: [
|
||||
"Inter:wght@400;500;600;700;800",
|
||||
"Roboto+Mono:wght@400;500",
|
||||
"Material+Icons",
|
||||
],
|
||||
},
|
||||
}),
|
||||
legacy({
|
||||
modernPolyfills: ["es.string.replace-all"],
|
||||
renderLegacyChunks: false,
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user