466 lines
15 KiB
Vue
466 lines
15 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from "vue";
|
|
|
|
definePageMeta({
|
|
middleware: ["auth"],
|
|
});
|
|
|
|
interface SessionData {
|
|
user: {
|
|
name: string;
|
|
email: string;
|
|
roles?: string | string[];
|
|
};
|
|
status: string;
|
|
createdAt: number;
|
|
expiresAt: number;
|
|
accessToken: string;
|
|
idToken: string;
|
|
refreshToken: string;
|
|
accessTokenPayload: any;
|
|
idTokenPayload: any;
|
|
fullSessionObject: any;
|
|
}
|
|
|
|
const sessionData = ref<SessionData | null>(null);
|
|
const loading = ref(true);
|
|
const authError = ref<any>(null);
|
|
|
|
const sessionExpiresDate = computed(() => {
|
|
if (!sessionData.value?.expiresAt) return "N/A";
|
|
return new Date(sessionData.value.expiresAt).toLocaleString("id-ID");
|
|
});
|
|
|
|
const sessionCreatedDate = computed(() => {
|
|
if (!sessionData.value?.createdAt) return "N/A";
|
|
return new Date(sessionData.value.createdAt).toLocaleString("id-ID");
|
|
});
|
|
|
|
const remainingTime = computed(() => {
|
|
if (!sessionData.value?.expiresAt) return "N/A";
|
|
const remaining = sessionData.value.expiresAt - Date.now();
|
|
if (remaining <= 0) return "Expired";
|
|
|
|
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
|
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
|
const seconds = Math.floor((remaining % (1000 * 60)) / 1000);
|
|
|
|
return `${hours}h ${minutes}m ${seconds}s`;
|
|
});
|
|
|
|
const tokenIssuedAt = computed(() => {
|
|
if (!sessionData.value?.accessTokenPayload?.iat) return "N/A";
|
|
return new Date(
|
|
sessionData.value.accessTokenPayload.iat * 1000,
|
|
).toLocaleString("id-ID");
|
|
});
|
|
|
|
const tokenExpiresAt = computed(() => {
|
|
if (!sessionData.value?.accessTokenPayload?.exp) return "N/A";
|
|
return new Date(
|
|
sessionData.value.accessTokenPayload.exp * 1000,
|
|
).toLocaleString("id-ID");
|
|
});
|
|
|
|
const formatJson = (data: any) => {
|
|
return JSON.stringify(data, null, 2);
|
|
};
|
|
|
|
const copyToClipboard = async (text: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
alert("Berhasil disalin ke clipboard!");
|
|
} catch (err) {
|
|
console.error("Gagal menyalin: ", err);
|
|
}
|
|
};
|
|
|
|
const expandedSections = ref<Record<string, boolean>>({
|
|
idToken: false,
|
|
accessToken: false,
|
|
refreshToken: false,
|
|
});
|
|
|
|
// External API Validation State
|
|
const externalValidationLoading = ref(false);
|
|
const externalValidationResult = ref<any>(null);
|
|
const externalValidationError = ref<string | null>(null);
|
|
|
|
const testTokenValidation = async () => {
|
|
try {
|
|
externalValidationLoading.value = true;
|
|
externalValidationError.value = null;
|
|
externalValidationResult.value = null;
|
|
|
|
console.log("📡 Testing token validation with external API proxy...");
|
|
|
|
const response = await $fetch<any>("/api/external/validate-token", {
|
|
method: "POST",
|
|
});
|
|
|
|
externalValidationResult.value = response;
|
|
|
|
if (!response.success) {
|
|
externalValidationError.value = response.error || "Validation failed";
|
|
}
|
|
|
|
console.log("✅ Validation test completed:", response);
|
|
} catch (e: any) {
|
|
console.error("❌ Validation test failed:", e);
|
|
externalValidationError.value = e.data?.message || e.message || "Request failed";
|
|
} finally {
|
|
externalValidationLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const toggleSection = (section: string) => {
|
|
expandedSections.value[section] = !expandedSections.value[section];
|
|
};
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const data = await $fetch<SessionData>("/api/auth/session");
|
|
sessionData.value = data;
|
|
authError.value = null;
|
|
} catch (e: any) {
|
|
console.error("Failed to fetch session data:", e);
|
|
authError.value =
|
|
e.data?.statusMessage || "Session check failed. Please log in.";
|
|
sessionData.value = null;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="dashboard-container">
|
|
<div class="dashboard-content">
|
|
<div class="dashboard-header">
|
|
<h1>Complete Session Data</h1>
|
|
</div>
|
|
|
|
<div v-if="loading" class="dashboard-body">
|
|
<div class="text-center p-8">
|
|
<v-progress-circular
|
|
indeterminate
|
|
color="primary"
|
|
size="48"
|
|
></v-progress-circular>
|
|
<p class="mt-4">Memuat data sesi...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="authError" class="dashboard-body">
|
|
<div class="bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
|
<h3 class="font-bold text-red-800">Authentication Error</h3>
|
|
<p class="text-red-700">{{ authError }}</p>
|
|
<NuxtLink
|
|
to="/LoginPage"
|
|
class="text-blue-600 hover:underline mt-2 inline-block"
|
|
>
|
|
Go to Login Page
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="sessionData" class="dashboard-body">
|
|
<!-- External API Validation Section -->
|
|
<div class="section mb-8 bg-blue-50 p-6 rounded-lg border border-blue-200">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="section-title mb-0 border-0 pb-0">External API Token Validation</h3>
|
|
<v-btn
|
|
color="primary"
|
|
:loading="externalValidationLoading"
|
|
prepend-icon="mdi-shield-check"
|
|
@click="testTokenValidation"
|
|
>
|
|
Test Token Validation
|
|
</v-btn>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Test and validate your current access token against the external API endpoint:
|
|
<code class="bg-blue-100 px-1 rounded text-blue-800">http://10.10.150.100:8084/api/v1/auth/me</code>
|
|
</p>
|
|
|
|
<!-- Validation Status -->
|
|
<div v-if="externalValidationResult || externalValidationError" class="mt-4">
|
|
<v-alert
|
|
v-if="externalValidationResult?.success"
|
|
type="success"
|
|
variant="tonal"
|
|
title="Validation Successful"
|
|
class="mb-4"
|
|
>
|
|
The access token is valid and accepted by the external API.
|
|
</v-alert>
|
|
|
|
<v-alert
|
|
v-else-if="externalValidationError"
|
|
type="error"
|
|
variant="tonal"
|
|
title="Validation Failed"
|
|
class="mb-4"
|
|
>
|
|
{{ externalValidationError }}
|
|
<div v-if="externalValidationResult?.details" class="mt-2 text-xs">
|
|
<strong>Details:</strong> {{ externalValidationResult.details }}
|
|
</div>
|
|
</v-alert>
|
|
|
|
<!-- Result Data -->
|
|
<div v-if="externalValidationResult?.data" class="bg-white p-4 rounded border border-gray-200 mt-2">
|
|
<h4 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">External API Response:</h4>
|
|
<div class="payload-content text-xs">
|
|
{{ formatJson(externalValidationResult.data) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Basic User Information -->
|
|
<div class="section">
|
|
<h3 class="section-title">Basic User Information</h3>
|
|
<div class="info-grid">
|
|
<div class="label">Name:</div>
|
|
<div class="value">{{ sessionData.user.name }}</div>
|
|
|
|
<div class="label">Email:</div>
|
|
<div class="value">{{ sessionData.user.email }}</div>
|
|
|
|
<div class="label">Roles:</div>
|
|
<div class="value">
|
|
{{ sessionData.user.roles || "authenticated" }}
|
|
</div>
|
|
|
|
<div class="label">Status:</div>
|
|
<div class="value">{{ sessionData.status }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OAuth & Token Metadata -->
|
|
<div class="section">
|
|
<h3 class="section-title">OAuth & Token Metadata</h3>
|
|
<div class="info-grid">
|
|
<div class="label">Subject (User ID):</div>
|
|
<div class="value">
|
|
{{ sessionData.accessTokenPayload?.sub || "N/A" }}
|
|
</div>
|
|
|
|
<div class="label">Issuer:</div>
|
|
<div class="value">
|
|
{{ sessionData.accessTokenPayload?.iss || "N/A" }}
|
|
</div>
|
|
|
|
<div class="label">Audience:</div>
|
|
<div class="value">
|
|
{{ sessionData.accessTokenPayload?.aud || "N/A" }}
|
|
</div>
|
|
|
|
<div class="label">Token Issued At:</div>
|
|
<div class="value">{{ tokenIssuedAt }}</div>
|
|
|
|
<div class="label">Token Expires At:</div>
|
|
<div class="value">{{ tokenExpiresAt }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Token Information -->
|
|
<div class="section">
|
|
<h3 class="section-title">Token Information (after callbacks […])</h3>
|
|
|
|
<!-- ID Token -->
|
|
<div class="token-section">
|
|
<div class="token-header">
|
|
<h4>ID Token (session.id_token)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(sessionData.idToken)"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="token-content"
|
|
:class="{ collapsed: !expandedSections.idToken }"
|
|
>
|
|
{{ sessionData.idToken }}
|
|
</div>
|
|
<button class="toggle-btn" @click="toggleSection('idToken')">
|
|
{{ expandedSections.idToken ? "Show less" : "Show more" }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Access Token -->
|
|
<div class="token-section">
|
|
<div class="token-header">
|
|
<h4>JWT Token (session.jwt)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(sessionData.accessToken)"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="token-content"
|
|
:class="{ collapsed: !expandedSections.accessToken }"
|
|
>
|
|
{{ sessionData.accessToken }}
|
|
</div>
|
|
<button class="toggle-btn" @click="toggleSection('accessToken')">
|
|
{{ expandedSections.accessToken ? "Show less" : "Show more" }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Refresh Token -->
|
|
<div class="token-section" v-if="sessionData.refreshToken">
|
|
<div class="token-header">
|
|
<h4>Refresh Token (session.refresh_token)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(sessionData.refreshToken)"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="token-content"
|
|
:class="{ collapsed: !expandedSections.refreshToken }"
|
|
>
|
|
{{ sessionData.refreshToken }}
|
|
</div>
|
|
<button class="toggle-btn" @click="toggleSection('refreshToken')">
|
|
{{ expandedSections.refreshToken ? "Show less" : "Show more" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session Timeline -->
|
|
<div class="section">
|
|
<h3 class="section-title">Session Timeline</h3>
|
|
<div class="info-grid">
|
|
<div class="label">Session Created:</div>
|
|
<div class="value">{{ sessionCreatedDate }}</div>
|
|
|
|
<div class="label">Session Expires:</div>
|
|
<div class="value">{{ sessionExpiresDate }}</div>
|
|
|
|
<div class="label">Remaining Time:</div>
|
|
<div class="value" style="font-weight: 600; color: #2563eb">
|
|
{{ remainingTime }}
|
|
</div>
|
|
|
|
<div class="label">Session Scope:</div>
|
|
<div class="value">
|
|
{{
|
|
sessionData.accessTokenPayload?.scope || "openid email profile"
|
|
}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Full Session Object (Debug) -->
|
|
<div class="section payload-section">
|
|
<h3 class="section-title">Complete Raw Session Data (Debug)</h3>
|
|
<div class="token-header mb-2">
|
|
<h4>Full Session Object</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="
|
|
copyToClipboard(formatJson(sessionData.fullSessionObject))
|
|
"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div class="payload-content">
|
|
{{ formatJson(sessionData.fullSessionObject) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Token Payload -->>
|
|
<div class="section payload-section">
|
|
<h3 class="section-title">Access Token Payload (Parsed data […])</h3>
|
|
|
|
<!-- Raw Access Token -->
|
|
<div class="token-header mb-2">
|
|
<h4>Raw Access Token (JWT)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(sessionData.accessToken)"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div class="token-content mb-4" style="max-height: 100px;">
|
|
{{ sessionData.accessToken }}
|
|
</div>
|
|
|
|
<!-- Parsed Payload -->
|
|
<div class="token-header mb-2">
|
|
<h4>Parsed Payload (Decoded from JWT)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="
|
|
copyToClipboard(formatJson(sessionData.accessTokenPayload))
|
|
"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div class="payload-content">
|
|
{{ formatJson(sessionData.accessTokenPayload) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ID Token Payload -->
|
|
<div class="section payload-section">
|
|
<h3 class="section-title">ID Token Payload (Parsed data […])</h3>
|
|
|
|
<!-- Raw ID Token -->
|
|
<div class="token-header mb-2">
|
|
<h4>Raw ID Token (JWT)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(sessionData.idToken)"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div class="token-content mb-4" style="max-height: 100px;">
|
|
{{ sessionData.idToken }}
|
|
</div>
|
|
|
|
<!-- Parsed Payload -->
|
|
<div class="token-header mb-2">
|
|
<h4>Parsed Payload (Decoded from JWT)</h4>
|
|
<button
|
|
class="copy-btn"
|
|
@click="copyToClipboard(formatJson(sessionData.idTokenPayload))"
|
|
>
|
|
<v-icon size="small">mdi-content-copy</v-icon>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<div class="payload-content">
|
|
{{ formatJson(sessionData.idTokenPayload) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
// Styles are in _dashboard.scss
|
|
</style>
|