Some checks failed
Build Docker Image / build (push) Has been cancelled
- Add layout: 'topnav' to all 27 pages that were using default layout - Delete app/layouts/default.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
277 lines
9.7 KiB
Vue
277 lines
9.7 KiB
Vue
<template>
|
|
<Section variant="plain">
|
|
<Stack gap="6">
|
|
<PageHeader
|
|
title="Debug Tokens"
|
|
:actions="[
|
|
{ label: 'Back', icon: 'lucide:arrow-left', to: localePath('/clientarea/profile') }
|
|
]"
|
|
/>
|
|
|
|
<Alert v-if="error" variant="error">
|
|
<Text>{{ error }}</Text>
|
|
</Alert>
|
|
|
|
<Card padding="lg">
|
|
<Stack gap="4">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="3">JWT Tokens</Heading>
|
|
<Button variant="ghost" size="sm" @click="loadTokens" :loading="isLoading">
|
|
<Icon name="lucide:refresh-ccw" size="16" />
|
|
<span>Refresh</span>
|
|
</Button>
|
|
</Stack>
|
|
|
|
<!-- ID Token -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">ID Token</Heading>
|
|
<Button v-if="rawTokens.id" variant="ghost" size="sm" @click="copyToken(rawTokens.id)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.id || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.id) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token: teams -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">Access Token: teams.optovia.ru</Heading>
|
|
<Button v-if="rawTokens.teams" variant="ghost" size="sm" @click="copyToken(rawTokens.teams)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.teams || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.teams) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token: exchange -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">Access Token: exchange.optovia.ru</Heading>
|
|
<Button v-if="rawTokens.exchange" variant="ghost" size="sm" @click="copyToken(rawTokens.exchange)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.exchange || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.exchange) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token: orders -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">Access Token: orders.optovia.ru</Heading>
|
|
<Button v-if="rawTokens.orders" variant="ghost" size="sm" @click="copyToken(rawTokens.orders)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.orders || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.orders) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token: kyc -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">Access Token: kyc.optovia.ru</Heading>
|
|
<Button v-if="rawTokens.kyc" variant="ghost" size="sm" @click="copyToken(rawTokens.kyc)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.kyc || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.kyc) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token: billing -->
|
|
<div class="space-y-2">
|
|
<Stack direction="row" align="center" justify="between">
|
|
<Heading :level="4">Access Token: billing.optovia.ru</Heading>
|
|
<Button v-if="rawTokens.billing" variant="ghost" size="sm" @click="copyToken(rawTokens.billing)">
|
|
<Icon name="lucide:copy" size="16" />
|
|
<span>Copy</span>
|
|
</Button>
|
|
</Stack>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Raw JWT:</Text>
|
|
<input
|
|
type="text"
|
|
readonly
|
|
:value="rawTokens.billing || '—'"
|
|
class="input input-bordered input-sm w-full font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Text variant="label" class="text-xs text-base-content/60">Decoded:</Text>
|
|
<pre class="p-3 rounded-lg bg-base-200 text-xs overflow-x-auto max-h-48">{{ formatJson(decodedTokens.billing) }}</pre>
|
|
</div>
|
|
</div>
|
|
</Stack>
|
|
</Card>
|
|
</Stack>
|
|
</Section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: 'topnav',
|
|
middleware: ['auth-oidc']
|
|
})
|
|
|
|
const RESOURCES = {
|
|
teams: 'https://teams.optovia.ru',
|
|
exchange: 'https://exchange.optovia.ru',
|
|
orders: 'https://orders.optovia.ru',
|
|
kyc: 'https://kyc.optovia.ru',
|
|
billing: 'https://billing.optovia.ru'
|
|
} as const
|
|
|
|
type ResourceKey = keyof typeof RESOURCES
|
|
|
|
const localePath = useLocalePath()
|
|
const auth = useAuth()
|
|
|
|
const isLoading = ref(false)
|
|
const error = ref('')
|
|
|
|
const rawTokens = ref<{ id: string | null } & Record<ResourceKey, string | null>>({
|
|
id: null,
|
|
teams: null,
|
|
exchange: null,
|
|
orders: null,
|
|
kyc: null,
|
|
billing: null
|
|
})
|
|
|
|
const decodedTokens = ref<{ id: unknown } & Record<ResourceKey, unknown>>({
|
|
id: null,
|
|
teams: null,
|
|
exchange: null,
|
|
orders: null,
|
|
kyc: null,
|
|
billing: null
|
|
})
|
|
|
|
function decodeJwt(token?: string | null) {
|
|
if (!token) return null
|
|
try {
|
|
const payload = token.split('.')[1]
|
|
if (!payload) return null
|
|
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
|
|
return JSON.parse(json)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function formatJson(data: unknown) {
|
|
if (!data) return '—'
|
|
return JSON.stringify(data, null, 2)
|
|
}
|
|
|
|
async function copyToken(token: string | null) {
|
|
if (!token) return
|
|
try {
|
|
await navigator.clipboard.writeText(token)
|
|
} catch (e) {
|
|
console.error('Failed to copy:', e)
|
|
}
|
|
}
|
|
|
|
const loadTokens = async () => {
|
|
try {
|
|
isLoading.value = true
|
|
error.value = ''
|
|
|
|
// Get ID token
|
|
try {
|
|
const idToken = await auth.getIdToken()
|
|
rawTokens.value.id = idToken || null
|
|
decodedTokens.value.id = decodeJwt(idToken)
|
|
} catch (e: unknown) {
|
|
rawTokens.value.id = null
|
|
decodedTokens.value.id = { error: e instanceof Error ? e.message : 'Failed to get ID token' }
|
|
}
|
|
|
|
// Get access tokens for ALL resources
|
|
for (const [key, url] of Object.entries(RESOURCES) as [ResourceKey, string][]) {
|
|
try {
|
|
const accessToken = await auth.getAccessToken(url)
|
|
rawTokens.value[key] = accessToken || null
|
|
decodedTokens.value[key] = decodeJwt(accessToken)
|
|
} catch (e: unknown) {
|
|
rawTokens.value[key] = null
|
|
decodedTokens.value[key] = { error: e instanceof Error ? e.message : 'Failed to get access token' }
|
|
}
|
|
}
|
|
} catch (err: unknown) {
|
|
error.value = err instanceof Error ? err.message : 'Error loading tokens'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadTokens()
|
|
})
|
|
</script>
|