Files
web-antrean/pages/index.vue
T
2026-02-10 09:51:17 +07:00

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>