feat : middleware and refresh token

This commit is contained in:
Yusron alamsyah
2026-04-06 13:55:31 +07:00
parent d438fb0f5f
commit 4325bae76f
16 changed files with 1127 additions and 476 deletions
+63 -63
View File
@@ -8,17 +8,18 @@
<p class="text-body-1 text-grey mb-7">
Silakan masuk dengan akun yang telah terdaftar
</p>
<!-- Error Message -->
<v-alert v-if="errorMessage" type="error" closable class="mb-4" @click:close="errorMessage = ''">
{{ errorMessage }}
</v-alert>
<!-- Error Message -->
<v-alert v-if="errorMessage" type="error" closable class="mb-4" @click:close="errorMessage = ''">
{{ errorMessage }}
</v-alert>
<!-- Success Message -->
<v-alert v-if="successMessage" type="success" class="mb-4">
{{ successMessage }}
</v-alert>
<!-- Success Message -->
<v-alert v-if="successMessage" type="success" class="mb-4">
{{ successMessage }}
</v-alert>
<v-btn :loading="isLoading" :disabled="isLoading" block color="primary" size="large" rounded="pill" class="text-none mb-7 btn-login" @click="loginWithKeycloak">
<v-btn :loading="isLoadingKeycloak" :disabled="isLoadingKeycloak" block color="primary" size="large" rounded="pill"
class="text-none mb-7 btn-login" @click="loginWithKeycloak">
<v-icon size="16" class="mr-2">mdi-key-variant</v-icon>
Masuk dengan Keycloak
</v-btn>
@@ -30,41 +31,19 @@
</div>
<v-form v-model="valid" @submit.prevent="login">
<label class="text-subtitle-2 text-grey-darken-1 mb-2 d-inline-block">Email </label>
<v-text-field
v-model="email"
placeholder="Masukkan email"
density="comfortable"
variant="outlined"
rounded="pill"
class="mb-5"
:rules="emailRules"
/>
<label for="email" class="text-subtitle-2 text-grey-darken-1 mb-2 d-inline-block">Email </label>
<v-text-field v-model="email" id="email" placeholder="Masukkan email" density="comfortable" variant="outlined"
rounded="pill" class="mb-5" :rules="[required, emailRules]" />
<div class="d-flex align-center justify-space-between mb-2">
<label class="text-subtitle-2 text-grey-darken-1">Password</label>
<label for="password" class="text-subtitle-2 text-grey-darken-1">Password</label>
</div>
<v-text-field
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="Masukkan password"
density="comfortable"
variant="outlined"
rounded="pill"
class="mb-6"
:rules="passwordRules"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showPassword = !showPassword"
/>
<v-btn
block
type="submit"
color="primary"
size="large"
rounded="pill"
variant="tonal"
class="text-none mb-8 btn-login"
>
<v-text-field v-model="password" id="password" :type="showPassword ? 'text' : 'password'"
placeholder="Masukkan password" density="comfortable" variant="outlined" rounded="pill" class="mb-6"
:rules="passwordRules" :append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showPassword = !showPassword" />
<v-btn block type="submit" color="primary" size="large" rounded="pill" variant="tonal"
class="text-none mb-8 btn-login" :loading="isLoadingKeycloak" :disabled="isLoadingKeycloak">
Masuk
</v-btn>
</v-form>
@@ -76,38 +55,57 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ref, onMounted } from "vue";
import Logoimg from "/images/logos/logo-farmasi.svg";
import { useValidation } from '~/composables/useValidation';
import api from '~/utils/api';
import { useSnackbarStore } from '~/store/snackbar';
interface LoginResponse {
data : {
provider: string;
access_token: string;
refresh_token: string;
expires_in: number;
token_type: string;
}
}
import type { LoginResponse } from '~/types/auth';
import { useAuth } from '~/composables/useAuth';
const snackbarStore = useSnackbarStore();
const { required } = useValidation();
const { required, emailRules } = useValidation();
const { fetchUserSession, sessionData } = useAuth();
const email = ref("");
const password = ref("");
const valid = ref(false);
const showPassword = ref(false);
const isLoadingKeycloak = ref(false);
const isLoading = ref(false);
const errorMessage = ref("");
const successMessage = ref("");
const emailRules = [
required,
(v : string) => /.+@.+\..+/.test(v) || 'Email tidak valid',
];
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search);
const authStatus = urlParams.get('auth');
if (authStatus === 'success') {
isLoadingKeycloak.value = true;
successMessage.value = 'Authentication successful. Redirecting to dashboard...';
}
const errorParam = urlParams.get('error');
if (errorParam) {
errorMessage.value = decodeURIComponent(errorParam);
}
await fetchUserSession();
if (sessionData.value?.accessToken && sessionData.value?.refreshToken) {
localStorage.setItem('accessToken', sessionData.value.accessToken);
localStorage.setItem('refreshToken', sessionData.value.refreshToken);
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
isLoadingKeycloak.value = false;
window.location.href = "/apps/dashboard";
}
});
const passwordRules = [required];
const login = async () => {
@@ -125,7 +123,6 @@ const login = async () => {
});
const loginData = response.data.data;
console.log('Login response:', loginData);
if (!loginData?.access_token || !loginData?.refresh_token) {
throw new Error('Token login tidak valid');
@@ -139,7 +136,10 @@ const login = async () => {
},
});
localStorage.setItem('accessToken', loginData.access_token);
localStorage.setItem('refreshToken', loginData.refresh_token);
window.location.href = "/apps/dashboard";
} catch (error: any) {
const loginError = error?.response?.data?.message || error?.message || 'Terjadi kesalahan saat login';
@@ -150,12 +150,12 @@ const login = async () => {
};
const loginWithKeycloak = async (): Promise<void> => {
isLoading.value = true;
isLoadingKeycloak.value = true;
errorMessage.value = '';
successMessage.value = '';
try {
// Call API route to initiate Keycloak login process
const response = await $fetch<any>('/api/auth/keycloak-login', {
method: 'POST'
@@ -163,7 +163,7 @@ const loginWithKeycloak = async (): Promise<void> => {
if (response?.success && response?.data?.authUrl) {
successMessage.value = 'Redirecting to Keycloak...';
// Redirect the user to the Keycloak authorization URL
setTimeout(() => {
window.location.href = response.data!.authUrl;
@@ -178,7 +178,7 @@ const loginWithKeycloak = async (): Promise<void> => {
} finally {
// Only set isLoading back to false if no redirect is happening
if (!successMessage.value.includes('Redirecting')) {
isLoading.value = false;
isLoadingKeycloak.value = false;
}
}
};