update Keycloak logout & fullLogout
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user