fix: handle actions for logged-in users in case of token expiration (#5249)

Co-authored-by: nivedin <nivedinp@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Chhavi Goyal 2025-09-22 09:36:40 -04:00 committed by GitHub
parent ba700886b5
commit 637c380c07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 340 additions and 58 deletions

View file

@ -216,4 +216,14 @@ export class AuthController {
return tokens.right;
}
@Get('verify-token')
@UseGuards(JwtAuthGuard)
async verifyToken(@GqlUser() user: AuthUser) {
return {
isValid: true,
uid: user.uid,
message: 'Token is valid',
};
}
}

View file

@ -69,6 +69,7 @@ const addNewCollection = () => {
authActive: true,
},
headers: [],
variables: [],
})
)

View file

@ -255,6 +255,7 @@ import { useService } from "dioc/vue"
import { computed, ref } from "vue"
import { Picked } from "~/helpers/types/HoppPicked"
import { removeGraphqlCollection } from "~/newstore/collections"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
import { GQLTabService } from "~/services/tab/graphql"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCopy from "~icons/lucide/copy"
@ -358,7 +359,9 @@ const toggleShowChildren = () => {
showChildren.value = !showChildren.value
}
const removeCollection = () => {
const removeCollection = async () => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
// Cancel pick if picked collection is deleted
if (
props.picked?.pickedType === "gql-my-collection" &&

View file

@ -38,6 +38,7 @@ import { editGraphqlCollection } from "~/newstore/collections"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const props = defineProps<{
show: boolean
@ -62,12 +63,15 @@ watch(
}
)
const saveCollection = () => {
const saveCollection = async () => {
if (!editingName.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
// TODO: Better typechecking here ?
const collectionUpdated = {
...(props.editingCollection as any),

View file

@ -37,6 +37,7 @@ import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { editGraphqlFolder } from "~/newstore/collections"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const t = useI18n()
const toast = useToast()
@ -59,11 +60,13 @@ watch(
}
)
const editFolder = () => {
const editFolder = async () => {
if (!name.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
editGraphqlFolder(props.folderPath, {
...(props.folder as any),
name: name.value,

View file

@ -108,6 +108,7 @@ import { GQLTabService } from "~/services/tab/graphql"
import IconSparkle from "~icons/lucide/sparkles"
import IconThumbsUp from "~icons/lucide/thumbs-up"
import IconThumbsDown from "~icons/lucide/thumbs-down"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const t = useI18n()
const toast = useToast()
@ -155,12 +156,15 @@ watch(
const submittedFeedback = ref(false)
const { submitFeedback, isSubmitFeedbackPending } = useSubmitFeedback()
const saveRequest = () => {
const saveRequest = async () => {
if (!editingName.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const requestUpdated = {
...(props.request as any),
name: editingName.value || (props.request as any).name,
@ -173,7 +177,7 @@ const saveRequest = () => {
folderPath: props.folderPath!,
})
editGraphqlRequest(props.folderPath, props.requestIndex, requestUpdated)
editGraphqlRequest(props.folderPath!, props.requestIndex!, requestUpdated)
if (possibleActiveTab) {
possibleActiveTab.value.document.request.name = requestUpdated.name

View file

@ -237,6 +237,7 @@ import { useToast } from "@composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import { computed, ref } from "vue"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
import { Picked } from "~/helpers/types/HoppPicked"
import { removeGraphqlFolder } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
@ -322,7 +323,9 @@ const toggleShowChildren = () => {
showChildren.value = !showChildren.value
}
const removeFolder = () => {
const removeFolder = async () => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
// Cancel pick if the picked folder is deleted
if (
props.picked?.pickedType === "gql-my-folder" &&

View file

@ -139,6 +139,7 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { HoppGQLRequest } from "@hoppscotch/data"
import { removeGraphqlRequest } from "~/newstore/collections"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
@ -221,7 +222,9 @@ const dragStart = ({ dataTransfer }: any) => {
dataTransfer.setData("requestIndex", props.requestIndex)
}
const removeRequest = () => {
const removeRequest = async () => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
// Cancel pick if the picked request is deleted
if (
props.picked &&

View file

@ -196,6 +196,7 @@ import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { EditingProperties } from "../Properties.vue"
import { defineActionHandler } from "~/helpers/actions"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const t = useI18n()
const toast = useToast()
@ -333,7 +334,9 @@ const filteredCollections = computed(() => {
return filteredCollections
})
const displayModalAdd = (shouldDisplay: boolean) => {
const displayModalAdd = async (shouldDisplay: boolean) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
showModalAdd.value = shouldDisplay
}
@ -343,7 +346,9 @@ const displayModalEdit = (shouldDisplay: boolean) => {
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
const displayModalImportExport = async (shouldDisplay: boolean) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
showModalImportExport.value = shouldDisplay
}
@ -386,15 +391,21 @@ const editCollection = (
displayModalEdit(true)
}
const duplicateCollection = ({
const duplicateCollection = async ({
path,
collectionSyncID,
}: {
path: string
collectionSyncID?: string
}) => duplicateGraphQLCollection(path, collectionSyncID)
}) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
duplicateGraphQLCollection(path, collectionSyncID)
}
const onAddRequest = ({ name, path }: { name: string; path: string }) => {
const onAddRequest = async ({ name, path }: { name: string; path: string }) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const newRequest = {
...getDefaultGQLRequest(),
name,
@ -429,13 +440,15 @@ const addRequest = (payload: { path: string }) => {
displayModalAddRequest(true)
}
const onAddFolder = ({
const onAddFolder = async ({
name,
path,
}: {
name: string
path: string | undefined
}) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
addGraphqlFolder(name, path ?? "0")
platform.analytics?.logEvent({
@ -489,13 +502,15 @@ const editRequest = (payload: {
displayModalEditRequest(true)
}
const duplicateRequest = ({
const duplicateRequest = async ({
folderPath,
request,
}: {
folderPath: string
request: HoppGQLRequest
}) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
saveGraphqlRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${t("action.duplicate")}`,
@ -536,7 +551,7 @@ const selectRequest = ({
})
}
const dropRequest = ({
const dropRequest = async ({
folderPath,
requestIndex,
collectionIndex,
@ -545,6 +560,9 @@ const dropRequest = ({
requestIndex: number
collectionIndex: number
}) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
@ -607,11 +625,13 @@ const editProperties = ({
displayModalEditProperties(true)
}
const setCollectionProperties = (newCollection: {
const setCollectionProperties = async (newCollection: {
collection: Partial<HoppCollection> | null
path: string
isRootCollection: boolean
}) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const { collection, path, isRootCollection } = newCollection
if (!collection) {
return

View file

@ -49,7 +49,9 @@
@drop-request="dropRequest"
@drop-collection="dropCollection"
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
@display-modal-import-export="
displayModalImportExport(true, 'my-collections')
"
@duplicate-collection="duplicateCollection"
@duplicate-request="duplicateRequest"
@duplicate-response="duplicateResponse"
@ -246,6 +248,7 @@ import {
getCompleteCollectionTree,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
import {
createChildCollection,
createNewRootCollection,
@ -381,6 +384,7 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const myCollections = useReadonlyStream(restCollections$, [], "deep")
// Dragging
@ -755,7 +759,14 @@ const displayModalEditResponse = (show: boolean) => {
if (!show) resetSelectedData()
}
const displayModalImportExport = (show: boolean) => {
const displayModalImportExport = async (
show: boolean,
collectionType?: string
) => {
if (collectionType === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
}
showModalImportExport.value = show
if (!show) resetSelectedData()
@ -779,8 +790,14 @@ const displayTeamModalAdd = (show: boolean) => {
teamListAdapter.fetchList()
}
const addNewRootCollection = (name: string) => {
const addNewRootCollection = async (name: string) => {
if (collectionsType.value.type === "my-collections") {
modalLoadingState.value = true
const isValidToken = await handleTokenValidation()
if (!isValidToken) {
modalLoadingState.value = false
return
}
addRESTCollection(
makeCollection({
name,
@ -802,6 +819,7 @@ const addNewRootCollection = (name: string) => {
isRootCollection: true,
})
modalLoadingState.value = false
displayModalAdd(false)
} else if (hasTeamWriteAccess.value) {
if (!collectionsType.value.selectedTeam) return
@ -841,7 +859,7 @@ const addRequest = (payload: {
displayModalAddRequest(true)
}
const onAddRequest = (requestName: string) => {
const onAddRequest = async (requestName: string) => {
const newRequest = {
...getDefaultRESTRequest(),
name: requestName,
@ -850,6 +868,9 @@ const onAddRequest = (requestName: string) => {
const path = editingFolderPath.value
if (!path) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const insertionIndex = saveRESTRequestAs(path, newRequest)
tabs.createNewTab({
@ -935,11 +956,13 @@ const addFolder = (payload: {
displayModalAddFolder(true)
}
const onAddFolder = (folderName: string) => {
const onAddFolder = async (folderName: string) => {
const path = editingFolderPath.value
if (collectionsType.value.type === "my-collections") {
if (!path) return
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
addRESTFolder(folderName, path)
platform.analytics?.logEvent({
@ -1000,7 +1023,7 @@ const editCollection = (payload: {
displayModalEditCollection(true)
}
const updateEditingCollection = (newName: string) => {
const updateEditingCollection = async (newName: string) => {
if (!editingCollection.value) return
if (!newName) {
@ -1009,6 +1032,8 @@ const updateEditingCollection = (newName: string) => {
}
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const collectionIndex = editingCollectionIndex.value
if (collectionIndex === null) return
@ -1058,10 +1083,12 @@ const editFolder = (payload: {
displayModalEditFolder(true)
}
const updateEditingFolder = (newName: string) => {
const updateEditingFolder = async (newName: string) => {
if (!editingFolder.value) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (!editingFolderPath.value) return
editRESTFolder(editingFolderPath.value, {
@ -1104,6 +1131,8 @@ const duplicateCollection = async ({
collectionSyncID?: string
}) => {
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
duplicateRESTCollection(pathOrID, collectionSyncID)
} else if (hasTeamWriteAccess.value) {
duplicateCollectionLoading.value = true
@ -1141,7 +1170,7 @@ const editRequest = (payload: {
displayModalEditRequest(true)
}
const updateEditingRequest = (newName: string) => {
const updateEditingRequest = async (newName: string) => {
const request = editingRequest.value
if (!request) return
@ -1150,6 +1179,9 @@ const updateEditingRequest = (newName: string) => {
name: newName || request.name,
}
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const folderPath = editingFolderPath.value
const requestIndex = editingRequestIndex.value
@ -1295,12 +1327,15 @@ const updateEditingResponse = (newName: string) => {
possibleExampleActiveTab.value.document.response.name = newName
nextTick(() => {
possibleExampleActiveTab.value.document.isDirty = false
possibleExampleActiveTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
exampleID: editingResponseID.value!,
const doc = possibleExampleActiveTab.value.document
if (doc.type === "example-response") {
doc.isDirty = false
doc.saveContext = {
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
exampleID: editingResponseID.value!,
}
}
})
}
@ -1361,11 +1396,14 @@ const updateEditingResponse = (newName: string) => {
) {
possibleActiveResponseTab.value.document.response.name = newName
nextTick(() => {
possibleActiveResponseTab.value.document.isDirty = false
possibleActiveResponseTab.value.document.saveContext = {
originLocation: "team-collection",
requestID,
exampleID: editingResponseID.value!,
const doc = possibleActiveResponseTab.value.document
if (doc.type === "example-response") {
doc.isDirty = false
doc.saveContext = {
originLocation: "team-collection",
requestID,
exampleID: editingResponseID.value!,
}
}
})
}
@ -1381,7 +1419,7 @@ const updateEditingResponse = (newName: string) => {
}
}
const duplicateRequest = (payload: {
const duplicateRequest = async (payload: {
folderPath: string
request: HoppRESTRequest
}) => {
@ -1394,6 +1432,8 @@ const duplicateRequest = (payload: {
}
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
saveRESTRequestAs(folderPath, newRequest)
toast.success(t("request.duplicated"))
} else if (hasTeamWriteAccess.value) {
@ -1424,7 +1464,7 @@ const duplicateRequest = (payload: {
}
}
const duplicateResponse = (payload: ResponseConfigPayload) => {
const duplicateResponse = async (payload: ResponseConfigPayload) => {
const { folderPath, requestIndex, request, responseName } = payload
const response = request.responses[responseName]
@ -1453,6 +1493,8 @@ const duplicateResponse = (payload: ResponseConfigPayload) => {
}
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
editRESTRequest(folderPath, parseInt(requestIndex), updatedRequest)
toast.success(t("response.duplicated"))
@ -1544,8 +1586,10 @@ const removeTeamCollectionOrFolder = async (collectionID: string) => {
)()
}
const onRemoveCollection = () => {
const onRemoveCollection = async () => {
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const collectionIndex = editingCollectionIndex.value
const collectionToRemove =
@ -1625,8 +1669,10 @@ const removeFolder = (id: string) => {
displayConfirmModal(true)
}
const onRemoveFolder = () => {
const onRemoveFolder = async () => {
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const folderPath = editingFolderPath.value
if (!folderPath) return
@ -1712,8 +1758,10 @@ const removeRequest = (payload: {
displayConfirmModal(true)
}
const onRemoveRequest = () => {
const onRemoveRequest = async () => {
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const folderPath = editingFolderPath.value
const requestIndex = editingRequestIndex.value
@ -1825,7 +1873,7 @@ const removeResponse = (payload: ResponseConfigPayload) => {
displayConfirmModal(true)
}
const onRemoveResponse = () => {
const onRemoveResponse = async () => {
const request = cloneDeep(editingRequest.value)
if (!request) return
@ -1840,6 +1888,8 @@ const onRemoveResponse = () => {
}
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const folderPath = editingFolderPath.value
const requestIndex = editingRequestIndex.value
@ -2141,7 +2191,7 @@ const pathToLastIndex = (path: string) => {
* This function is called when the user drops the request inside a collection
* @param payload Object that contains the folder path, request index and the destination collection index
*/
const dropRequest = (payload: {
const dropRequest = async (payload: {
folderPath?: string | undefined
requestIndex: string
destinationCollectionIndex: string
@ -2153,6 +2203,8 @@ const dropRequest = (payload: {
let possibleTab = null
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
@ -2293,7 +2345,7 @@ const isMoveToSameLocation = (
* to a different collection or folder
* @param payload - object containing the collection index dragged and the destination collection index
*/
const dropCollection = (payload: {
const dropCollection = async (payload: {
collectionIndexDragged: string
destinationCollectionIndex: string
destinationParentPath?: string
@ -2309,6 +2361,8 @@ const dropCollection = (payload: {
if (collectionIndexDragged === destinationCollectionIndex) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (
checkIfCollectionIsAParentOfTheChildren(
collectionIndexDragged,
@ -2430,11 +2484,13 @@ const isAlreadyInRoot = (id: string) => {
* to the root
* @param payload - object containing the collection index dragged
*/
const dropToRoot = ({ dataTransfer }: DragEvent) => {
const dropToRoot = async ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
if (!collectionIndexDragged) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
// check if the collection is already in the root
if (isAlreadyInRoot(collectionIndexDragged)) {
toast.error(`${t("collection.invalid_root_move")}`)
@ -2545,7 +2601,7 @@ const isSameSameParent = (
* @param payload - object containing the request index dragged and the destination request index
* with the destination collection index
*/
const updateRequestOrder = (payload: {
const updateRequestOrder = async (payload: {
dragedRequestIndex: string
destinationRequestIndex: string | null
destinationCollectionIndex: string
@ -2561,6 +2617,8 @@ const updateRequestOrder = (payload: {
if (dragedRequestIndex === destinationRequestIndex) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (
!isSameSameParent(
dragedRequestIndex,
@ -2616,7 +2674,7 @@ const updateRequestOrder = (payload: {
* This function is called when the user updates the collection or folder order
* @param payload - object containing the collection index dragged and the destination collection index
*/
const updateCollectionOrder = (payload: {
const updateCollectionOrder = async (payload: {
dragedCollectionIndex: string
destinationCollection: {
destinationCollectionIndex: string | null
@ -2630,6 +2688,8 @@ const updateCollectionOrder = (payload: {
if (dragedCollectionIndex === destinationCollectionIndex) return
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (
!isSameSameParent(
dragedCollectionIndex,
@ -2781,7 +2841,7 @@ const getCurrentValue = (
)?.currentValue
}
const editProperties = (payload: {
const editProperties = async (payload: {
collectionIndex: string
collection: HoppCollection | TeamCollection
}) => {
@ -2790,6 +2850,8 @@ const editProperties = (payload: {
const collectionId = collection.id ?? collectionIndex.split("/").pop()
if (collectionsType.value.type === "my-collections") {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties: HoppInheritedProperty = {

View file

@ -30,7 +30,7 @@
:icon="IconEdit"
:title="`${t('action.edit')}`"
class="hidden group-hover:inline-flex"
@click="emit('edit-environment')"
@click="emitEditEnvironment"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@ -76,9 +76,9 @@
:shortcut="['E']"
:disabled="duplicateGlobalEnvironmentLoading"
@click="
() => {
emit('edit-environment')
hide()
async () => {
const ok = await emitEditEnvironment()
if (ok) hide()
}
"
/>
@ -150,6 +150,7 @@ import {
deleteEnvironment,
duplicateEnvironment,
} from "~/newstore/environments"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
@ -160,6 +161,13 @@ import { CurrentValueService } from "~/services/current-environment-value.servic
const t = useI18n()
const toast = useToast()
const emitEditEnvironment = async (): Promise<boolean> => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return false
emit("edit-environment")
return true
}
const props = withDefaults(
defineProps<{
environment: Environment
@ -213,7 +221,9 @@ const duplicate = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>()
const removeEnvironment = () => {
const removeEnvironment = async () => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (props.environmentIndex === null) return
if (!isGlobalEnvironment.value) {
deleteEnvironment(props.environmentIndex as number, props.environment.id)
@ -223,7 +233,9 @@ const removeEnvironment = () => {
toast.success(`${t("state.deleted")}`)
}
const duplicateEnvironments = () => {
const duplicateEnvironments = async () => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
if (props.environmentIndex === null) {
return
}

View file

@ -120,6 +120,7 @@ import { defineActionHandler } from "~/helpers/actions"
import { sortPersonalEnvironmentsAlphabetically } from "~/helpers/utils/sortEnvironmentsAlphabetically"
import { HandleEnvChangeProp } from "../index.vue"
import { Environment } from "@hoppscotch/data"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const t = useI18n()
const colorMode = useColorMode()
@ -159,17 +160,25 @@ const editingEnvironmentIndex = ref<number | null>(null)
const editingVariableName = ref("")
const secretOptionSelected = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => {
const displayModalAdd = async (shouldDisplay: boolean) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
action.value = "new"
showModalDetails.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
action.value = "edit"
const displayModalEdit = async (shouldDisplay: boolean) => {
if (shouldDisplay) {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
action.value = "edit"
}
showModalDetails.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
const displayModalImportExport = async (shouldDisplay: boolean) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
showModalImportExport.value = shouldDisplay
}
const selectEnvironment = (index: number, environment: Environment) => {

View file

@ -172,6 +172,7 @@ import { getEnvActionErrorMessage } from "~/helpers/error-messages"
import { HandleEnvChangeProp } from "../index.vue"
import { selectedEnvironmentIndex$ } from "~/newstore/environments"
import { useReadonlyStream } from "~/composables/stream"
import { handleTokenValidation } from "~/helpers/handleTokenValidation"
const t = useI18n()
@ -223,7 +224,9 @@ const selectedEnvironmentID = ref<string | null>(null)
const isTeamViewer = computed(() => props.team?.role === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => {
const displayModalAdd = async (shouldDisplay: boolean) => {
const isValidToken = await handleTokenValidation()
if (!isValidToken) return
action.value = "new"
showModalDetails.value = shouldDisplay
}

View file

@ -0,0 +1,19 @@
import { useToast } from "@composables/toast"
import { isValidUser } from "~/helpers/isValidUser"
/**
* High-level authentication validation handler with automatic error notifications.
*
* This wrapper around `isValidUser()` provides automatic toast error messages for invalid tokens.
* Use this when you want standard error handling with user notifications.
*
* For silent validation or custom error handling, use `isValidUser()` directly.
*
* @returns {Promise<boolean>} True if user is valid, false otherwise (with toast error shown)
*/
export const handleTokenValidation = async (): Promise<boolean> => {
const toast = useToast()
const { valid, error } = await isValidUser()
if (!valid) toast.error(error)
return valid
}

View file

@ -0,0 +1,44 @@
import { platform } from "~/platform"
export type ValidUserResponse = {
valid: boolean
error: string
}
export const SESSION_EXPIRED = "Session expired. Please log in again."
/**
* Validates user authentication and token validity by making an API call.
*
* This function is kept separate from `handleTokenValidation()` to enable different use cases:
* - Silent validation for conditional UI states (e.g., disabling components on token expiration)
* - Background checks without triggering user notifications
* - Custom error handling based on validation results
*
* Use `handleTokenValidation()` when you need automatic toast error notifications.
* Use `isValidUser()` for silent validation or custom error handling scenarios.
*
* @returns {Promise<ValidUserResponse>} Authentication status with user existence and token validity
*/
export const isValidUser = async (): Promise<ValidUserResponse> => {
const user = platform.auth.getCurrentUser()
if (user) {
try {
// If the platform provides a method to verify auth tokens, use it else assume tokens are valid (for central instance where firebase handles it)
const hasValidTokens = platform.auth.verifyAuthTokens
? await platform.auth.verifyAuthTokens()
: true
return {
valid: hasValidTokens,
error: hasValidTokens ? "" : SESSION_EXPIRED,
}
} catch (error) {
return { valid: false, error: SESSION_EXPIRED }
}
}
// allow user to perform actions without being logged in
return { valid: true, error: "" }
}

View file

@ -275,4 +275,10 @@ export type AuthPlatformDef = {
* If a value is not given, then the value is assumed to be false.
*/
isEmailEditable?: boolean
/** Verifies if the current user's authentication tokens are valid
* For self-hosted, this should verify the tokens with the backend
* @returns True if tokens are valid, false otherwise
*/
verifyAuthTokens?: () => Promise<boolean>
}

View file

@ -394,4 +394,31 @@ export const def: AuthPlatformDef = {
event: "logout",
})
},
/**
* Verifies if the current user's authentication tokens are valid
* @returns True if tokens are valid, false otherwise
*/
async verifyAuthTokens() {
try {
const BACKEND_API_URL = import.meta.env.VITE_BACKEND_API_URL
const client = await getClient()
const response = await client.get(
`${BACKEND_API_URL}/auth/verify-token`,
{
headers: {
"Content-Type": "application/json",
...this.getBackendHeaders(),
},
}
)
// axios automatically throws on error status codes, so if we reach here, it was successful
return !!response.data.isValid
} catch (error) {
return false
}
},
}

View file

@ -526,4 +526,30 @@ export const def: AuthPlatformDef = {
event: "logout",
})
},
/**
* Verifies if the current user's authentication tokens are valid
* @returns True if tokens are valid, false otherwise
*/
async verifyAuthTokens() {
const BACKEND_API_URL = import.meta.env.VITE_BACKEND_API_URL
const { response } = interceptorService.execute({
id: Date.now(),
url: `${BACKEND_API_URL}/auth/verify-token`,
method: "GET",
version: "HTTP/1.1",
headers: {
"Content-Type": "application/json",
...this.getBackendHeaders(),
},
})
const res = await response
if (E.isLeft(res)) {
return false
}
return res.right.isValid
},
}

View file

@ -371,4 +371,27 @@ export const def: AuthPlatformDef = {
}
},
getAllowedAuthProviders,
/**
* Verifies if the current user's authentication tokens are valid
* @returns True if tokens are valid, false otherwise
*/
async verifyAuthTokens() {
try {
const BACKEND_API_URL = import.meta.env.VITE_BACKEND_API_URL
const response = await axios.get(`${BACKEND_API_URL}/auth/verify-token`, {
headers: {
"Content-Type": "application/json",
...this.getBackendHeaders(),
},
withCredentials: true,
})
// axios automatically throws on error status codes, so if we reach here, it was successful
return !!response.data.isValid
} catch (error) {
return false
}
},
}