Commit 704d29ca by Samuel Taniel Mulyadi

done token

1 parent a364264c
......@@ -14,19 +14,16 @@ const currentTime = ref(new Date())
// Function to check if the current time is within a schedule item range
const { api } = useCivitasApi()
async function getData() {
loading.value = true
schedule.value = []
try {
const response = await fetch('https://api.ui.ac.id/my/ac', {
headers: { Authorization: `Bearer ${keycloakStore.accessToken}` },
})
if (!response.ok)
throw new Error(`Error: ${response.statusText}`)
const { response, error } = await api.user.getRiwayatAkademik()
const data = await response.json()
const data = response.value?.value // ✅ UNWRAP the inner ref
if (keycloakStore.civitas === 'mahasiswa') {
// Get the latest semester
......
<script setup lang="ts">
import { useKeycloakStore } from "@/@core/stores/keycloakStore";
import { ref, computed, onMounted } from "vue";
import { computed, onMounted, ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
// Store Keycloak
const keycloakStore = useKeycloakStore();
const keycloakStore = useKeycloakStore()
// Data dan state
const items = ref<any[]>([]);
const loading = ref(false);
const searchQuery = ref("");
const items = ref<any[]>([])
const loading = ref(false)
const searchQuery = ref('')
// Header tabel
const logHeaders = [
{ title: "KOLEKSI", key: "koleksi" },
{ title: "TANGGAL PEMINJAMAN", key: "tglpinjam" },
{ title: "TANGGAL PENGEMBALIAN", key: "tglkembali" },
{ title: "DIPINJAM", key: "dipinjam" },
{ title: "PERPANJANGAN", key: "perpanjangan" },
{ title: "DENDA", key: "denda" },
{ title: "DENDA TOTAL", key: "dendatotal" },
];
{ title: 'KOLEKSI', key: 'koleksi' },
{ title: 'TANGGAL PEMINJAMAN', key: 'tglpinjam' },
{ title: 'TANGGAL PENGEMBALIAN', key: 'tglkembali' },
{ title: 'DIPINJAM', key: 'dipinjam' },
{ title: 'PERPANJANGAN', key: 'perpanjangan' },
{ title: 'DENDA', key: 'denda' },
{ title: 'DENDA TOTAL', key: 'dendatotal' },
]
const { api } = useCivitasApi()
// Fungsi ambil data
async function getData() {
loading.value = true;
loading.value = true
try {
const apiEndpoint = "https://api.ui.ac.id/my/lib";
const response = await fetch(apiEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) throw new Error("Gagal mengambil data");
const dataku = await response.json();
const { response, error } = await api.user.getLib()
const dataku = response.value?.value // ✅ UNWRAP the inner ref
// dataku adalah array, tiap elemen punya properti root-level
const requiredKeys = [
"denda",
"dendatotal",
"dipinjam",
"koleksi",
"perpanjangan",
"tglkembali",
"tglpinjam",
"username",
];
'denda',
'dendatotal',
'dipinjam',
'koleksi',
'perpanjangan',
'tglkembali',
'tglpinjam',
'username',
]
// ambil hanya objek yang memiliki semua key di root
items.value = Array.isArray(dataku)
? dataku.filter((r: any) => requiredKeys.every(k => k in r))
: [];
} catch (err) {
console.error("Gagal mengambil data:", err);
} finally {
loading.value = false;
: []
}
catch (err) {
console.error('Gagal mengambil data:', err)
}
finally {
loading.value = false
}
}
// Panggil saat komponen mount
onMounted(() => {
getData();
});
getData()
})
// Filter hasil pencarian
const filteredItems = computed(() => {
if (!searchQuery.value) return items.value;
const q = searchQuery.value.toLowerCase();
if (!searchQuery.value)
return items.value
const q = searchQuery.value.toLowerCase()
return items.value.filter(item =>
logHeaders.some(
h => item[h.key] && String(item[h.key]).toLowerCase().includes(q)
)
);
});
h => item[h.key] && String(item[h.key]).toLowerCase().includes(q),
),
)
})
</script>
<template>
<AppCardActions
:title="`Status Peminjaman Buku`"
title="Status Peminjaman Buku"
class="jadwalShift"
action-collapsed
action-remove
......@@ -104,9 +104,7 @@ const filteredItems = computed(() => {
item-value="tglpinjam"
:sort-by="['tglpinjam']"
:sort-asc="[true]"
></VDataTable>
/>
</AppCardActions>
</template>
......@@ -153,4 +151,4 @@ const filteredItems = computed(() => {
display: flex;
justify-content: flex-end;
}
</style>
\ No newline at end of file
</style>
<script lang="ts" setup>
import { useKeycloakStore } from "@core/stores/keycloakStore";
import { useKeycloakStore } from '@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore();
const isAuthenticated = computed(() => keycloakStore.authenticated);
const keycloakStore = useKeycloakStore()
const isAuthenticated = computed(() => keycloakStore.authenticated)
interface ShiftData {
shift_date: string;
shift_start: string;
shift_end: string;
shift: string;
start_time?: string;
end_time?: string;
shift_date: string
shift_start: string
shift_end: string
shift: string
start_time?: string
end_time?: string
}
const shifts = ref<ShiftData[]>([]);
const loading = ref(false);
const error = ref("");
const searchQuery = ref("");
const shifts = ref<ShiftData[]>([])
const loading = ref(false)
const error = ref('')
const searchQuery = ref('')
const headersShift = [
{ title: "Tanggal", key: "shift_date", sortable: true },
{ title: "Nama Hari", key: "day_name", sortable: false },
{ title: "Shift", key: "shift", sortable: false },
{ title: "Jadwal Shift", key: "shift_schedule", sortable: false },
{ title: "Mulai Aktual", key: "start_time", sortable: false },
{ title: "Selesai Aktual", key: "end_time", sortable: false },
{ title: "Status", key: "status", sortable: false },
];
{ title: 'Tanggal', key: 'shift_date', sortable: true },
{ title: 'Nama Hari', key: 'day_name', sortable: false },
{ title: 'Shift', key: 'shift', sortable: false },
{ title: 'Jadwal Shift', key: 'shift_schedule', sortable: false },
{ title: 'Mulai Aktual', key: 'start_time', sortable: false },
{ title: 'Selesai Aktual', key: 'end_time', sortable: false },
{ title: 'Status', key: 'status', sortable: false },
]
const { api } = useCivitasApi()
async function fetchShiftData() {
loading.value = true;
error.value = "";
loading.value = true
error.value = ''
try {
const apiEndpoint = `https://api.ui.ac.id/my/hr/attendance`;
const response = await fetch(apiEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) {
throw new Error("Gagal fetch data");
}
// const apiEndpoint = `https://api.ui.ac.id/my/hr/attendance`;
// const response = await fetch(apiEndpoint, {
// headers: {
// Authorization: `Bearer ${keycloakStore.accessToken}`,
// },
// });
// if (!response.ok) {
// throw new Error("Gagal fetch data");
// }
const data = await response.json();
const { response, error } = await api.user.getAttendance()
const data = response.value?.value // ✅ UNWRAP the inner ref
shifts.value = data
.map((item: any) => ({
...item,
shift_start: `${item.shift_date} ${item.shift_start || "00:00"}`,
shift_end: `${item.shift_date} ${item.shift_end || "00:00"}`,
shift_start: `${item.shift_date} ${item.shift_start || '00:00'}`,
shift_end: `${item.shift_date} ${item.shift_end || '00:00'}`,
}))
.sort((a, b) => new Date(b.shift_date).getTime() - new Date(a.shift_date).getTime());
} catch (err: any) {
error.value = err.message || "Terjadi kesalahan saat mengambil data";
} finally {
loading.value = false;
.sort((a, b) => new Date(b.shift_date).getTime() - new Date(a.shift_date).getTime())
}
catch (err: any) {
error.value = err.message || 'Terjadi kesalahan saat mengambil data'
}
finally {
loading.value = false
}
}
function getDayName(date: string) {
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
const dayIndex = new Date(date).getDay();
return days[dayIndex];
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']
const dayIndex = new Date(date).getDay()
return days[dayIndex]
}
const resolveDayColor = (day: string) => {
if (day === "Sabtu" || day === "Minggu") return "error";
};
if (day === 'Sabtu' || day === 'Minggu')
return 'error'
}
function getHour(time: string | undefined) {
if (!time) return "-";
const parts = time.split(" ");
return parts.length === 2 ? parts[1] : time;
if (!time)
return '-'
const parts = time.split(' ')
return parts.length === 2 ? parts[1] : time
}
function getStatus(start: string | undefined, end: string | undefined) {
if (!start && !end) return "Tidak Ada";
if (!start || !end) return "Belum Hitung";
return "On Time";
if (!start && !end)
return 'Tidak Ada'
if (!start || !end)
return 'Belum Hitung'
return 'On Time'
}
const resolveStatusColor = (status: string) => {
if (status === "On Time") return "success";
if (status === "Belum Hitung") return "primary";
if (status === "Tidak Ada") return "error";
};
if (status === 'On Time')
return 'success'
if (status === 'Belum Hitung')
return 'primary'
if (status === 'Tidak Ada')
return 'error'
}
const filteredShifts = computed(() => {
if (!searchQuery.value) return shifts.value;
if (!searchQuery.value)
return shifts.value
return shifts.value.filter((shift) => {
const dayName = getDayName(shift.shift_date);
const status = getStatus(shift.start_time, shift.end_time);
return shifts.value.filter(shift => {
const dayName = getDayName(shift.shift_date)
const status = getStatus(shift.start_time, shift.end_time)
return (
shift.shift_date.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
dayName.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.shift.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.start_time?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.end_time?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
status.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
});
shift.shift_date.toLowerCase().includes(searchQuery.value.toLowerCase())
|| dayName.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.shift.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.start_time?.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.end_time?.toLowerCase().includes(searchQuery.value.toLowerCase())
|| status.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
})
onMounted(() => {
fetchShiftData();
});
fetchShiftData()
})
</script>
<template>
<AppCardActions
:title="`Log Absen`"
title="Log Absen"
class="jadwalShift"
action-collapsed
action-remove
......
......@@ -38,6 +38,7 @@ function resolveStatusColor(status: string) {
default: return 'default'
}
}
const { api } = useCivitasApi()
async function getData() {
loading.value = true
......@@ -45,15 +46,8 @@ async function getData() {
items.value = []
try {
const res = await fetch('https://api.ui.ac.id/my/ac', {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
if (!res.ok)
throw new Error('Gagal mengambil data')
const raw = await res.json()
const { response, error } = await api.user.getRiwayatAkademik()
const raw = response.value?.value // ✅ UNWRAP the inner ref
items.value = raw.map((item: any, idx: number) => {
const expandedFiltered = Object.values(item.IRS || {})
......
import { defu } from 'defu'
import type { UseFetchOptions } from 'nuxt/app'
import type { Ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import { useAuthFetch } from '@/composables/useAuthFetch'
import keycloakInstance from '@/keycloak'
export const useApi: typeof useFetch = <T>(url: MaybeRefOrGetter<string>, options: UseFetchOptions<T> = {}) => {
const config = useRuntimeConfig()
const accessToken = useCookie('accessToken')
const BASE_URL = 'https://api.ui.ac.id'
const keycloakStore = useKeycloakStore()
const defaults: UseFetchOptions<T> = {
baseURL: config.public.apiBaseUrl,
key: toValue(url),
headers: accessToken.value ? { Authorization: `Bearer ${accessToken.value}` } : {},
}
// --- Interface Definitions ---
interface PasswordPayload {
oldPassword: string
newPassword: string
}
// for nice deep defaults, please use unjs/defu
const params = defu(options, defaults)
interface ApiErrorDetail {
message: string
details?: any
}
return useFetch(url, params)
interface ApiErrorResponse {
success: false
code: number
error: ApiErrorDetail
message?: string
}
// import { defu } from 'defu'
// import { destr } from 'destr'
// import type { UseFetchOptions } from 'nuxt/app'
// export const useApi = <T>(url: MaybeRefOrGetter<string>, options: UseFetchOptions<T> = {}) => {
// const config = useRuntimeConfig()
// const accessToken = useCookie('accessToken')
// const defaults: UseFetchOptions<T> = {
// baseURL: config.public.apiBaseUrl || '/api',
// key: toValue(url),
// headers: {
// Accept: 'application/json',
// ...(accessToken.value ? { Authorization: `Bearer ${accessToken.value}` } : {}),
// },
// onResponse({ response }) {
// // Parse response with destr (like Vite version)
// try {
// response._data = destr(response._data)
// }
// catch (error) {
// console.error('Failed to parse response:', error)
// }
// },
// onRequest({ options }) {
// // Similar to `beforeFetch`
// if (accessToken.value) {
// options.headers = {
// ...options.headers,
// Authorization: `Bearer ${accessToken.value}`,
// }
// }
// },
// }
// // Merge user options with defaults
// const params = defu(options, defaults)
// return useFetch<T>(url, params)
// }
export function useCivitasApi() {
const loading = ref(false)
const error: Ref<string | null> = ref(null)
const errorResponse: Ref<ApiErrorResponse | null> = ref(null)
const fetchWithAuth = async <T = any>(
endpoint: string,
options: Record<string, any> = {},
) => {
loading.value = true
error.value = null
errorResponse.value = null
const url = `${BASE_URL}${
endpoint.startsWith('/') ? endpoint : `/${endpoint}`
}`
const responseData: Ref<T | null> = ref(null)
const errorRef: Ref<any | null> = ref(null)
const currentTime = Math.floor(Date.now() / 1000) // Current time in seconds
const tokenExp = keycloakInstance.tokenParsed?.exp ?? 0 // Token expiration time in seconds
const timeRemaining = tokenExp - currentTime // Time remaining in seconds
if (timeRemaining < 5) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken() // Refresh the token if it's about to expire
}
try {
const { data, error: fetchError } = await useAuthFetch<
T | ApiErrorResponse
>(url, options)
responseData.value = data as Ref<T | null>
errorRef.value = fetchError
// so up until this is work
if (fetchError.value) {
const errorData = fetchError.value.data as ApiErrorResponse
errorResponse.value = errorData
if (errorData?.error?.message) {
error.value = errorData.error.message
}
else if (errorData?.message) {
error.value = errorData.message
}
else {
error.value
= fetchError.value.message || 'An error occurred during fetch'
}
console.error('API call error details:', fetchError.value)
}
}
catch (err: any) {
error.value
= err.message || 'An unexpected network or setup error occurred'
console.error('API call exception:', err)
errorResponse.value = {
success: false,
code: 0,
error: { message: error.value || 'Unknown error' },
}
errorRef.value = { message: error.value }
}
finally {
loading.value = false
}
return {
response: responseData,
error: errorRef,
fetchError: error,
errorResponse,
}
}
// Grouped API endpoints
const api = {
staf: {
getStafData: (stafNIP: number) =>
fetchWithAuth(`/staf/${stafNIP}`),
},
dosen: {
getListDosen: () =>
fetchWithAuth('/listdosen'),
},
user: {
getRiwayatAkademik: () => fetchWithAuth('/my/ac'),
getProfile: () => fetchWithAuth('/me'),
getParkirAktif: () => fetchWithAuth('/my/parkir/aktif'),
getAttendance: () => fetchWithAuth('/my/hr/attendance'),
getLib: () => fetchWithAuth('/my/lib'),
changePassword: (payload: PasswordPayload) =>
fetchWithAuth('/my/pw', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
}),
// getPublicPhoto: (kodeIdentitas: number) =>
// fetchWithAuth(`/public/photo/${kodeIdentitas}.jpg`),
},
}
return {
api,
loading,
error,
errorResponse,
}
}
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
export async function useAuthFetch<T>(url: string, options?: RequestInit): Promise<T> {
const keycloakStore = useKeycloakStore()
import { useFetch } from '#app'
// Ensure token is valid before making the request
if (keycloakStore.isTokenExpired) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken()
}
export function useAuthFetch<T>(url: string, options?: any) {
const keycloakStore = useKeycloakStore()
const response = await fetch(url, {
return useFetch<T>(url, {
...options,
headers: {
...(options?.headers || {}),
'Authorization': `Bearer ${keycloakStore.accessToken}`,
'Content-Type': 'application/json',
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
async beforeRequest({ options }: { options: any }) {
// Cek apakah token hampir expired (less than 5 seconds remaining)
const currentTime = Math.floor(Date.now() / 1000) // Current time in seconds
const tokenExp = keycloakStore.keycloakInstance.tokenParsed?.exp ?? 0 // Token expiration time in seconds
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
const timeRemaining = tokenExp - currentTime // Time remaining in seconds
return response.json() as Promise<T>
if (timeRemaining < 5) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken() // Refresh the token if it's about to expire
}
// Set ulang Authorization header setelah refresh token
options.headers = {
...options.headers,
Authorization: `Bearer ${keycloakStore.accessToken}`,
}
},
})
}
......@@ -7,15 +7,10 @@ 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.refreshTokenParsed.exp - keycloakInstance.refreshTokenParsed.iat
timer = setInterval(() => {
now.value = Math.floor(Date.now() / 1000)
}, 1000)
......@@ -27,11 +22,17 @@ onUnmounted(() => {
const computedExpIn = computed(() => {
return authenticated.value && keycloakInstance.refreshTokenParsed?.exp
? Math.max(keycloakInstance.refreshTokenParsed.exp - now.value, 0)
: 0
? keycloakInstance.refreshTokenParsed.exp - now.value
: null
})
const formattedExpIn = computed(() => {
if (computedExpIn.value === null)
return '--:--'
if (computedExpIn.value < 0)
return 'Expired'
const minutes = Math.floor(computedExpIn.value / 60)
const seconds = computedExpIn.value % 60
......
......@@ -88,6 +88,7 @@ export default defineNuxtConfig({
'@themeConfig': ['../themeConfig.ts'],
'@layouts/*': ['../@layouts/*'],
'@layouts': ['../@layouts'],
'@composables': ['../@composables'],
'@core/*': ['../@core/*'],
'@core': ['../@core'],
'@images/*': ['../assets/images/*'],
......
......@@ -98,16 +98,16 @@ const onSubmit = () => {
}
// Watch perubahan accessToken, panggil ulang useAuthFetch
watchEffect(async () => {
if (!keycloakStore.accessToken)
return // Hindari fetch jika token masih kosong
console.log('Fetching data dengan token baru...')
// watchEffect(async () => {
// if (!keycloakStore.accessToken)
// return // Hindari fetch jika token masih kosong
// console.log('Fetching data dengan token baru...')
const { data: newData, error: newError } = await useAuthFetch('https://api.ui.ac.id/my/ac/st')
// const { data: newData, error: newError } = await useAuthFetch('https://api.ui.ac.id/my/ac/st')
data.value = newData.value || null
error.value = newError.value || null
})
// data.value = newData.value || null
// error.value = newError.value || null
// })
</script>
<template>
......
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import authLoginBg from '/assets/images/naput/bg-blue-yellow.png'
import authLoginLogo from '/assets/images/naput/illust-login.png'
......
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import type { VForm } from 'vuetify/components/VForm'
import authV2MaskDark from '@images/pages/mask-v2-dark.png'
import authV2MaskLight from '@images/pages/mask-v2-light.png'
import { useKeycloakStore } from "@/@core/stores/keycloakStore"
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import authLoginLogo from "/assets/images/naput/illust-login.png"
import authLoginBg from "/assets/images/naput/rektorat-bg.png"
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import authLoginLogo from '/assets/images/naput/illust-login.png'
import authLoginBg from '/assets/images/naput/rektorat-bg.png'
const authThemeImg = authLoginLogo
const authThemeImg = authLoginLogo;
const authThemeBg = authLoginBg
const authThemeBg = authLoginBg;
const keycloakStore = useKeycloakStore();
const data = ref<Record<string, any> | null>(null);
const error = ref<Record<string, any> | null>(null);
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
const { signIn, data: sessionData } = useAuth()
......@@ -51,97 +47,92 @@ const credentials = ref({
})
const rememberMe = ref(false)
</script>
<template>
<div class="container">
<div class="left"></div>
<div class="left" />
<div class="right">
<img src="/assets/images/naput/logo-ui-hitam.png" alt="Logo UI" class="logo" />
<img
src="/assets/images/naput/logo-ui-hitam.png"
alt="Logo UI"
class="logo"
>
<VCol
cols="5"
class="text-center"
>
<AuthProvider/>
cols="5"
class="text-center"
>
<AuthProvider />
</VCol>
</div>
</div>
</template>
<style scoped>
.login-button {
margin-top: 20px;
background-color: #FFD700;
padding: 12px 24px;
font-size: 18px;
border: none;
border-radius: 8px;
background-color: #ffd700;
color: white;
cursor: pointer;
font-size: 18px;
font-weight: bold;
margin-block-start: 20px;
padding-block: 12px;
padding-inline: 24px;
transition: background-color 0.2s;
color: white;
}
.container {
display: flex;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
padding: 0;
margin: 0;
block-size: 100vh;
font-family: Arial, sans-serif;
}
/* Left Side */
.left {
flex: 1;
background: url(/assets/images/naput/rbg.png) no-repeat center center;
background: url("/assets/images/naput/rbg.png") no-repeat center center;
background-size: cover;
}
/* Right Side */
.right {
flex: 1;
background-color: #FDF8E7;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 240px;
justify-content: center;
background-color: #fdf8e7;
gap: 20px; /* Menambah jarak antar elemen */
padding-block-start: 240px;
}
/* Logo */
.logo {
width: 120px;
margin-bottom: 20px;
inline-size: 120px;
margin-block-end: 20px;
}
/* Title */
h1 {
font-size: 24px;
color: #000;
font-size: 24px;
text-align: center;
}
.login-button:hover {
background-color: #E6C200;
background-color: #e6c200;
}
@media (max-width: 768px) {
.container {
justify-content: center;
align-items: center;
background: url(/assets/images/naput/rektorat-ipad-bg.png) no-repeat center center;
justify-content: center;
background: url("/assets/images/naput/rektorat-ipad-bg.png") no-repeat center center;
background-size: cover;
}
......@@ -150,36 +141,33 @@ h1 {
}
.right {
background-color: rgba(253, 248, 231, 0.9);
padding: 40px;
border-radius: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
max-width: 350px;
width: 90%;
height: auto;
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
padding: 40px;
border-radius: 20px;
background-color: rgba(253, 248, 231, 90%);
block-size: auto;
box-shadow: 0 4px 10px rgba(0, 0, 0, 10%);
inline-size: 90%;
max-inline-size: 350px;
}
}
@media (max-width: 480px) {
.container {
background: url(/assets/images/naput/rektorat-iphone-bg.png) no-repeat center center;
background: url("/assets/images/naput/rektorat-iphone-bg.png") no-repeat center center;
background-size: cover;
}
.right {
padding: 30px;
max-width: 320px;
width: 90%;
inline-size: 90%;
max-inline-size: 320px;
}
.logo {
width: 90px;
inline-size: 90px;
}
}
</style>
......@@ -5,7 +5,7 @@ import ImgLight from '@images/dstipro/auth-bg-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import authV2LoginBgDark from '@images/naput/header-bg-dark.png'
import authV2LoginBgLight from '@images/naput/header-bg.png'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
......
<script setup lang="ts">
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
......@@ -7,6 +6,7 @@ import miscMaskDark from '@images/misc/misc-mask-dark.png'
import miscMaskLight from '@images/misc/misc-mask-light.png'
import tree1 from '@images/misc/tree1.png'
import tree3 from '@images/misc/tree3.png'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
......
<script setup lang="ts">
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
......@@ -10,6 +9,7 @@ import authV2LoginIllustrationDark from '@images/pages/auth-v2-login-illustratio
import authV2LoginIllustrationLight from '@images/pages/auth-v2-login-illustration-light.png'
import authV2MaskDark from '@images/pages/mask-v2-dark.png'
import authV2MaskLight from '@images/pages/mask-v2-light.png'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeImg = useGenerateImageVariant(
authV2LoginIllustrationLight,
......
......@@ -8,7 +8,7 @@ export default defineNuxtPlugin(async nuxtApp => {
const authenticated = await keycloakInstance.init({
onLoad: 'check-sso',
checkLoginIframe: true,
checkLoginIframeInterval: 5, // seconds
checkLoginIframeInterval: 5,
})
keycloakStore.authenticated = authenticated
......@@ -17,36 +17,22 @@ export default defineNuxtPlugin(async nuxtApp => {
keycloakStore.refresh()
console.log('User is authenticated')
setInterval(async () => {
setInterval(() => {
const now = Math.floor(Date.now() / 1000)
const accessTokenExp = keycloakInstance.tokenParsed?.exp ?? 0
const refreshTokenExp = keycloakInstance.refreshTokenParsed?.exp ?? 0
if (refreshTokenExp <= now) {
console.warn('Refresh token expired. Logging out...')
await keycloakInstance.logout({ redirectUri: `${window.location.origin}/login` })
return
const tokenExp = keycloakInstance.refreshTokenParsed?.exp ?? 0
const tokenParsed = keycloakInstance.tokenParsed?.exp ?? 0
if (tokenExp <= now) {
console.warn('Token expired. Logging out...')
keycloakInstance.logout({
redirectUri: `${window.location.origin}/`,
})
}
// Refresh the token if the access token is close to expiring (e.g., within 60 seconds)
if (accessTokenExp - now < 60) {
try {
const refreshed = await keycloakInstance.updateToken(60)
if (refreshed) {
console.log('Access token refreshed')
keycloakStore.refresh() // update store data if necessary
}
else {
console.log('Access token still valid, no need to refresh')
}
}
catch (err) {
console.error('Failed to refresh token', err)
await keycloakInstance.logout({ redirectUri: `${window.location.origin}/login` })
}
else {
console.log('Token expires in:', tokenParsed - now, 'seconds')
console.log('Token expires in:', tokenExp - now, 'seconds')
}
}, 10_000) // check every 10 seconds
}, 10_000)
}
else {
console.log('User is not authenticated')
......
<script lang="ts" setup>
import type { ProfileHeader } from '@db/dstipro/profile/types'
import profileImg from '@images/avatars/avatar-1.png'
// import coverImg from '@images/pages/user-profile-header-bg.png'
import coverImgLight from '@images/naput/header-bg.png'
import coverImgDark from '@images/naput/header-bg-dark.png'
import coverImgLight from '@images/naput/header-bg.png'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import PersonAvatar from '@/layouts/components/PersonAvatar.vue'
......@@ -16,7 +17,6 @@ const profileHeaderData = ref<ProfileHeader | null>(null)
const coverImg = useGenerateImageVariant(coverImgLight, coverImgDark)
// const userImg = computed(() => profileHeaderData.profileImg || profileImg)
const keycloakStore = useKeycloakStore()
......@@ -45,44 +45,73 @@ const loading = ref(false)
const errordata = ref('')
const isBirthday = ref(false)
const parkirItems = ref(null)
const { api } = useCivitasApi()
async function getData() {
loading.value = true
errordata.value = ''
try {
const apiEndpoints = [
'https://api.ui.ac.id/me',
'https://api.ui.ac.id/my/parkir/aktif',
]
const responses = await Promise.all(apiEndpoints.map(endpoint =>
fetch(endpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
}),
))
for (const response of responses) {
if (!response.ok)
throw new Error('Gagal mengambil data')
}
const { response: profileResponse, error: profileError } = await api.user.getProfile()
const { response: parkirResponse, error: parkirError } = await api.user.getParkirAktif()
// Unwrap actual values using consistent helper
const profileData = unwrapResponseJson(profileResponse)
const parkirData = unwrapResponseJson(parkirResponse)
const [dataku, parkirData] = await Promise.all(responses.map(res => res.json()))
if (profileData)
items.value = profileData
else
throw new Error('Data profil tidak ditemukan')
items.value = dataku
// Assuming parkirData.data is a reactive array
if (Array.isArray(parkirData?.data) && parkirData.data.length === 0)
parkirItems.value = null // Set to null if the array is empty
else
parkirItems.value = parkirData?.data || null // If not empty, set the data, otherwise null
// parkirItems.value = parkirData.data || {} // Store parkir data separately
parkirItems.value = parkirData.data && Object.keys(parkirData.data).length ? parkirData.data : null
console.log(parkirItems.value) // Check the result
}
catch (err) {
errordata.value = err.message || 'Terjadi kesalahan saat mengambil data'
catch (err: any) {
errordata.value = err?.message || 'Terjadi kesalahan saat mengambil data'
console.error('Gagal mengambil data:', errordata.value)
}
finally {
loading.value = false
}
}
function unwrapResponseJson(refObj: any) {
// Check if the object is a Vue ref and unwrap its value if it is
const isRef = (val: any) => val && typeof val === 'object' && val.__v_isRef
let data = refObj
while (isRef(data))
data = data.value
// Try parsing if the result is a string JSON
if (typeof data === 'string') {
try {
return JSON.parse(data)
}
catch {
throw new Error('Format string JSON tidak valid')
}
}
// If it's an object, we return it after removing any potential circular references
if (typeof data === 'object') {
// If data is an empty object or array, return null
if (Object.keys(data).length === 0)
return null
return JSON.parse(JSON.stringify(data)) // Avoid Vue reactivity here if needed
}
// If data is not valid or recognized, throw an error
throw new Error('Data tidak ditemukan atau format tidak dikenali')
}
const daysLeft = computed(() => {
if (!parkirItems.value)
return 0
......@@ -220,14 +249,13 @@ function openDialog() {
>
<div class="d-flex h-0">
<VAvatar
rounded="circle"
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<PersonAvatar style="block-size: 120px; inline-size: 120px;" />
</VAvatar>
rounded="circle"
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<PersonAvatar style="block-size: 120px; inline-size: 120px;" />
</VAvatar>
</div>
<div class="user-profile-info w-100 mt-16 pt-6 pt-sm-0 mt-sm-0">
......@@ -384,19 +412,23 @@ function openDialog() {
</div>
<!-- tombol test aksi ultah -->
<!-- <div class="d-flex align-center gap-x-2">
<!--
<div class="d-flex align-center gap-x-2">
<VBtn @click="isBirthday = true">
<VIcon
size="24"
icon="ri-hand-heart-line"
/>
<VIcon
size="24"
icon="ri-hand-heart-line"
/>
</VBtn>
</div> -->
</div>
-->
</div>
<!-- <VBtn prepend-icon="ri-user-follow-line">
<!--
<VBtn prepend-icon="ri-user-follow-line">
Connected
</VBtn> -->
</VBtn>
-->
<VBtn
class="bg-error"
......@@ -409,80 +441,82 @@ function openDialog() {
</div>
</VCardText>
<!-- <VCardText
<!--
<VCardText
v-else
class="d-flex align-bottom flex-sm-row flex-column justify-center gap-x-6"
>
>
<div class="d-flex h-0">
<VAvatar
rounded
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<VImg
:src="profileHeaderData.profileImg"
height="120"
width="120"
/>
</VAvatar>
<VAvatar
rounded
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<VImg
:src="profileHeaderData.profileImg"
height="120"
width="120"
/>
</VAvatar>
</div>
<div class="user-profile-info w-100 mt-16 pt-6 pt-sm-0 mt-sm-0">
<h4 class="text-h4 text-center text-sm-start mb-2">
{{ profileHeaderData.fullName }}
</h4>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-6">
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-palette-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.designation }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-map-pin-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.location }}
</div>
</div>
<h4 class="text-h4 text-center text-sm-start mb-2">
{{ profileHeaderData.fullName }}
</h4>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-6">
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-palette-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.designation }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-calendar-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.joiningDate }}
</div>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-map-pin-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.location }}
</div>
</div>
<VBtn
class="bg-error"
prepend-icon="ri-user-follow-line"
>
Not Connected
</VBtn>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-calendar-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.joiningDate }}
</div>
</div>
</div>
<VBtn
class="bg-warning"
prepend-icon="ri-login-box-line"
@click="login"
>
Login
</VBtn>
</div>
<VBtn
class="bg-error"
prepend-icon="ri-user-follow-line"
>
Not Connected
</VBtn>
<VBtn
class="bg-warning"
prepend-icon="ri-login-box-line"
@click="login"
>
Login
</VBtn>
</div>
</div>
</VCardText> -->
</VCardText>
-->
</VCard>
</template>
......
<script setup lang="ts">
import { useTheme } from 'vuetify'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router' // Import Vue Router
import keycloakInstance from '@/keycloak'
const router = useRouter() // Inisialisasi router
function login() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/profile`,
})
}
function loginGoogle() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/profile`, idpHint: 'google',
})
}
// Cek apakah user sudah login saat komponen dimuat
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/profile')
})
const { global } = useTheme()
const authProviders = [
{
icon: 'bxl-facebook',
color: '#4267b2',
colorInDark: '#4267b2',
},
{
icon: 'bxl-twitter',
color: '#1da1f2',
colorInDark: '#1da1f2',
},
{
icon: 'bxl-github',
color: '#272727',
colorInDark: '#fff',
},
{
icon: 'bxl-google',
color: '#db4437',
colorInDark: '#db4437',
},
]
</script>
<template>
<VBtn
v-for="link in authProviders"
:key="link.icon"
:icon="link.icon"
variant="text"
size="small"
:color="global.name.value === 'dark' ? link.colorInDark : link.color"
/>
<VBtn
color="warning"
class="sso-btn font-weight-medium"
color="#3d3d3d"
prepend-icon="ri-key-fill"
block
type="submit"
rounded="xl"
@click="login"
>
<VIcon
start
icon="ri-login-box-line"
/> Login SSO
Single Sign On
</VBtn>
<VBtn
class="google-btn font-weight-medium"
color="#3d3d3d"
prepend-icon="ri-google-fill"
block
type="submit"
rounded="xl"
variant="outlined"
style="margin-block-start: 5px;"
@click="loginGoogle"
>
Sign in with Google
</VBtn>
</template>
<style scoped>
/* Use :deep() correctly to apply styles to the Vuetify button */
.v-btn.sso-btn {
background-color: #535666 !important; /* semi-transparent dark */
transition: background-color 0.3s ease, transform 0.2s ease;
}
.v-btn.sso-btn:hover {
background-color: #3d3e50 !important;
transform: translateY(-2px);
}
/* Google button */
.v-btn.google-btn {
border: 2px solid #a3a3a3;
background-color: rgba(255, 255, 255, 80%) !important;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.v-btn.google-btn:hover {
background-color: rgba(61, 61, 61, 100%);
transform: translateY(-2px);
}
</style>
......@@ -92,6 +92,8 @@ function toBase64(str: string) {
return btoa(unescape(encodeURIComponent(str)))
}
const { api } = useCivitasApi()
// Set Password
async function setPassword() {
isSubmitting.value = true
......@@ -135,16 +137,18 @@ async function setPassword() {
try {
// Panggil API untuk mengganti password
const apiEndpoint = 'https://api.ui.ac.id/my/pw'
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${keycloakStore.accessToken}`,
},
body: JSON.stringify(requestData),
})
// const apiEndpoint = 'https://api.ui.ac.id/my/pw'
// const response = await fetch(apiEndpoint, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${keycloakStore.accessToken}`,
// },
// body: JSON.stringify(requestData),
// })
const { response } = await api.user.changePassword(requestData)
// Check for HTTP status code 401 which indicates old password is incorrect
if (response.status === 401)
......
......@@ -8,7 +8,8 @@ const isAuthenticated = computed(() => keycloakStore.authenticated)
const items = ref<any[]>([])
const expandedRows = ref<any[]>([])
const loading = ref(false)
const error = ref('')
// const error = ref('')
const searchQuery = ref('')
// Headers
......@@ -39,21 +40,23 @@ function resolveStatusColor(status: string) {
}
}
const { api } = useCivitasApi()
async function getData() {
loading.value = true
error.value = ''
items.value = []
try {
const res = await fetch('https://api.ui.ac.id/my/ac', {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
// const response = await fetch('https://api.ui.ac.id/my/ac', {
// headers: {
// Authorization: `Bearer ${keycloakStore.accessToken}`,
// },
// })
const { response, error } = await api.user.getProfile()
if (!res.ok)
throw new Error('Gagal mengambil data')
const raw = await res.json()
const raw = response.value?.value // ✅ UNWRAP the inner ref
items.value = raw.map((item: any, idx: number) => {
const expandedFiltered = Object.values(item.IRS || {})
......@@ -75,7 +78,7 @@ async function getData() {
}).filter(item => item.expanded.length > 0) // Optionally remove parent rows with no expanded items
}
catch (err: any) {
error.value = err.message || 'Terjadi kesalahan saat mengambil data'
err.value = err.message || 'Terjadi kesalahan saat mengambil data'
}
finally {
loading.value = false
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!