feat: hoppscotch agent and agent interceptor (#4396)
Co-authored-by: CuriousCorrelation <CuriousCorrelation@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('agent.client_certs')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<ul
|
||||
v-if="certificateMap.size > 0"
|
||||
class="mx-4 border border-dividerDark rounded"
|
||||
>
|
||||
<li
|
||||
v-for="([domain, certificate], index) in certificateMap"
|
||||
:key="domain"
|
||||
class="flex border-dividerDark px-2 items-center justify-between"
|
||||
:class="{ 'border-t border-dividerDark': index !== 0 }"
|
||||
>
|
||||
<div class="flex space-x-2">
|
||||
<div class="truncate">
|
||||
{{ domain }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="text-secondaryLight mr-2">
|
||||
{{ "PEMCert" in certificate.cert ? "PEM" : "PFX/PKCS12" }}
|
||||
</div>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="certificate.enabled ? IconCheckCircle : IconCircle"
|
||||
:title="
|
||||
certificate.enabled
|
||||
? t('action.turn_off')
|
||||
: t('action.turn_on')
|
||||
"
|
||||
color="green"
|
||||
@click="toggleEntryEnabled(domain)"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconTrash"
|
||||
:title="t('action.remove')"
|
||||
color="red"
|
||||
@click="deleteEntry(domain)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<HoppButtonSecondary
|
||||
class="mx-4"
|
||||
:icon="IconPlus"
|
||||
:label="t('agent.add_cert_file')"
|
||||
filled
|
||||
outline
|
||||
@click="showAddModal = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary :label="t('action.save')" @click="save" />
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('hide-modal')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
|
||||
<InterceptorsAgentModalNativeClientCertsAdd
|
||||
:show="showAddModal"
|
||||
:existing-domains="Array.from(certificateMap.keys())"
|
||||
@hide-modal="showAddModal = false"
|
||||
@save="saveCertificate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- TODO: i18n -->
|
||||
<script setup lang="ts">
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useService } from "dioc/vue"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
ClientCertificateEntry,
|
||||
AgentInterceptorService,
|
||||
} from "~/platform/std/interceptors/agent"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const nativeInterceptorService = useService(AgentInterceptorService)
|
||||
|
||||
const certificateMap = ref(new Map<string, ClientCertificateEntry>())
|
||||
|
||||
const showAddModal = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
certificateMap.value = cloneDeep(
|
||||
nativeInterceptorService.clientCertificates.value
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function save() {
|
||||
nativeInterceptorService.clientCertificates.value = cloneDeep(
|
||||
certificateMap.value
|
||||
)
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
function saveCertificate(cert: ClientCertificateEntry) {
|
||||
certificateMap.value.set(cert.domain, cert)
|
||||
}
|
||||
|
||||
function toggleEntryEnabled(domain: string) {
|
||||
const certificate = certificateMap.value.get(domain)
|
||||
|
||||
if (certificate) {
|
||||
certificateMap.value.set(domain, {
|
||||
...certificate,
|
||||
enabled: !certificate.enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEntry(domain: string) {
|
||||
certificateMap.value.delete(domain)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('agent.add_client_cert')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<HoppSmartInput
|
||||
v-model="domain"
|
||||
:autofocus="false"
|
||||
styles="flex-1"
|
||||
placeholder=" "
|
||||
:label="t('agent.domain')"
|
||||
input-styles="input floating-input"
|
||||
/>
|
||||
|
||||
<HoppSmartTabs v-model="selectedTab">
|
||||
<HoppSmartTab :id="'pem'" :label="'PEM'">
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label> {{ t("agent.cert") }} </label>
|
||||
<HoppButtonSecondary
|
||||
:icon="pemCert?.type === 'loaded' ? IconFile : IconPlus"
|
||||
:loading="pemCert?.type === 'loading'"
|
||||
:label="
|
||||
pemCert?.type === 'loaded'
|
||||
? pemCert.filename
|
||||
: t('agent.add_cert_file')
|
||||
"
|
||||
filled
|
||||
outline
|
||||
@click="openFilePicker('pem_cert')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label> {{ t("agent.key") }} </label>
|
||||
<HoppButtonSecondary
|
||||
:icon="pemKey?.type === 'loaded' ? IconFile : IconPlus"
|
||||
:loading="pemKey?.type === 'loading'"
|
||||
:label="
|
||||
pemKey?.type === 'loaded'
|
||||
? pemKey.filename
|
||||
: t('agent.add_key_file')
|
||||
"
|
||||
filled
|
||||
outline
|
||||
@click="openFilePicker('pem_key')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab :id="'pfx'" :label="t('agent.pfx_or_pkcs')">
|
||||
<div class="p-4 space-y-6">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label> {{ t("agent.pfx_or_pkcs_file") }} </label>
|
||||
<HoppButtonSecondary
|
||||
:icon="pfxCert?.type === 'loaded' ? IconFile : IconPlus"
|
||||
:loading="pfxCert?.type === 'loading'"
|
||||
:label="
|
||||
pfxCert?.type === 'loaded'
|
||||
? pfxCert.filename
|
||||
: t('agent.add_pfx_or_pkcs_file')
|
||||
"
|
||||
filled
|
||||
outline
|
||||
@click="openFilePicker('pfx_cert')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border border-divider rounded">
|
||||
<HoppSmartInput
|
||||
v-model="pfxPassword"
|
||||
:type="showPfxPassword ? 'text' : 'password'"
|
||||
:label="t('authorization.password')"
|
||||
input-styles="floating-input !border-0 "
|
||||
:placeholder="' '"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
showPfxPassword
|
||||
? t('hide.password')
|
||||
: t('show.password')
|
||||
"
|
||||
:icon="showPfxPassword ? IconEye : IconEyeOff"
|
||||
@click="showPfxPassword = !showPfxPassword"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.save')"
|
||||
:disabled="!isValidCertificate || anyFileSelectorIsLoading"
|
||||
@click="save"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('hide-modal')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<!-- TODO: i18n -->
|
||||
<script setup lang="ts">
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconEyeOff from "~icons/lucide/eye-off"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconFile from "~icons/lucide/file"
|
||||
import { ref, watch, computed } from "vue"
|
||||
import { useFileDialog } from "@vueuse/core"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { ClientCertificateEntry } from "~/platform/std/interceptors/agent"
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
existingDomains: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(e: "save", certificate: ClientCertificateEntry): void
|
||||
}>()
|
||||
|
||||
type FileSelectorState =
|
||||
| null
|
||||
| { type: "loading" }
|
||||
| { type: "loaded"; filename: string; data: Uint8Array }
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const domain = ref("")
|
||||
|
||||
const pemCert = ref<FileSelectorState>(null)
|
||||
const pemKey = ref<FileSelectorState>(null)
|
||||
|
||||
const pfxCert = ref<FileSelectorState>(null)
|
||||
|
||||
const pfxPassword = ref("")
|
||||
const showPfxPassword = ref(false)
|
||||
|
||||
const anyFileSelectorIsLoading = computed(
|
||||
() =>
|
||||
pemCert.value?.type === "loading" ||
|
||||
pemKey.value?.type === "loading" ||
|
||||
pfxCert.value?.type === "loading"
|
||||
)
|
||||
|
||||
const currentlyPickingFile = ref<null | "pem_cert" | "pem_key" | "pfx_cert">(
|
||||
null
|
||||
)
|
||||
|
||||
const selectedTab = ref<"pem" | "pfx">("pem")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) return
|
||||
|
||||
currentlyPickingFile.value = null
|
||||
|
||||
domain.value = ""
|
||||
pemCert.value = null
|
||||
pemKey.value = null
|
||||
pfxCert.value = null
|
||||
pfxPassword.value = ""
|
||||
showPfxPassword.value = false
|
||||
selectedTab.value = "pem"
|
||||
}
|
||||
)
|
||||
|
||||
const certificate = computed<ClientCertificateEntry | null>(() => {
|
||||
if (selectedTab.value === "pem") {
|
||||
if (pemCert.value?.type === "loaded" && pemKey.value?.type === "loaded") {
|
||||
return <ClientCertificateEntry>{
|
||||
domain: domain.value,
|
||||
enabled: true,
|
||||
cert: {
|
||||
PEMCert: {
|
||||
certificate_filename: pemCert.value.filename,
|
||||
certificate_pem: pemCert.value.data,
|
||||
|
||||
key_filename: pemKey.value.filename,
|
||||
key_pem: pemKey.value.data,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pfxCert.value?.type === "loaded") {
|
||||
return <ClientCertificateEntry>{
|
||||
domain: domain.value.trim(),
|
||||
enabled: true,
|
||||
cert: {
|
||||
PFXCert: {
|
||||
certificate_filename: pfxCert.value.filename,
|
||||
certificate_pfx: pfxCert.value.data,
|
||||
password: pfxPassword.value,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const isValidCertificate = computed(() => {
|
||||
if (certificate.value === null) return false
|
||||
|
||||
if (props.existingDomains.includes(certificate.value.domain)) {
|
||||
toast.error("A certificate for this domain already exists")
|
||||
return false
|
||||
}
|
||||
|
||||
return ClientCertificateEntry.safeParse(certificate.value).success
|
||||
})
|
||||
|
||||
const {
|
||||
open: openFileDialog,
|
||||
reset: resetFilePicker,
|
||||
onChange: onFilePickerChange,
|
||||
} = useFileDialog({
|
||||
reset: true,
|
||||
multiple: false,
|
||||
})
|
||||
|
||||
onFilePickerChange(async (files) => {
|
||||
if (!files) return
|
||||
|
||||
const file = files.item(0)
|
||||
|
||||
if (!file) return
|
||||
|
||||
if (currentlyPickingFile.value === "pem_cert") {
|
||||
pemCert.value = { type: "loading" }
|
||||
} else if (currentlyPickingFile.value === "pem_key") {
|
||||
pemKey.value = { type: "loading" }
|
||||
} else if (currentlyPickingFile.value === "pfx_cert") {
|
||||
pfxCert.value = { type: "loading" }
|
||||
}
|
||||
|
||||
const data = new Uint8Array(await file.arrayBuffer())
|
||||
|
||||
if (currentlyPickingFile.value === "pem_cert") {
|
||||
pemCert.value = { type: "loaded", filename: file.name, data }
|
||||
} else if (currentlyPickingFile.value === "pem_key") {
|
||||
pemKey.value = { type: "loaded", filename: file.name, data }
|
||||
} else if (currentlyPickingFile.value === "pfx_cert") {
|
||||
pfxCert.value = { type: "loaded", filename: file.name, data }
|
||||
}
|
||||
|
||||
currentlyPickingFile.value = null
|
||||
|
||||
resetFilePicker()
|
||||
})
|
||||
|
||||
function openFilePicker(type: "pem_cert" | "pem_key" | "pfx_cert") {
|
||||
currentlyPickingFile.value = type
|
||||
|
||||
openFileDialog()
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (certificate.value) {
|
||||
emit("save", certificate.value)
|
||||
emit("hide-modal")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<!-- TODO: i18n -->
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
styles="sm:max-w-md"
|
||||
:title="modalTitle"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<p v-if="status === 'agent_not_running'" class="text-secondaryLight">
|
||||
{{ t("agent.not_running") }}
|
||||
</p>
|
||||
|
||||
<template v-else-if="status === 'registration_required'">
|
||||
<p
|
||||
v-if="registrationStatus === 'initial'"
|
||||
class="text-secondaryLight"
|
||||
>
|
||||
{{ t("agent.registration_instruction") }}
|
||||
</p>
|
||||
|
||||
<template v-else-if="registrationStatus === 'otp_required'">
|
||||
<p class="text-secondaryLight">
|
||||
{{ t("agent.enter_otp_instruction") }}
|
||||
</p>
|
||||
|
||||
<HoppSmartInput
|
||||
v-model="userEnteredOTP"
|
||||
placeholder=" "
|
||||
:label="t('agent.otp_label')"
|
||||
input-styles="input floating-input"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-else-if="isRegistrationLoading"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<HoppSmartSpinner />
|
||||
|
||||
<p class="text-secondaryLight">{{ t("agent.processing") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-start flex-1">
|
||||
<HoppButtonPrimary
|
||||
:label="primaryButtonLabel"
|
||||
:loading="isRegistrationLoading"
|
||||
@click="primaryActionHandler"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
class="ml-2"
|
||||
filled
|
||||
outline
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
const userEnteredOTP = ref("")
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
status: "agent_not_running" | "registration_required" | "hidden"
|
||||
registrationStatus: "initial" | "otp_required" | "loading"
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(e: "register"): void
|
||||
(e: "verify", otp: string): void
|
||||
(e: "retry-connection"): void
|
||||
}>()
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
switch (props.status) {
|
||||
case "agent_not_running":
|
||||
return t("agent.not_running_title")
|
||||
case "registration_required":
|
||||
return t("agent.registration_title")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
})
|
||||
|
||||
const isRegistrationLoading = computed(
|
||||
() => props.registrationStatus === "loading"
|
||||
)
|
||||
|
||||
const primaryButtonLabel = computed(() => {
|
||||
if (isRegistrationLoading.value) {
|
||||
return t("state.loading")
|
||||
}
|
||||
|
||||
if (props.status === "agent_not_running") {
|
||||
return t("action.retry")
|
||||
}
|
||||
|
||||
if (props.status === "registration_required") {
|
||||
if (props.registrationStatus === "initial") {
|
||||
return t("action.register")
|
||||
}
|
||||
|
||||
if (props.registrationStatus === "otp_required") {
|
||||
return t("action.verify")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
|
||||
const primaryActionHandler = () => {
|
||||
if (props.status === "agent_not_running") {
|
||||
return emit("retry-connection")
|
||||
}
|
||||
|
||||
if (props.status === "registration_required") {
|
||||
if (props.registrationStatus === "initial") {
|
||||
return emit("register")
|
||||
}
|
||||
|
||||
if (props.registrationStatus === "otp_required") {
|
||||
return emit("verify", userEnteredOTP.value)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const hideModal = () => emit("hide-modal")
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<InterceptorsAgentRegistrationModal
|
||||
:show="showModal"
|
||||
:status="modalStatus"
|
||||
:registration-status="registrationStatus"
|
||||
@hide-modal="hideModal"
|
||||
@register="register"
|
||||
@verify="verifyOTP"
|
||||
@retry-connection="checkAgentStatus(true)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useService } from "dioc/vue"
|
||||
import { AgentInterceptorService } from "~/platform/std/interceptors/agent"
|
||||
import { ref, onMounted, computed, watch } from "vue"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
// TODO: Move as much as logic as possible to AgentInterceptorService
|
||||
|
||||
const interceptorService = useService(InterceptorService) // TODO: Try to remove dependency to InterceptorService
|
||||
const agentService = useService(AgentInterceptorService)
|
||||
const showModal = ref(false)
|
||||
const toast = useToast()
|
||||
|
||||
const modalStatus = computed(() => {
|
||||
if (!agentService.isAgentRunning.value) return "agent_not_running"
|
||||
if (!agentService.isAuthKeyPresent()) return "registration_required"
|
||||
return "hidden"
|
||||
})
|
||||
|
||||
const registrationStatus = ref<"initial" | "otp_required" | "loading">(
|
||||
"initial"
|
||||
)
|
||||
|
||||
async function checkAgentStatus(isRetry = false) {
|
||||
if (
|
||||
interceptorService.currentInterceptor.value?.interceptorID ===
|
||||
agentService.interceptorID
|
||||
) {
|
||||
await agentService.checkAgentStatus()
|
||||
updateModalVisibility()
|
||||
|
||||
if (isRetry && !agentService.isAgentRunning.value) {
|
||||
toast.error("Agent is not running.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(interceptorService.currentInterceptor, () => {
|
||||
checkAgentStatus()
|
||||
})
|
||||
|
||||
function updateModalVisibility() {
|
||||
showModal.value = modalStatus.value !== "hidden"
|
||||
if (showModal.value && modalStatus.value === "registration_required") {
|
||||
registrationStatus.value = "initial"
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await checkAgentStatus()
|
||||
})
|
||||
|
||||
function hideModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
async function register() {
|
||||
registrationStatus.value = "loading"
|
||||
try {
|
||||
await agentService.initiateRegistration()
|
||||
registrationStatus.value = "otp_required"
|
||||
} catch (error) {
|
||||
toast.error("Failed to initiate registration. Please try again.")
|
||||
registrationStatus.value = "initial"
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyOTP(otp: string) {
|
||||
registrationStatus.value = "loading"
|
||||
try {
|
||||
await agentService.verifyRegistration(otp)
|
||||
toast.success("Registration successful!")
|
||||
hideModal()
|
||||
} catch (error) {
|
||||
toast.error("Failed to verify OTP. Please try again.")
|
||||
registrationStatus.value = "otp_required"
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler("agent.open-registration-modal", () => {
|
||||
if (!showModal.value) {
|
||||
showModal.value = true
|
||||
registrationStatus.value = "initial"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
114
packages/hoppscotch-common/src/components/settings/Agent.vue
Normal file
114
packages/hoppscotch-common/src/components/settings/Agent.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="py-4 space-y-4">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="allowSSLVerification"
|
||||
@change="allowSSLVerification = !allowSSLVerification"
|
||||
/>
|
||||
{{ t("agent.verify_ssl_certs") }}
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<!--
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLucideFileBadge"
|
||||
:label="'CA Certificates'"
|
||||
outline
|
||||
@click="showCACertificatesModal = true"
|
||||
/>
|
||||
-->
|
||||
<!--
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLucideFileKey"
|
||||
:label="t('agent.client_certs')"
|
||||
outline
|
||||
@click="showClientCertificatesModal = true"
|
||||
/>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<ModalsNativeCACertificates
|
||||
:show="showCACertificatesModal"
|
||||
@hide-modal="showCACertificatesModal = false"
|
||||
/>
|
||||
-->
|
||||
|
||||
<!--
|
||||
<InterceptorsAgentModalNativeClientCertificates
|
||||
:show="showClientCertificatesModal"
|
||||
@hide-modal="showClientCertificatesModal = false"
|
||||
/>
|
||||
-->
|
||||
|
||||
<div class="pt-4 space-y-4">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle :on="allowProxy" @change="allowProxy = !allowProxy" />
|
||||
{{ t("agent.use_http_proxy") }}
|
||||
</div>
|
||||
|
||||
<HoppSmartInput
|
||||
v-if="allowProxy"
|
||||
v-model="proxyURL"
|
||||
:autofocus="false"
|
||||
styles="flex-1"
|
||||
placeholder=" "
|
||||
:label="t('settings.proxy_url')"
|
||||
input-styles="input floating-input"
|
||||
/>
|
||||
|
||||
<p class="my-1 text-secondaryLight">
|
||||
{{ t("agent.proxy_capabilities") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TODO: i18n -->
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
// import IconLucideFileKey from "~icons/lucide/file-key"
|
||||
import { useService } from "dioc/vue"
|
||||
import {
|
||||
RequestDef,
|
||||
AgentInterceptorService,
|
||||
} from "~/platform/std/interceptors/agent"
|
||||
import { syncRef } from "@vueuse/core"
|
||||
|
||||
type RequestProxyInfo = RequestDef["proxy"]
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const agentInterceptorService = useService(AgentInterceptorService)
|
||||
|
||||
const allowSSLVerification = agentInterceptorService.validateCerts
|
||||
|
||||
// const showCACertificatesModal = ref(false)
|
||||
// const showClientCertificatesModal = ref(false)
|
||||
|
||||
const allowProxy = ref(false)
|
||||
const proxyURL = ref("")
|
||||
|
||||
const proxyInfo = computed<RequestProxyInfo>({
|
||||
get() {
|
||||
if (allowProxy.value) {
|
||||
return {
|
||||
url: proxyURL.value,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
set(newData) {
|
||||
if (newData) {
|
||||
allowProxy.value = true
|
||||
proxyURL.value = newData.url
|
||||
} else {
|
||||
allowProxy.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
syncRef(agentInterceptorService.proxyInfo, proxyInfo, { direction: "both" })
|
||||
</script>
|
||||
Reference in New Issue
Block a user