Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"sqlmap",
"srem",
"unixepoch",
"unref",
"Whois"
]
}
46 changes: 34 additions & 12 deletions app/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<code
class="hidden select-none cursor-pointer sm:inline-block rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
@click="copyPayloadUrl">
/api/payload/{{ shortSlug(selectedToken) }}
/api/payload/{{ friendlyId || shortSlug(selectedToken) }}
</code>
</UTooltip>
</div>
Expand Down Expand Up @@ -68,6 +68,12 @@
<div v-if="showMobileExtras && hasMobileExtras" id="header-mobile-extras"
class="mt-3 grid gap-3 rounded-lg border border-gray-200 dark:border-gray-800 bg-white/90 dark:bg-gray-900/90 p-3 shadow-sm md:hidden">
<ClientOnly>
<code v-if="selectedToken"
class="select-none cursor-pointer sm:inline-block rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
@click="copyPayloadUrl">
/api/payload/{{ friendlyId || shortSlug(selectedToken) }}
</code>

<UButton v-if="sessionInfo && sessionRestoreEnabled" color="neutral" variant="soft" size="sm"
icon="i-lucide-user" @click="copySessionId">
{{ sessionInfo.friendlyId }}
Expand Down Expand Up @@ -95,7 +101,7 @@
<script setup lang="ts">
import { computed, watch, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useTokens } from '~/composables/useTokens'
import { useTokensStore } from '~/stores/tokens'
import { useSSE } from '~/composables/useSSE'
import type { SSEEventPayload } from '~~/shared/types'
import { notify } from '~/composables/useNotificationBridge'
Expand All @@ -105,22 +111,38 @@ const route = useRoute()
const colorMode = useColorMode()
const runtimeConfig = useRuntimeConfig()

const { tokens, loadTokens, deleteToken: removeToken } = useTokens()
const tokensStore = useTokensStore()
const { data: tokens, refetch: refetchTokens } = tokensStore.useTokensList()
const { mutateAsync: deleteToken } = tokensStore.useDeleteToken()

const sse = useSSE()

const sessionRestoreEnabled = runtimeConfig.public?.sessionRestoreEnabled !== false

const selectedToken = ref<string>('')
const friendlyId = ref<string>('')
const isDeleting = ref(false)
const showDeleteModal = ref(false)
const showRestoreModal = ref(false)
const sessionInfo = ref<{ friendlyId: string } | null>(null)
const authRequired = ref(false)
const showMobileExtras = ref(false)

// Query for the currently selected token to get friendlyId
const { data: currentTokenData } = tokensStore.useToken(selectedToken)

// Watch for changes in token data to update friendlyId
watch(currentTokenData, (tokenData) => {
if (tokenData) {
friendlyId.value = tokenData.friendlyId || ''
} else {
friendlyId.value = ''
}
})

