-
-
-
-
-
-
{{ page.name }}
-
{{ page.url }}
+
+
+
+
+
Menu
+
+
+
+ mdi-plus
+ Tambah Menu
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/scss/_override.scss b/scss/_override.scss
index 8c74e5c..6bc107e 100644
--- a/scss/_override.scss
+++ b/scss/_override.scss
@@ -151,4 +151,9 @@ html {
.v-input__append {
display: none !important;
}
+}
+
+.required::after {
+ content: " *";
+ color: red;
}
\ No newline at end of file
diff --git a/types/setting/menu.ts b/types/setting/menu.ts
new file mode 100644
index 0000000..e87f766
--- /dev/null
+++ b/types/setting/menu.ts
@@ -0,0 +1,19 @@
+
+export interface RolePage {
+ id: number;
+ name: string;
+ icon: string;
+ url: string;
+ level: number;
+ sort: number;
+ active: boolean;
+ children?: RolePage[];
+}
+
+export type ModalMode = 'create' | 'detail' | 'edit';
+
+export type ParentPageOption = {
+ id: number;
+ name: string;
+ level: number;
+};
\ No newline at end of file
diff --git a/utils/api.ts b/utils/api.ts
index 123ef46..436bdf9 100644
--- a/utils/api.ts
+++ b/utils/api.ts
@@ -3,9 +3,160 @@ import axios from 'axios'
const config = useRuntimeConfig()
const api = axios.create({
- baseURL: config.public.baseUrl,
- timeout: 10000,
+ baseURL: config.public.baseUrl
})
+// Flag to prevent multiple simultaneous refresh attempts
+let isRefreshing = false
+let failedQueue: any[] = []
+
+const processQueue = (error: any, token: string | null = null) => {
+ failedQueue.forEach(prom => {
+ if (error) {
+ prom.reject(error)
+ } else {
+ prom.resolve(token)
+ }
+ })
+
+ failedQueue = []
+}
+
+const refreshAccessToken = async (): Promise
=> {
+ try {
+ const refreshToken = localStorage.getItem('refreshToken')
+ if (!refreshToken) {
+ throw new Error('No refresh token available')
+ }
+
+ const response = await fetch(config.public.baseUrl+"/api/v1/auth/refresh", {
+ method: 'POST',
+ body: JSON.stringify({
+ refresh_token: refreshToken,
+ provider: 'keycloak',
+ }),
+ })
+
+ const responseBody = await response.json()
+
+ const data = responseBody.data
+
+ // Update tokens in localStorage
+ if (data.access_token) {
+ localStorage.setItem('accessToken', data.access_token)
+ }
+ if (data.refresh_token) {
+ localStorage.setItem('refreshToken', data.refresh_token)
+ }
+
+ // Update server-side session with new tokens
+ try {
+ await fetch('/api/auth/sessionUserStore', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ accessToken: data.access_token,
+ refreshToken: data.refresh_token,
+ // expiresAt: expiresAt
+ }),
+ })
+ console.log('✅ Server session updated with new tokens')
+ } catch (sessionError) {
+ console.error('⚠️ Failed to update server session:', sessionError)
+ // Don't throw error here - token refresh was successful, session update is secondary
+ }
+
+ console.log('✅ Token refreshed successfully')
+ return data.access_token
+ } catch (error) {
+ console.error('❌ Token refresh failed:', error)
+ return null
+ }
+}
+
+// Shared request interceptor
+const requestInterceptor = (config: any) => {
+ // Only access localStorage on client-side (avoid SSR errors)
+ if (process.client) {
+ const accessToken = localStorage.getItem('accessToken')
+
+ if (accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`
+ }
+ }
+
+ return config
+}
+
+// Shared response interceptor
+const responseInterceptor = (response: any) => response
+const responseErrorInterceptor = async (error: any) => {
+ const originalRequest = error.config
+
+ // centralized error
+ console.error('API Error:', error.response)
+
+ // Handle token expiration on client-side
+ if (process.client && error.response) {
+ const { status } = error.response
+
+ // Check if response is 401 (token expired or unauthorized)
+ if (status === 401 && !originalRequest._retry) {
+ if (isRefreshing) {
+ // If already refreshing, queue this request
+ return new Promise((resolve, reject) => {
+ failedQueue.push({ resolve, reject })
+ })
+ .then(token => {
+ originalRequest.headers['Authorization'] = 'Bearer ' + token
+ return axios(originalRequest)
+ })
+ .catch(err => {
+ return Promise.reject(err)
+ })
+ }
+
+ originalRequest._retry = true
+ isRefreshing = true
+
+ // Try to refresh the token
+ const newToken = await refreshAccessToken()
+
+ if (newToken) {
+ // Token refresh successful, retry original request
+ isRefreshing = false
+ processQueue(null, newToken)
+
+ originalRequest.headers['Authorization'] = 'Bearer ' + newToken
+ return axios(originalRequest)
+ } else {
+ // Token refresh failed (refresh token expired or error), logout user
+ isRefreshing = false
+ processQueue(new Error('Token refresh failed'), null)
+
+ // Clear expired tokens
+ localStorage.removeItem('accessToken')
+ localStorage.removeItem('refreshToken')
+
+ // Call logout endpoint to clear session
+ fetch('/api/auth/logout', { method: 'POST' })
+ .catch(err => console.error('Logout failed:', err))
+
+ // Redirect to login with error message
+ const errorMessage = encodeURIComponent('Token is expired')
+ window.location.href = `/auth/login?error=${errorMessage}`
+
+ return Promise.reject(error)
+ }
+ }
+ }
+
+ return Promise.reject(error)
+}
+
+api.interceptors.request.use(requestInterceptor)
+api.interceptors.response.use(responseInterceptor, responseErrorInterceptor)
export default api
\ No newline at end of file