first commit
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import type { ExtendedUser } from "~/types/auth";
|
||||
import { computed, ref, watch, readonly } from "vue";
|
||||
|
||||
export interface UseUserInfoReturnType {
|
||||
// State
|
||||
isAuthenticated: ComputedRef<boolean>;
|
||||
isLoading: ComputedRef<boolean>;
|
||||
user: ComputedRef<ExtendedUser | null>;
|
||||
userRoles: ComputedRef<string[]>;
|
||||
clientRoles: ComputedRef<string[]>;
|
||||
accessToken: ComputedRef<string>;
|
||||
sessionExpires: ComputedRef<string | null>;
|
||||
isIdle: Ref<boolean>;
|
||||
|
||||
// Raw data for debugging
|
||||
data: Ref<any>;
|
||||
status: Ref<string>;
|
||||
|
||||
// Session data from [...].ts
|
||||
idToken: ComputedRef<string>;
|
||||
refreshToken: ComputedRef<string>;
|
||||
accessTokenPayload: ComputedRef<any>;
|
||||
expiresAt: ComputedRef<number | null>;
|
||||
sessionScope: ComputedRef<string>;
|
||||
jwtToken: ComputedRef<string>;
|
||||
|
||||
// Role checking functions
|
||||
hasRole: (role: string) => boolean;
|
||||
hasClientRole: (role: string) => boolean;
|
||||
hasAnyRole: (roles: string[]) => boolean;
|
||||
isAdmin: ComputedRef<boolean>;
|
||||
|
||||
// Session monitoring functions
|
||||
updateActivity: () => void;
|
||||
startSessionMonitoring: () => (() => void) | null;
|
||||
stopSessionMonitoring: () => void;
|
||||
|
||||
// Authentication actions
|
||||
login: (provider?: string) => Promise<void>;
|
||||
logout: (options?: any) => Promise<void>;
|
||||
fullLogout: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
forceRefreshSession: () => Promise<void>;
|
||||
isKeycloakSessionActive: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const useUserInfo = (): UseUserInfoReturnType => {
|
||||
const { data, status, signIn, signOut, getSession } = useAuth();
|
||||
|
||||
// Internal writable refs for manual setting
|
||||
const internalData = ref(data.value);
|
||||
const internalStatus = ref(status.value);
|
||||
|
||||
// Sync internal refs with useAuth data and status
|
||||
watch(
|
||||
() => data.value,
|
||||
(newVal) => {
|
||||
internalData.value = newVal;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => status.value,
|
||||
(newVal) => {
|
||||
internalStatus.value = newVal;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Session monitoring state
|
||||
const lastActivity = ref(Date.now());
|
||||
const isIdle = ref(false);
|
||||
const tabVisible = ref(true);
|
||||
|
||||
// Constants
|
||||
const IDLE_TIMEOUT = 15 * 60 * 1000; // 15 menit
|
||||
const SESSION_CHECK_INTERVAL = 5 * 60 * 1000; // 5 menit
|
||||
const TAB_INACTIVE_THRESHOLD = 10 * 60 * 1000; // 10 menit
|
||||
|
||||
// Basic computed properties
|
||||
const isAuthenticated = computed(
|
||||
() => internalStatus.value === "authenticated"
|
||||
);
|
||||
const isLoading = computed(() => internalStatus.value === "loading");
|
||||
|
||||
// User info from session
|
||||
const user: ComputedRef<ExtendedUser | null> = computed(() => {
|
||||
if (internalData.value?.user) {
|
||||
return internalData.value.user as ExtendedUser;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Session data dari [...].ts callbacks - INILAH DATA YANG AKAN DITAMPILKAN
|
||||
const idToken = computed(() => (internalData.value as any)?.id_token || "");
|
||||
|
||||
const accessToken = computed(
|
||||
() => (internalData.value as any)?.access_token || ""
|
||||
);
|
||||
|
||||
const refreshToken = computed(
|
||||
() => (internalData.value as any)?.refresh_token || ""
|
||||
);
|
||||
|
||||
const accessTokenPayload = computed(
|
||||
() => (internalData.value as any)?.access_token_payload || null
|
||||
);
|
||||
|
||||
const expiresAt = computed(
|
||||
() => (internalData.value as any)?.expires_at || null
|
||||
);
|
||||
|
||||
const sessionScope = computed(() => (internalData.value as any)?.scope || "");
|
||||
|
||||
const jwtToken = computed(() => (internalData.value as any)?.jwt || "");
|
||||
|
||||
// User roles dari session
|
||||
const userRoles = computed(() => {
|
||||
return (internalData.value?.user as any)?.roles || [];
|
||||
});
|
||||
|
||||
const clientRoles = computed(() => {
|
||||
return (internalData.value?.user as any)?.client_roles || [];
|
||||
});
|
||||
|
||||
// Session expires
|
||||
const sessionExpires = computed(() => data.value?.expires || null);
|
||||
|
||||
// Activity tracking functions
|
||||
const updateActivity = () => {
|
||||
if (process.client) {
|
||||
lastActivity.value = Date.now();
|
||||
isIdle.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkIdleStatus = () => {
|
||||
if (!process.client) return;
|
||||
const now = Date.now();
|
||||
const timeSinceLastActivity = now - lastActivity.value;
|
||||
if (
|
||||
timeSinceLastActivity >= IDLE_TIMEOUT &&
|
||||
tabVisible.value &&
|
||||
isAuthenticated.value
|
||||
) {
|
||||
isIdle.value = true;
|
||||
handleIdleTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleIdleTimeout = async () => {
|
||||
if (!process.client) return;
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
await navigateTo("/auth/login?reason=idle&continue=true");
|
||||
} else {
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Idle timeout handling error:", error);
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = async () => {
|
||||
if (!process.client) return;
|
||||
tabVisible.value = !document.hidden;
|
||||
if (!document.hidden) {
|
||||
updateActivity();
|
||||
const inactiveTime = Date.now() - lastActivity.value;
|
||||
if (inactiveTime >= TAB_INACTIVE_THRESHOLD && isAuthenticated.value) {
|
||||
await handleTabReactivation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabReactivation = async () => {
|
||||
if (!process.client) return;
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
await navigateTo("/auth/login?reason=tab_inactive&continue=true");
|
||||
} else {
|
||||
await navigateTo("/auth/login?reason=session_expired");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Tab reactivation error:", error);
|
||||
await navigateTo("/auth/login?reason=error");
|
||||
}
|
||||
};
|
||||
|
||||
const startSessionMonitoring = (): (() => void) | null => {
|
||||
if (!process.client) {
|
||||
console.warn("Session monitoring requires client-side environment");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAuthenticated.value) {
|
||||
console.warn("Session monitoring requires authenticated user");
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = [
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"keydown",
|
||||
"scroll",
|
||||
"touchstart",
|
||||
"click"
|
||||
];
|
||||
const activityHandler = () => updateActivity();
|
||||
|
||||
events.forEach((event) => {
|
||||
document.addEventListener(event, activityHandler, { passive: true });
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
const idleInterval = setInterval(checkIdleStatus, 60 * 1000);
|
||||
|
||||
const sessionInterval = setInterval(async () => {
|
||||
if (isAuthenticated.value) {
|
||||
try {
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
console.error("Session check failed:", error);
|
||||
clearInterval(sessionInterval);
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
}
|
||||
}, SESSION_CHECK_INTERVAL);
|
||||
|
||||
return () => {
|
||||
events.forEach((event) => {
|
||||
document.removeEventListener(event, activityHandler);
|
||||
});
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
clearInterval(idleInterval);
|
||||
clearInterval(sessionInterval);
|
||||
};
|
||||
};
|
||||
|
||||
// **PERBAIKAN: Stop Session Monitoring Function**
|
||||
const stopSessionMonitoring = (): void => {
|
||||
// Implementation bisa ditambahkan jika diperlukan
|
||||
//console.log("Stopping session monitoring...");
|
||||
};
|
||||
|
||||
// Role checking functions
|
||||
const hasRole = (role: string): boolean => {
|
||||
return userRoles.value.includes(role);
|
||||
};
|
||||
|
||||
const hasClientRole = (role: string): boolean => {
|
||||
return clientRoles.value.includes(role);
|
||||
};
|
||||
|
||||
const hasAnyRole = (roles: string[]): boolean => {
|
||||
return roles.some((role) => userRoles.value.includes(role));
|
||||
};
|
||||
|
||||
const isAdmin = computed(
|
||||
() => hasRole("admin") || hasRole("super_admin") || hasRole("realm-admin")
|
||||
);
|
||||
|
||||
// Login function
|
||||
const login = async (provider = "keycloak") => {
|
||||
try {
|
||||
const currentUrl = window.location.pathname;
|
||||
const callbackUrl = currentUrl !== "/auth/login" ? currentUrl : "/";
|
||||
|
||||
const result = await signIn(provider, {
|
||||
callbackUrl,
|
||||
redirect: true
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
// **PERBAIKAN: Update activity setelah login berhasil**
|
||||
updateActivity();
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced Logout function dengan reason support
|
||||
const logout = async (options?: {
|
||||
redirect?: string;
|
||||
clearStorage?: boolean;
|
||||
confirmDialog?: boolean;
|
||||
reason?: "manual" | "expired" | "admin" | "idle";
|
||||
}) => {
|
||||
const defaultOptions = {
|
||||
redirect: "/auth/login",
|
||||
clearStorage: true,
|
||||
confirmDialog: false,
|
||||
reason: "idle" as const,
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
//console.log("Logout called with options:", defaultOptions);
|
||||
|
||||
// Menghilangkan pemanggilan fullLogout manual sesuai permintaan
|
||||
|
||||
if (defaultOptions.clearStorage && process.client) {
|
||||
//console.log("Clearing localStorage and sessionStorage");
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
// Memanggil API logout server untuk menghandle logout Keycloak dan pembersihan session
|
||||
if (process.client) {
|
||||
// Mengambil keycloakIssuer dan redirectUri dari public runtime config
|
||||
const config = useRuntimeConfig();
|
||||
const keycloakIssuer = config.public.keycloakIssuer;
|
||||
const origin = window.location.origin;
|
||||
const redirectUri = `${origin}/auth/login`;
|
||||
|
||||
let logoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}`;
|
||||
if (idToken.value) {
|
||||
logoutUrl += `&id_token_hint=${encodeURIComponent(idToken.value)}`;
|
||||
}
|
||||
|
||||
//console.log("Calling Keycloak logout URL:", logoutUrl);
|
||||
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ id_token_hint: idToken.value })
|
||||
});
|
||||
|
||||
// Bersihkan local storage setelah logout server
|
||||
//console.log("Clearing localStorage and sessionStorage after server logout");
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
await signOut({
|
||||
callbackUrl: `${defaultOptions.redirect}?reason=idle`,
|
||||
redirect: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
throw new Error("Logout failed. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
// Perbaikan fullLogout - simplified dan menggunakan logout yang sudah ada
|
||||
const fullLogout = async () => {
|
||||
try {
|
||||
console.log("Full logout initiated");
|
||||
|
||||
// 1. Stop session monitoring terlebih dahulu
|
||||
stopSessionMonitoring();
|
||||
console.log("Session monitoring stopped");
|
||||
console.log(process);
|
||||
|
||||
if (process.client) {
|
||||
// 2. Clear all storage before logout
|
||||
console.log("Clearing all storage before logout");
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
localStorage.clear();
|
||||
|
||||
// Clear sessionStorage
|
||||
sessionStorage.clear();
|
||||
|
||||
// Log cookies and sessionStorage before clearing
|
||||
console.log("Cookies before clearing:", document.cookie);
|
||||
console.log(
|
||||
"SessionStorage before clearing:",
|
||||
JSON.stringify(sessionStorage)
|
||||
);
|
||||
|
||||
// Clear cookies if any
|
||||
const cookies = document.cookie.split(";");
|
||||
const domain = window.location.hostname;
|
||||
cookies.forEach((c, index) => {
|
||||
const eqPos = c.indexOf("=");
|
||||
const name = eqPos > -1 ? c.substr(0, eqPos).trim() : c.trim();
|
||||
|
||||
console.log(`Clearing cookie [${index}]:`, name);
|
||||
|
||||
// Clear cookie for current path
|
||||
document.cookie =
|
||||
name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||||
|
||||
// Clear cookie for current path and domain
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
|
||||
|
||||
// Clear cookie for current path and domain with leading dot (for subdomains)
|
||||
if (domain.indexOf(".") !== -1) {
|
||||
const domainWithDot = "." + domain;
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
|
||||
}
|
||||
|
||||
// Explicitly clear Keycloak cookies if present
|
||||
const keycloakCookies = [
|
||||
"KEYCLOAK_SESSION",
|
||||
"KEYCLOAK_IDENTITY",
|
||||
"KEYCLOAK_IDENTITY_LEGACY",
|
||||
"KEYCLOAK_REMEMBER_ME",
|
||||
"KEYCLOAK_REMEMBER_ME_LEGACY"
|
||||
];
|
||||
if (keycloakCookies.includes(name)) {
|
||||
console.log(`Explicitly clearing Keycloak cookie: ${name}`);
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
|
||||
if (domain.indexOf(".") !== -1) {
|
||||
const domainWithDot = "." + domain;
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
|
||||
}
|
||||
}
|
||||
console.log("Cleared cookie:", name);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Call server API to get the correct logout URL
|
||||
const response = await $fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id_token_hint: idToken.value
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success && response.logoutUrl && process.client) {
|
||||
console.log("Redirecting to Keycloak logout:", response.logoutUrl);
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Append query parameters to logoutUrl
|
||||
let logoutUrlWithParams = response.logoutUrl;
|
||||
// 4. Redirect directly to Keycloak logout endpoint with appended params
|
||||
window.location.href = logoutUrlWithParams;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to signOut if server API fails
|
||||
|
||||
// Clear local data and update status to logged out
|
||||
if (process.client) {
|
||||
// Clear localStorage and sessionStorage again to be sure
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Reset data and status to reflect logged out state
|
||||
internalData.value = null;
|
||||
internalStatus.value = "unauthenticated";
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("Full logout failed:", error);
|
||||
// Emergency fallback - manual logout to Keycloak
|
||||
if (process.client) {
|
||||
try {
|
||||
console.log("Executing emergency logout");
|
||||
const config = useRuntimeConfig();
|
||||
const keycloakIssuer = config.public.keycloakIssuer;
|
||||
const origin = window.location.origin;
|
||||
const redirectUri = `${origin}/auth/login?reason=emergency`;
|
||||
// Clear all storage again to ensure
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// Build logout URL with client_id as fallback
|
||||
let emergencyLogoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}`;
|
||||
// Add id_token_hint if available
|
||||
if (idToken.value) {
|
||||
emergencyLogoutUrl += `&id_token_hint=${encodeURIComponent(
|
||||
idToken.value
|
||||
)}`;
|
||||
} else {
|
||||
// Use client_id as alternative if id_token_hint is missing
|
||||
const clientId = config.public.keycloakClientId || "your-client-id";
|
||||
emergencyLogoutUrl += `&client_id=${encodeURIComponent(
|
||||
String(clientId)
|
||||
)}`;
|
||||
}
|
||||
console.log("Emergency logout URL:", emergencyLogoutUrl);
|
||||
window.location.href = emergencyLogoutUrl;
|
||||
} catch (emergencyError) {
|
||||
console.error("Emergency logout failed:", emergencyError);
|
||||
// Last resort - force redirect and clear storage
|
||||
if (process.client) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.href = "/auth/login?reason=force";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Force refresh session
|
||||
const forceRefreshSession = async () => {
|
||||
try {
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
console.error("Force refresh session error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// New method to check if Keycloak session is active
|
||||
const isKeycloakSessionActive = async (): Promise<boolean> => {
|
||||
if (!process.client) return false;
|
||||
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking Keycloak session active status:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Add computed property for accessTokenPayload
|
||||
// **PERBAIKAN: Return semua fungsi yang diperlukan termasuk session monitoring**
|
||||
return {
|
||||
// State
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
user,
|
||||
userRoles,
|
||||
clientRoles,
|
||||
accessToken,
|
||||
sessionExpires,
|
||||
isIdle: readonly(isIdle),
|
||||
|
||||
// Raw data untuk debugging
|
||||
data: internalData,
|
||||
status: internalStatus,
|
||||
|
||||
// Session data dari [...].ts
|
||||
idToken,
|
||||
refreshToken,
|
||||
accessTokenPayload,
|
||||
expiresAt,
|
||||
sessionScope,
|
||||
jwtToken,
|
||||
|
||||
// Role checking functions
|
||||
hasRole,
|
||||
hasClientRole,
|
||||
hasAnyRole,
|
||||
isAdmin,
|
||||
|
||||
// Session monitoring functions
|
||||
updateActivity,
|
||||
startSessionMonitoring,
|
||||
stopSessionMonitoring,
|
||||
|
||||
// Authentication actions
|
||||
login,
|
||||
logout,
|
||||
fullLogout,
|
||||
refresh: async () => {
|
||||
await getSession();
|
||||
},
|
||||
forceRefreshSession,
|
||||
isKeycloakSessionActive
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user