update Keycloak logout & fullLogout

This commit is contained in:
2025-08-28 14:46:51 +07:00
parent 43c0aca0c0
commit 6bb29e35f0
22 changed files with 2171 additions and 291 deletions
+81
View File
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Form } from 'vee-validate';
/*Social icons*/
import google from '@/assets/images/svgs/google-icon.svg';
import facebook from '@/assets/images/svgs/facebook-icon.svg';
const checkbox = ref(false);
const valid = ref(false);
const show1 = ref(false);
const password = ref('admin123');
const username = ref('info@wrappixel.com');
const passwordRules = ref([
(v: string) => !!v || 'Password is required',
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
]);
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
const { signIn, getProviders } = useAuth()
const providers = await getProviders()
const login = () => {
console.log(providers)
}
</script>
<template>
<v-row class="d-flex mb-3">
<v-col cols="6" sm="6" class="pr-2">
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
<img :src="google" height="16" class="mr-2" alt="google" />
Google
</v-btn>
</v-col>
<v-col cols="6" sm="6" class="pl-2">
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
Facebook
</v-btn>
</v-col>
</v-row>
<div class="d-flex align-center text-center mb-6">
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
<span class="bg-surface px-5 py-3 position-relative">or sign in with</span>
</div>
</div>
<Form class="mt-5">
<v-label class="font-weight-semibold pb-2 ">Username</v-label>
<VTextField
v-model="username"
:rules="emailRules"
class="mb-8"
required
hide-details="auto"
></VTextField>
<v-label class="font-weight-semibold pb-2 ">Password</v-label>
<VTextField
v-model="password"
:rules="passwordRules"
required
hide-details="auto"
type="password"
class="pwdInput"
></VTextField>
<div class="d-flex flex-wrap align-center my-3 ml-n2">
<v-checkbox class="pe-2" v-model="checkbox" :rules="[(v:any) => !!v || 'You must agree to continue!']" required hide-details color="primary">
<template v-slot:label class="font-weight-medium">Remeber this Device</template>
</v-checkbox>
<div class="ml-sm-auto">
<RouterLink to="" class="text-primary text-decoration-none font-weight-medium"
>Forgot Password ?</RouterLink
>
</div>
</div>
<v-btn v-for="provider in providers" :key="provider" @click="signIn(provider.id)" color="primary" size="large"
block flat>Sign in with {{ provider.name }}</v-btn>
<!-- <v-btn size="large" color="primary" :disabled="valid" block type="submit" flat>Sign In</v-btn> -->
<!-- <div class="mt-2">
<v-alert color="error"></v-alert>
</div> -->
</Form>
</template>
+306 -73
View File
@@ -1,81 +1,314 @@
<!-- components/auth/LoginForm.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { Form } from 'vee-validate';
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useUserInfo } from "~/composables/useUserInfo";
import { useRouter, useRoute } from "vue-router";
/*Social icons*/
import google from '@/assets/images/svgs/google-icon.svg';
import facebook from '@/assets/images/svgs/facebook-icon.svg';
const router = useRouter();
const route = useRoute();
const userInfo = useUserInfo();
// State
const isLoggingIn = ref(false);
const isLoggingOut = ref(false);
const errorMessage = ref("");
const showDebugInfo = ref(false);
// **PERBAIKAN: Enhanced Query Parameter Handling**
const reason = computed(() => route.query.reason as string);console.log("reason:", reason.value);
const shouldShowContinue = computed(() => route.query.continue === "true");
const returnUrl = computed(() => (route.query.returnUrl as string) || "/");console.log("returnUrl:", returnUrl.value);
const getReasonMessage = () => {
// console.log("LoginForm: current reason value:", reason.value);
switch (reason.value) {
case "idle":
return {
type: "warning" as const,
title: "Sesi Tidak Aktif",
message:
"Anda telah tidak aktif selama 15 menit. Sesi Keycloak masih valid.",
icon: "mdi-clock-alert-outline"
};
case "tab_inactive":
return {
type: "info" as const,
title: "Tab Kembali Aktif",
message:
"Tab telah tidak aktif lebih dari 10 menit. Sesi Keycloak masih valid.",
icon: "mdi-tab"
};
case "session_expired":
return {
type: "error" as const,
title: "Sesi Berakhir",
message: "Sesi Anda telah berakhir. Silakan login kembali.",
icon: "mdi-clock-alert"
};
case "session_error":
return {
type: "error" as const,
title: "Error Sesi",
message: "Terjadi error pada sesi Anda. Silakan login kembali.",
icon: "mdi-alert"
};
case "auth_required":
return {
type: "error" as const,
title: "Autentikasi Diperlukan",
message: "Anda harus login untuk mengakses halaman ini.",
icon: "mdi-lock-alert"
};
default:
return null;
}
};
// **PERBAIKAN: Enhanced Keycloak Login**
const signInKeycloak = async () => {
try {
isLoggingIn.value = true;
errorMessage.value = "";
await userInfo.login("keycloak");
} catch (error: any) {
errorMessage.value = error.message || "Login gagal. Silakan coba lagi.";
} finally {
isLoggingIn.value = false;
}
};
// **PERBAIKAN: Enhanced Continue Handler**
const handleContinue = async () => {
try {
// Update activity untuk reset idle timer
userInfo.updateActivity();
// Force refresh session to ensure session is valid
await userInfo.forceRefreshSession();
// Navigate ke return URL atau dashboard
await router.push(returnUrl.value);
} catch (error) {
console.error("Navigation error:", error);
errorMessage.value = "Gagal melanjutkan sesi. Silakan coba lagi.";
}
};
// **PERBAIKAN: Enhanced Logout Handler**
const handleSignOut = async () => {
try {
isLoggingOut.value = true;
await userInfo.fullLogout();
} catch (error) {
//console.error("Logout error:", error);
errorMessage.value = "Logout gagal. Silakan coba lagi.";
} finally {
isLoggingOut.value = false;
}
};
// **PERBAIKAN: Ubah tipe variabel untuk menerima null**
let cleanupSessionMonitoring: (() => void) | null = null;
// **PERBAIKAN: Enhanced onMounted dengan proper type handling**
onMounted(() => {
if (userInfo.isAuthenticated.value) {
const cleanup = userInfo.startSessionMonitoring();
if (cleanup) {
cleanupSessionMonitoring = cleanup;
}
}
});
// **PERBAIKAN: Enhanced onUnmounted dengan null check**
onUnmounted(() => {
if (cleanupSessionMonitoring) {
cleanupSessionMonitoring();
cleanupSessionMonitoring = null;
}
});
// User display functions
const getUserDisplayName = () => {
if (!userInfo.user.value) return "";
const user = userInfo.user.value as any;
return (
user.name ||
user.given_name ||
`${user.given_name || ""} ${user.family_name || ""}`.trim() ||
user.preferred_username ||
user.username ||
user.email?.split("@")[0] ||
"User"
);
};
console.log("user is authenticated:", userInfo.isAuthenticated.value, shouldShowContinue.value);
const checkbox = ref(false);
const valid = ref(false);
const show1 = ref(false);
const password = ref('admin123');
const username = ref('info@wrappixel.com');
const passwordRules = ref([
(v: string) => !!v || 'Password is required',
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
]);
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
const { signIn, getProviders } = useAuth()
const providers = await getProviders()
const login = () => {
console.log(providers)
}
</script>
<template>
<v-row class="d-flex mb-3">
<v-col cols="6" sm="6" class="pr-2">
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
<img :src="google" height="16" class="mr-2" alt="google" />
Google
</v-btn>
</v-col>
<v-col cols="6" sm="6" class="pl-2">
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
Facebook
</v-btn>
</v-col>
</v-row>
<div class="d-flex align-center text-center mb-6">
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
<span class="bg-surface px-5 py-3 position-relative">or sign in with</span>
</div>
</div>
<Form class="mt-5">
<v-label class="font-weight-semibold pb-2 ">Username</v-label>
<VTextField
v-model="username"
:rules="emailRules"
class="mb-8"
required
hide-details="auto"
></VTextField>
<v-label class="font-weight-semibold pb-2 ">Password</v-label>
<VTextField
v-model="password"
:rules="passwordRules"
required
hide-details="auto"
type="password"
class="pwdInput"
></VTextField>
<div class="d-flex flex-wrap align-center my-3 ml-n2">
<v-checkbox class="pe-2" v-model="checkbox" :rules="[(v:any) => !!v || 'You must agree to continue!']" required hide-details color="primary">
<template v-slot:label class="font-weight-medium">Remeber this Device</template>
</v-checkbox>
<div class="ml-sm-auto">
<RouterLink to="" class="text-primary text-decoration-none font-weight-medium"
>Forgot Password ?</RouterLink
>
</div>
<v-card-title class="text-h4 text-center mb-6">
<v-icon
icon="mdi-shield-account"
size="48"
color="primary"
class="mb-2"
></v-icon>
<br />
Masuk ke Sistem
</v-card-title>
<!-- **PERBAIKAN: Loading State** -->
<div v-if="userInfo.isLoading.value" class="text-center py-8">
<v-progress-circular
indeterminate
color="primary"
size="64"
></v-progress-circular>
<p class="text-body-1 mt-4">Memeriksa autentikasi...</p>
</div>
<!-- **PERBAIKAN: Continue Panel untuk User yang Sudah Authenticated** -->
<div
v-else-if="!shouldShowContinue || !reason || reason === 'auth_required'"
class="text-center"
>
<v-card-text>
<!-- **PERBAIKAN: Error Alert** -->
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
class="mb-6"
closable
@click:close="errorMessage = ''"
>
{{ errorMessage }}
</v-alert>
<!-- **PERBAIKAN: Reason Alert** -->
<v-alert
v-if="getReasonMessage()"
:type="getReasonMessage()!.type"
variant="tonal"
class="mb-6"
:icon="getReasonMessage()!.icon"
>
<div class="text-h6 mb-2">{{ getReasonMessage()!.title }}</div>
<div class="text-body-2">{{ getReasonMessage()!.message }}</div>
</v-alert>
<!-- Welcome Message -->
<div class="text-center mb-6">
<h5 class="text-h5 mb-2">Selamat Datang di Sistem RSSA</h5>
<p class="text-body-2 text-medium-emphasis">
Silakan masuk untuk melanjutkan ke dashboard Anda
</p>
</div>
<!-- **PERBAIKAN: Keycloak Login Button** -->
<v-btn
color="primary"
size="large"
variant="flat"
block
class="mb-4"
@click="signInKeycloak"
:loading="isLoggingIn"
:disabled="isLoggingIn"
prepend-icon="mdi-key"
>
<span>{{
isLoggingIn ? "Menghubungkan..." : "Masuk dengan Keycloak"
}}</span>
</v-btn>
<!-- Additional Info -->
<div class="text-center mt-6">
<p class="text-body-2 text-medium-emphasis">
Belum memiliki akun?
<span class="text-primary font-weight-medium">
Hubungi Administrator
</span>
</p>
</div>
</v-card-text>
</div>
<!-- **PERBAIKAN: Login Form untuk User yang Belum Authenticated** -->
<div v-else>
<v-card-text class="pb-4">
<!-- **PERBAIKAN: Reason-based Alert** -->
<v-alert
v-if="getReasonMessage()"
:type="getReasonMessage()!.type"
variant="tonal"
class="mb-6 text-left"
:icon="getReasonMessage()!.icon"
>
<div class="text-h6 mb-2">{{ getReasonMessage()!.title }}</div>
<div class="text-body-2">{{ getReasonMessage()!.message }}</div>
</v-alert>
<!-- Welcome Message -->
<v-alert
v-else
type="success"
variant="tonal"
class="mb-6 text-left"
icon="mdi-check-circle"
>
<div class="text-h6 mb-2">
Selamat datang kembali, <strong>{{ getUserDisplayName() }}</strong
>!
</div>
<v-btn v-for="provider in providers" :key="provider" @click="signIn(provider.id)" color="primary" size="large"
block flat>Sign in with {{ provider.name }}</v-btn>
<!-- <v-btn size="large" color="primary" :disabled="valid" block type="submit" flat>Sign In</v-btn> -->
<!-- <div class="mt-2">
<v-alert color="error"></v-alert>
</div> -->
</Form>
<div class="text-body-2">Anda masih terhubung dengan Keycloak.</div>
</v-alert>
<!-- Session Info -->
<v-card variant="outlined" class="mb-4">
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-account-circle" class="mr-2"></v-icon>
Informasi Sesi
</v-card-title>
<v-card-text>
<div class="d-flex justify-center">
<v-chip color="success" variant="outlined" size="small">
<v-icon start icon="mdi-clock-check"></v-icon>
Sesi Keycloak Aktif
</v-chip>
</div>
</v-card-text>
</v-card>
</v-card-text>
<!-- **PERBAIKAN: Action Buttons** -->
<v-card-actions class="justify-center px-6 pb-6">
<v-btn
color="primary"
size="large"
variant="flat"
class="mr-3 px-8"
@click="handleContinue"
prepend-icon="mdi-arrow-right"
>
Lanjutkan ke Aplikasi
</v-btn>
<v-btn
color="error"
size="large"
variant="outlined"
@click="handleSignOut"
prepend-icon="mdi-logout"
:loading="isLoggingOut"
:disabled="isLoggingOut"
>
{{ isLoggingOut ? "Keluar..." : "Keluar dari Keycloak" }}
</v-btn>
</v-card-actions>
</div>
</template>