636 lines
19 KiB
TypeScript
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,
|
|
};
|
|
};
|