Files
template-nuxtsim/composables/useUserInfo.ts

636 lines
19 KiB
TypeScript

import type { ComputedRef, Ref } from "vue";
import type { ExtendedUser } from "~/types/auth";
import { computed, ref, watch, readonly } from "vue";
import { fa } from "vuetify/locale";
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")
);
const config = useRuntimeConfig();
const authConfig = config.public.auth;
//console.log("Auth URL from config:", authConfig);
//console.log("callback URL:", window?.location?.pathname);
// Login function
const login = async (provider = "keycloak") => {
try {
const currentUrl = window.location.pathname;
// const currentUrl = "http://meninjar.dev.rssa.id:3000/";
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
// console.log("prosess:",process.server,process.client);
// console.log("localstorage:",localStorage)
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) {
console.log("masuk sini?");
// 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);
// const response = await fetch("/api/auth/logout", {
// method: "POST",
// headers: {
// "Content-Type": "application/json"
// },
// body: JSON.stringify({ id_token_hint: idToken.value })
// });
// const result = await response.json();
// console.log("Logout API response status:", response);
// 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();
}
// console.log("Redirecting to:", defaultOptions.redirect);
// console.log("process client:", `${defaultOptions.redirect}?reason=idle`);
// ini masih seperti fulllogout karena memanggil signOut
// const router = useRouter()
// router.push(`/auth/login?reason=manual`);
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 responseLogout = await fetch("/api/auth/fullLogOut", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id_token_hint: idToken.value }),
});
const response = await responseLogout.json();
console.log("Logout API response status:", response);
//const response = { success: false, logoutUrl: null }; // Dummy response for illustration
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.message.params;
// 4. Redirect directly to Keycloak logout endpoint with appended params
console.log(
"Final logout URL:",
response.logoutUrl,
logoutUrlWithParams
);
await signOut({
callbackUrl: `${response.logoutUrl}?reason=manual`,
redirect: true,
});
window.location.href = response.logoutUrl;
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,
};
};