Commit fb62f636 by Samuel Taniel Mulyadi

add token expired

1 parent d7205995
......@@ -75,7 +75,7 @@
min-block-size: 100vh; /* Ensures the element covers the full viewport height */
padding-block-start: 10px;
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
}
.main-component-margin {
......
......@@ -13,6 +13,7 @@ const username = ref<string>('')
const civitas = ref<string>('')
const kodeIdentitas = ref<string>('')
const accessToken = ref<string>('')
const tokenParsedExp = ref<number>(0)
const refreshTokenExp = ref<number>(0)
const roles = ref<string[]>([])
const selectedRole = useStorage('selectedRole', 'admin')
......@@ -28,6 +29,7 @@ const refresh = (): void => {
username.value = tokenParsed.preferred_username || ''
civitas.value = tokenParsed.civitas || ''
kodeIdentitas.value = tokenParsed.kodeIdentitas || ''
tokenParsedExp.value = tokenParsed.exp || 0
accessToken.value = keycloakInstance.token || ''
refreshTokenExp.value = refreshedTokenParsed.exp || 0
roles.value = keycloakInstance.resourceAccess?.vueplayground?.roles ?? []
......
<script lang="ts" setup>
import { HorizontalNavLayout } from '@layouts'
import navItems from '@/navigation/horizontal'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import keycloakInstance from '@/keycloak'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import NavbarTokenExpiredTime from '@/layouts/components/NavbarTokenExpiredTime.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import navItems from '@/navigation/horizontal'
// Keycloak & state
const keycloakStore = useKeycloakStore()
const authenticated = computed(() => keycloakStore.authenticated)
const now = ref(Math.floor(Date.now() / 1000))
const tokenLifetime = ref(0)
let timer: ReturnType<typeof setInterval>
onMounted(() => {
// Set tokenLifetime only once on mount
if (keycloakInstance.tokenParsed?.exp && keycloakInstance.tokenParsed?.iat)
tokenLifetime.value = keycloakInstance.tokenParsed.exp - keycloakInstance.tokenParsed.iat
// Start timer
timer = setInterval(() => {
now.value = Math.floor(Date.now() / 1000)
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
// Computed expiration values
const computedExpIn = computed(() => {
return authenticated.value && keycloakInstance.tokenParsed?.exp
? Math.max(keycloakInstance.tokenParsed?.exp - now.value, 0)
: 0
})
</script>
<template>
......@@ -39,10 +73,27 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
:languages="themeConfig.app.i18n.langConfig"
/>
-->
<NavbarTokenExpiredTime />
<NavbarThemeSwitcher />
<!-- <NavbarShortcuts /> -->
<!-- <NavBarNotifications class="me-2" /> -->
<!--
<div class="pa-4">
<VAlert
v-if="authenticated"
type="info"
border="start"
variant="outlined"
class="mb-4"
>
<h3 class="text-h6 font-weight-bold">
Access token expires in {{ computedExpIn }} sec,
Refresh token in {{ computedRefExpIn }} sec,
Session: {{ computedSessionTimer }}
</h3>
</VAlert>
</div>
-->
<UserProfile />
</template>
......
......@@ -5,6 +5,7 @@ import navItems from '@/navigation/vertical'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import NavbarTokenExpiredTime from '@/layouts/components/NavbarTokenExpiredTime.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
// @layouts plugin
......@@ -41,6 +42,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
:languages="themeConfig.app.i18n.langConfig"
/>
-->
<NavbarTokenExpiredTime />
<NavbarThemeSwitcher />
<!-- <NavbarShortcuts /> -->
<!-- <NavBarNotifications class="me-2" /> -->
......
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import keycloakInstance from '@/keycloak'
const keycloakStore = useKeycloakStore()
const authenticated = computed(() => keycloakStore.authenticated)
const now = ref(Math.floor(Date.now() / 1000))
const tokenLifetime = ref(0)
let timer: ReturnType<typeof setInterval>
onMounted(() => {
// Set tokenLifetime only once
if (keycloakInstance.tokenParsed?.exp && keycloakInstance.tokenParsed?.iat)
tokenLifetime.value = keycloakInstance.tokenParsed.exp - keycloakInstance.tokenParsed.iat
timer = setInterval(() => {
now.value = Math.floor(Date.now() / 1000)
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
const computedExpIn = computed(() => {
return authenticated.value && keycloakInstance.tokenParsed?.exp
? Math.max(keycloakInstance.tokenParsed.exp - now.value, 0)
: 0
})
</script>
<template>
<div
v-if="authenticated"
class="me-4"
style="position: relative; block-size: 40px; inline-size: 40px;"
>
<VProgressCircular
:model-value="(computedExpIn / tokenLifetime) * 100"
:size="40"
:width="3"
color="primary"
/>
<div
class="text-subtitle-1 font-weight-bold"
style="position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%);"
>
{{ computedExpIn }}
</div>
</div>
</template>
......@@ -15,6 +15,19 @@ export default defineNuxtPlugin(async nuxtApp => {
keycloakStore.refresh()
console.log('User is authenticated')
navigateTo('/profile')
setInterval(() => {
const now = Math.floor(Date.now() / 1000)
const tokenExp = keycloakInstance.tokenParsed?.exp ?? 0
if (tokenExp <= now) {
console.warn('Token expired. Logging out...')
keycloakInstance.logout({ redirectUri: `${window.location.origin}/login` })
}
else {
console.log('Token expires in:', tokenExp - now, 'seconds')
}
}, 10_000)
}
else {
console.log('User is not authenticated')
......
<script lang="ts" setup>
import { useKeycloakStore } from "@core/stores/keycloakStore";
import { useKeycloakStore } from '@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore();
const keycloakStore = useKeycloakStore()
const isCurrentPasswordVisible = ref(false);
const isNewPasswordVisible = ref(false);
const isConfirmPasswordVisible = ref(false);
const currentPassword = ref("");
const newPassword = ref("");
const newPasswordError = ref("");
const confirmPassword = ref("");
const isSubmitting = ref(false);
const isCurrentPasswordVisible = ref(false)
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
const currentPassword = ref('')
const newPassword = ref('')
const newPasswordError = ref('')
const confirmPassword = ref('')
const isSubmitting = ref(false)
watch(newPassword, (newValue) => {
watch(newPassword, newValue => {
if (currentPassword.value && newValue === currentPassword.value) {
newPasswordError.value =
"Kata sandi baru tidak boleh sama dengan kata sandi lama.";
} else {
newPasswordError.value = "";
newPasswordError.value
= 'Kata sandi baru tidak boleh sama dengan kata sandi lama.'
}
});
else {
newPasswordError.value = ''
}
})
const passwordRequirements = [
"Panjang minimal 8 karakter, maksimal 20 karakter",
"Minimal satu karakter huruf besar",
"Minimal satu angka",
"Minimal satu simbol, atau karakter spasi",
];
'Panjang minimal 8 karakter, maksimal 20 karakter',
'Minimal satu karakter huruf besar',
'Minimal satu angka',
'Minimal satu simbol, atau karakter spasi',
]
// Aturan Validasi
const oldPasswordRules = [
(v: string) => !!v || "Konfirmasi kata sandi diperlukan",
];
(v: string) => !!v || 'Konfirmasi kata sandi diperlukan',
]
const passwordRules = [
(v: string) => !!v || "Kata sandi diperlukan",
(v: string) => v.length >= 8 || "Kata sandi minimal terdiri dari 8 karakter",
(v: string) => !!v || 'Kata sandi diperlukan',
(v: string) => v.length >= 8 || 'Kata sandi minimal terdiri dari 8 karakter',
(v: string) =>
/[a-z]/.test(v) || "Kata sandi setidaknya mengandung satu huruf kecil",
/[a-z]/.test(v) || 'Kata sandi setidaknya mengandung satu huruf kecil',
(v: string) =>
/[A-Z]/.test(v) || "Kata sandi setidaknya mengandung satu huruf besar",
(v: string) => /\d/.test(v) || "Kata sandi setidaknya berisi satu angka",
/[A-Z]/.test(v) || 'Kata sandi setidaknya mengandung satu huruf besar',
(v: string) => /\d/.test(v) || 'Kata sandi setidaknya berisi satu angka',
(v: string) =>
/[ !"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]/.test(v) ||
"Kata sandi setidaknya mengandung satu simbol atau spasi",
];
/[ !"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]/.test(v)
|| 'Kata sandi setidaknya mengandung satu simbol atau spasi',
]
const confirmPasswordRules = [
(v: string) => !!v || "Konfirmasi kata sandi diperlukan",
(v: string) => v === newPassword.value || "Kata sandi tidak cocok",
];
(v: string) => !!v || 'Konfirmasi kata sandi diperlukan',
(v: string) => v === newPassword.value || 'Kata sandi tidak cocok',
]
// Generate Password
function generatePassword(length: number = 10): string {
const lowercase = "abcdefghijklmnopqrstuvwxyz";
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const numbers = "0123456789";
const symbols = ` !"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`;
const lowercase = 'abcdefghijklmnopqrstuvwxyz'
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const numbers = '0123456789'
const symbols = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
// At least one character from each required category
const mandatoryCharacters = [
......@@ -64,113 +65,119 @@ function generatePassword(length: number = 10): string {
uppercase[Math.floor(Math.random() * uppercase.length)],
numbers[Math.floor(Math.random() * numbers.length)],
symbols[Math.floor(Math.random() * symbols.length)],
];
]
// Combine all character sets into one charset
const allCharacters = lowercase + uppercase + numbers + symbols;
const remainingLength = length - mandatoryCharacters.length;
let password = mandatoryCharacters;
const allCharacters = lowercase + uppercase + numbers + symbols
const remainingLength = length - mandatoryCharacters.length
let password = mandatoryCharacters
// Fill the rest of the password with random characters from the combined charset
for (let i = 0; i < remainingLength; i++) {
const randomChar =
allCharacters[Math.floor(Math.random() * allCharacters.length)];
password.push(randomChar);
const randomChar
= allCharacters[Math.floor(Math.random() * allCharacters.length)]
password.push(randomChar)
}
// Shuffle the password to ensure randomness
password = password.sort(() => Math.random() - 0.5);
password = password.sort(() => Math.random() - 0.5)
// Return the password as a string
return password.join("");
return password.join('')
}
// fungsi untuk encoding Base64
function toBase64(str: string) {
return btoa(unescape(encodeURIComponent(str)));
return btoa(unescape(encodeURIComponent(str)))
}
// Set Password
async function setPassword() {
isSubmitting.value = true;
isSubmitting.value = true
if (isEmpty(currentPassword.value)) {
alert("Kata Sandi Lama tidak boleh kosong");
return;
alert('Kata Sandi Lama tidak boleh kosong')
return
}
// Buat objek data dengan password yang sudah di-encoding
const requestData = {
oldPassword: toBase64(currentPassword.value),
newPassword: toBase64(newPassword.value),
};
}
if (
newPassword.value.length < 8 ||
!/[a-z]/.test(newPassword.value) ||
!/\d/.test(newPassword.value) ||
!/[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(newPassword.value)
newPassword.value.length < 8
|| !/[a-z]/.test(newPassword.value)
|| !/\d/.test(newPassword.value)
|| !/[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(newPassword.value)
) {
alert(
"Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus."
);
return;
'Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus.',
)
return
}
if (newPassword.value === currentPassword.value) {
alert("Kata sandi baru tidak boleh sama dengan kata sandi lama.");
return;
alert('Kata sandi baru tidak boleh sama dengan kata sandi lama.')
return
}
if (newPassword.value !== confirmPassword.value) {
alert("Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok.");
return;
alert('Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok.')
return
}
try {
// Panggil API untuk mengganti password
const apiEndpoint = `https://api.ui.ac.id/my/pw`;
const apiEndpoint = 'https://api.ui.ac.id/my/pw'
const response = await fetch(apiEndpoint, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${keycloakStore.accessToken}`,
'Content-Type': 'application/json',
'Authorization': `Bearer ${keycloakStore.accessToken}`,
},
body: JSON.stringify(requestData),
});
})
// Check for HTTP status code 401 which indicates old password is incorrect
if (response.status === 401) {
throw new Error("Kata sandi lama tidak sesuai");
}
if (response.status === 401)
throw new Error('Kata sandi lama tidak sesuai')
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Gagal mengubah password");
const errorData = await response.json()
throw new Error(errorData.message || 'Gagal mengubah password')
}
// Jika berhasil, tampilkan pesan dan reset form
alert(
"Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru."
);
currentPassword.value = "";
newPassword.value = "";
confirmPassword.value = "";
} catch (error) {
'Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru.',
)
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
}
catch (error) {
// Handle error
if (error instanceof Error) {
alert(error.message);
} else {
alert("Terjadi kesalahan saat mengubah kata sandi");
}
} finally {
isSubmitting.value = false;
if (error instanceof Error)
alert(error.message)
else
alert('Terjadi kesalahan saat mengubah kata sandi')
}
finally {
isSubmitting.value = false
}
// Reset input
currentPassword.value = "";
newPassword.value = "";
confirmPassword.value = "";
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
}
</script>
......@@ -186,7 +193,10 @@ async function setPassword() {
<VCardText class="pt-0">
<!-- 👉 Current Password -->
<VRow>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<!-- 👉 current password -->
<VTextField
v-model="currentPassword"
......@@ -197,18 +207,21 @@ async function setPassword() {
"
autocomplete="on"
label="Kata Sandi Lama"
:rules="oldPasswordRules"
clearable
@click:append-inner="
isCurrentPasswordVisible = !isCurrentPasswordVisible
"
:rules="oldPasswordRules"
clearable
/>
</VCol>
</VRow>
<!-- 👉 New Password -->
<VRow>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
......@@ -219,16 +232,19 @@ async function setPassword() {
"
label="Kata Sandi Baru"
autocomplete="on"
@click:append-inner="
isNewPasswordVisible = !isNewPasswordVisible
"
:rules="passwordRules"
:error-messages="newPasswordError"
clearable
@click:append-inner="
isNewPasswordVisible = !isNewPasswordVisible
"
/>
</VCol>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
......@@ -239,11 +255,11 @@ async function setPassword() {
"
autocomplete="on"
label="Konfirmasi Kata Sandi"
:rules="confirmPasswordRules"
clearable
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
:rules="confirmPasswordRules"
clearable
/>
</VCol>
</VRow>
......@@ -276,11 +292,20 @@ async function setPassword() {
<!-- 👉 Action Buttons -->
<div class="d-flex flex-wrap gap-4">
<VBtn @click="setPassword" :disabled="isSubmitting">{{
isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
}}</VBtn>
<VBtn
:disabled="isSubmitting"
@click="setPassword"
>
{{
isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
}}
</VBtn>
<VBtn type="reset" color="secondary" variant="outlined">
<VBtn
type="reset"
color="secondary"
variant="outlined"
>
Reset
</VBtn>
<VBtn
......@@ -291,8 +316,8 @@ async function setPassword() {
confirmPassword = newPassword;
"
>
Generate Kata Sandi</VBtn
>
Generate Kata Sandi
</VBtn>
</div>
</VCardText>
</VForm>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!