const checkAuthStatus = async () => {
try {
const response = await $fetch('/api/auth/status')
const response = await $fetch<{ required: boolean }>('/api/auth/status')
authRequired.value = response.required
} catch {
authRequired.value = false
Expand All @@ -135,7 +157,7 @@ watch(selectedToken, newVal => {

const loadSessionInfo = async () => {
try {
const data = await $fetch('/api/session')
const data = await $fetch<{ friendlyId: string }>('/api/session')
if (data?.friendlyId) {
sessionInfo.value = { friendlyId: data.friendlyId }
}
Expand Down Expand Up @@ -165,12 +187,12 @@ const copySessionId = async () => {
}

const copyPayloadUrl = async () => {
if (!selectedToken.value) {
if (!selectedToken.value && !friendlyId.value) {
return
}

const origin = 'undefined' !== typeof window ? window.location.origin : ''
const url = `${origin}/api/payload/${selectedToken.value}`
const url = `${origin}/api/payload/${friendlyId.value || selectedToken.value}`

try {
if (false === (await copyText(url))) {
Expand Down Expand Up @@ -203,12 +225,13 @@ const tokenOptions = computed(() => {
})
})

watch(() => route.path, (path) => {
watch(() => route.path, async (path) => {
const match = path.match(/\/token\/(.+)/)
if (match && match[1]) {
selectedToken.value = match[1]
} else {
selectedToken.value = ''
friendlyId.value = ''
}
showMobileExtras.value = false
}, { immediate: true })
Expand All @@ -228,13 +251,12 @@ function handleClientEvent(payload: SSEEventPayload) {
return
}

loadTokens()
refetchTokens()
}

let unsubscribe: (() => void) | null = null

onMounted(async () => {
await loadTokens()
await loadSessionInfo()
await checkAuthStatus()
unsubscribe = sse.onAny(handleClientEvent)
Expand All @@ -257,7 +279,7 @@ const toggleMobileExtras = () => {

const handleLogout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
await $fetch<{ ok: boolean }>('/api/auth/logout', { method: 'POST' })
notify({
title: 'Logged out',
description: 'You have been logged out successfully.',
Expand Down Expand Up @@ -296,7 +318,7 @@ const confirmDelete = async () => {
const activeIndex = currentTokens.findIndex((t: { id: string }) => t.id === selectedToken.value)
const fallback = activeIndex !== -1 ? currentTokens[activeIndex + 1] ?? currentTokens[activeIndex - 1] : undefined

await removeToken(selectedToken.value)
await deleteToken(selectedToken.value)

if (fallback) {
await navigateTo(`/token/${fallback.id}`)
Expand Down
25 changes: 6 additions & 19 deletions app/components/IngestRequestModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Content-Type: application/json

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRequestsStore } from '~/stores/requests'

const props = withDefaults(defineProps<{
modelValue: boolean
Expand All @@ -115,6 +116,9 @@ const emit = defineEmits<{
(e: 'success'): void
}>()

const requestsStore = useRequestsStore()
const { mutateAsync: ingestRequest, isPending: loading } = requestsStore.useIngestRequest()

const isOpen = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
Expand All @@ -123,7 +127,6 @@ const isOpen = computed({
const rawRequest = ref('')
const clientIp = ref('')
const remoteIp = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const success = ref<{ id: string } | null>(null)
const errors = ref<{
Expand Down Expand Up @@ -182,7 +185,6 @@ const handleIngest = async () => {
return
}

loading.value = true
error.value = null
success.value = null

Expand All @@ -206,21 +208,8 @@ const handleIngest = async () => {
body.remoteIp = remoteIp.value
}

const res = await fetch(`/api/token/${props.tokenId}/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})

if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.message || `HTTP ${res.status}: ${res.statusText}`)
}

const data = await res.json()
success.value = { id: data.request.id }
const data = await ingestRequest({ tokenId: props.tokenId, body })
success.value = { id: data?.request?.id || '' }

emit('success')

Expand All @@ -229,8 +218,6 @@ const handleIngest = async () => {
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to ingest request'
console.error('Failed to ingest request:', err)
} finally {
loading.value = false
}
}

Expand Down
31 changes: 24 additions & 7 deletions app/components/NotificationToggle.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<template>
<ClientOnly>
<UTooltip :text="tooltipText" :shortcuts="[]">
<UButton :icon="currentIcon" :color="notificationType === 'browser' ? 'primary' : 'neutral'" variant="ghost"
:aria-label="ariaLabel" :disabled="!isBrowserNotificationSupported && notificationType === 'toast'"
@click="handleToggle" />
</UTooltip>
<div class="flex items-center gap-2">
<UTooltip :text="muteTooltipText" :shortcuts="[]">
<UButton :icon="muteIcon" :color="isMuted ? 'error' : 'neutral'" variant="ghost"
:aria-label="muteAriaLabel" @click="handleMuteToggle" />
</UTooltip>
<UTooltip :text="tooltipText" :shortcuts="[]">
<UButton :icon="currentIcon" :color="notificationType === 'browser' ? 'primary' : 'neutral'"
variant="ghost" :aria-label="ariaLabel"
:disabled="!isBrowserNotificationSupported && notificationType === 'toast'"
@click="handleToggle" />
</UTooltip>
</div>
</ClientOnly>
</template>

Expand All @@ -16,10 +23,18 @@ const {
notificationType,
toggleNotificationType,
isBrowserNotificationSupported,
browserPermission
browserPermission,
isMuted,
toggleMute
} = useNotificationBridge()

const currentIcon = computed(() => 'browser' === notificationType.value ? 'i-lucide-bell' : 'i-lucide-bell-off')
const currentIcon = computed(() => 'browser' === notificationType.value ? 'i-lucide-monitor' : 'i-lucide-message-square')

const muteIcon = computed(() => isMuted.value ? 'i-lucide-bell-off' : 'i-lucide-bell-ring')

const muteTooltipText = computed(() => isMuted.value ? 'Notifications muted (click to unmute)' : 'Notifications active (click to mute)')

const muteAriaLabel = computed(() => isMuted.value ? 'Unmute notifications' : 'Mute notifications')

const tooltipText = computed(() => {
if (!isBrowserNotificationSupported.value) {
Expand All @@ -40,4 +55,6 @@ const tooltipText = computed(() => {
const ariaLabel = computed(() => 'browser' === notificationType.value ? 'Switch to toast notifications' : 'Switch to browser notifications')

const handleToggle = async () => await toggleNotificationType()

const handleMuteToggle = () => toggleMute()
</script>
4 changes: 2 additions & 2 deletions app/components/RestoreSessionModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<p class="text-sm text-gray-600 dark:text-gray-400">
Enter your session ID to restore your tokens and requests.
</p>

<div class="space-y-2">
<label for="session-id" class="block font-bold text-sm text-gray-700 dark:text-gray-300">
Session ID
Expand Down Expand Up @@ -58,7 +58,7 @@ const restore = async () => {
loading.value = true

try {
const response = await $fetch('/api/session/restore', {
const response = await $fetch<{ success: boolean }>('/api/session/restore', {
method: 'POST',
body: { sessionId: sessionId.value.trim() },
})
Expand Down
9 changes: 5 additions & 4 deletions app/components/TokenSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
<div class="flex-1 min-w-0 flex items-center gap-2">
<ULink :to="`/token/${token.id}`"
class="font-mono text-sm text-primary hover:underline block truncate">
{{ shortSlug(token.id) }}
{{ token.friendlyId ? token.friendlyId : shortSlug(token.id) }}
</ULink>
<UBadge v-if="incomingTokenIds && incomingTokenIds.has(token.id)" color="success" variant="solid" size="xs"
class="font-semibold uppercase">
<UBadge v-if="incomingTokenIds && incomingTokenIds.has(token.id)" color="success"
variant="solid" size="xs" class="font-semibold uppercase">
New
</UBadge>
</div>
Expand All @@ -55,7 +55,8 @@
</UTooltip>
</div>
</div>
<div class="flex items-center justify-between gap-3 text-xs text-gray-500 dark:text-gray-400 select-none">
<div
class="flex items-center justify-between gap-3 text-xs text-gray-500 dark:text-gray-400 select-none">
<span>{{ getRequestCount(token) }} requests</span>
<span>{{ formatDate(token.createdAt) }}</span>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/token/RawRequestCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</UTooltip>

<UTooltip v-if="request" text="Download raw request">
<ULink type="button" variant="ghost" color="neutral" size="xs" role="button"
<ULink :external="true" type="button" variant="ghost" color="neutral" size="xs" role="button"
:href="`/api/token/${tokenId}/requests/${request?.id}/raw`" target="_blank">
<UIcon name="i-lucide-download" size="xs" class="h-4 w-4" />
</ULink>
Expand Down
3 changes: 1 addition & 2 deletions app/components/token/RequestDetailsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
<div class="flex items-center gap-1">
<div class="flex justify-end">
<UTooltip text="Download body">
<ULink role="button" variant="ghost" color="neutral" size="xs" target="_blank"
<ULink :external="true" role="button" variant="ghost" color="neutral" size="xs" target="_blank"
:href="`/api/token/${tokenId}/requests/${request.id}/body/download`">
<UIcon name="i-lucide-download" size="xs" class="h-4 w-4" />
</ULink>
Expand Down Expand Up @@ -306,7 +306,6 @@ const isBinary = computed(() => Boolean(props.request?.isBinary))

watch([() => props.request?.id, isBodyOpen], async ([newId, isOpen]: [string | undefined, boolean]) => {
if (!newId || !isOpen) {
console.log('Not loading body - no request or not open')
return
}

Expand Down
Loading