-
Name: {{ sessionData.user.name }}
-
Email: {{ sessionData.user.email }}
-
Status: {{ sessionData.status }}
-
Session Expires: {{ sessionExpiresDate }}
-
Created At: {{ sessionCreatedDate }}
+
-
- Token Information (Raw)
-
-
- ID Token (session.idToken)
- {{ sessionData.idToken }}
-
-
- Access Token (session.accessToken)
- {{ sessionData.accessToken }}
-
-
- Refresh Token (session.refreshToken)
- {{ sessionData.refreshToken || 'N/A' }}
-
+
+
+
Authentication Error
+
{{ authError }}
+
+ Go to Login Page
+
-
+
-
- Parsed Token Payloads
-
-
- Access Token Payload
- {{ formatJson(sessionData.accessTokenPayload) }}
-
-
- ID Token Payload
- {{ formatJson(sessionData.idTokenPayload) }}
-
+
+
+
+
+
External API Token Validation
+
+ Test Token Validation
+
+
+
+
+ Test and validate your current access token against the external API endpoint:
+ http://10.10.150.100:8084/api/v1/auth/me
+
+
+
+
+
+ The access token is valid and accepted by the external API.
+
+
+
+ {{ externalValidationError }}
+
+ Details: {{ externalValidationResult.details }}
+
+
+
+
+
+
External API Response:
+
+ {{ formatJson(externalValidationResult.data) }}
+
+
+
-
-
-
- Complete Raw Session Data (Debug)
-
- Full Session Object
- {{ formatJson(sessionData.fullSessionObject) }}
-
-
-
- Session Timeline
-
- -
-
-
Session Created: {{ sessionCreatedDate }}
-
- -
-
-
Current Time: {{ currentDateTime }}
-
- -
-
-
Session Expires: {{ sessionExpiresDate }}
-
-
-
+
+
+
Basic User Information
+
+
Name:
+
{{ sessionData.user.name }}
+
+
Email:
+
{{ sessionData.user.email }}
+
+
Roles:
+
+ {{ sessionData.user.roles || "authenticated" }}
+
+
+
Status:
+
{{ sessionData.status }}
+
+
+
+
+
+
OAuth & Token Metadata
+
+
Subject (User ID):
+
+ {{ sessionData.accessTokenPayload?.sub || "N/A" }}
+
+
+
Issuer:
+
+ {{ sessionData.accessTokenPayload?.iss || "N/A" }}
+
+
+
Audience:
+
+ {{ sessionData.accessTokenPayload?.aud || "N/A" }}
+
+
+
Token Issued At:
+
{{ tokenIssuedAt }}
+
+
Token Expires At:
+
{{ tokenExpiresAt }}
+
+
+
+
+
+
Token Information (after callbacks [โฆ])
+
+
+
+
+
+ {{ sessionData.idToken }}
+
+
+
+
+
+
+
+
+ {{ sessionData.accessToken }}
+
+
+
+
+
+
+
+
+ {{ sessionData.refreshToken }}
+
+
+
+
+
+
+
+
Session Timeline
+
+
Session Created:
+
{{ sessionCreatedDate }}
+
+
Session Expires:
+
{{ sessionExpiresDate }}
+
+
Remaining Time:
+
+ {{ remainingTime }}
+
+
+
Session Scope:
+
+ {{
+ sessionData.accessTokenPayload?.scope || "openid email profile"
+ }}
+
+
+
+
+
+
+
Complete Raw Session Data (Debug)
+
+
+ {{ formatJson(sessionData.fullSessionObject) }}
+
+
+
+ >
+
+
Access Token Payload (Parsed data [โฆ])
+
+
+
+
+ {{ sessionData.accessToken }}
+
+
+
+
+
+ {{ formatJson(sessionData.accessTokenPayload) }}
+
+
+
+
+
+
ID Token Payload (Parsed data [โฆ])
+
+
+
+
+ {{ sessionData.idToken }}
+
+
+
+
+
+ {{ formatJson(sessionData.idTokenPayload) }}
+
+
+
-
\ No newline at end of file
+
diff --git a/server/api/auth/clear-session.post.ts b/server/api/auth/clear-session.post.ts
new file mode 100644
index 0000000..5ce80e1
--- /dev/null
+++ b/server/api/auth/clear-session.post.ts
@@ -0,0 +1,77 @@
+// server/api/auth/clear-session.post.ts
+// Endpoint to forcefully clear session cookies and logout from Keycloak
+export default defineEventHandler(async (event) => {
+ try {
+ const config = useRuntimeConfig();
+ console.log('๐งน Clear session endpoint called');
+
+ // Get the current session to retrieve ID token for Keycloak logout
+ const sessionCookie = getCookie(event, 'user_session');
+ let idToken = null;
+
+ if (sessionCookie) {
+ try {
+ // Try to decode JWT-based session from cookie
+ const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
+ const session = JSON.parse(sessionJson);
+ idToken = session.idToken;
+ console.log('๐ ID token found for Keycloak logout');
+ } catch (error) {
+ console.warn('โ ๏ธ Could not parse session cookie (might be old format)');
+ // Continue anyway to clear cookies
+ }
+ }
+
+ // Clear all auth-related cookies
+ console.log('๐งน Clearing all session cookies...');
+ deleteCookie(event, 'user_session');
+ deleteCookie(event, 'oauth_state');
+
+ // Also clear with different path variations
+ deleteCookie(event, 'user_session', { path: '/' });
+ deleteCookie(event, 'oauth_state', { path: '/' });
+
+ console.log('โ
Local session cleared successfully');
+
+ // Build Keycloak logout URL
+ const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
+ const logoutUrl = new URL(logoutPath);
+
+ const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
+
+ logoutUrl.searchParams.set('client_id', config.keycloakClientId);
+ logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
+
+ // Add ID token hint if available for proper Keycloak session termination
+ if (idToken) {
+ logoutUrl.searchParams.set('id_token_hint', idToken);
+ console.log('๐ Added id_token_hint to Keycloak logout URL');
+ }
+
+ console.log('๐ Keycloak logout URL:', logoutUrl.toString());
+
+ return {
+ success: true,
+ logoutUrl: logoutUrl.toString(),
+ message: 'Session cleared successfully. Redirecting to Keycloak logout...'
+ };
+
+ } catch (error: any) {
+ console.error('โ Clear session error:', error);
+
+ // Even on error, provide a basic logout URL
+ const config = useRuntimeConfig();
+ const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
+ const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
+ const fallbackLogoutUrl = new URL(logoutPath);
+ fallbackLogoutUrl.searchParams.set('client_id', config.keycloakClientId);
+ fallbackLogoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
+
+ return {
+ success: false,
+ logoutUrl: fallbackLogoutUrl.toString(),
+ error: 'Error during session cleanup',
+ message: error.message
+ };
+ }
+});
diff --git a/server/api/auth/keycloak-callback.get.ts b/server/api/auth/keycloak-callback.get.ts
index c9afe11..f43bb94 100644
--- a/server/api/auth/keycloak-callback.get.ts
+++ b/server/api/auth/keycloak-callback.get.ts
@@ -1,242 +1,261 @@
const config = useRuntimeConfig();
// Define session duration (default to 1 hour if not specified in config)
-const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60 * 24;
+const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60 * 24;
// This is the MAIN SESSION duration. It controls how long a user stays logged in.
// Current configuration: 1 hour (3600 seconds).
export default defineEventHandler(async (event) => {
- try {
- const config = useRuntimeConfig();
- const query = getQuery(event);
+ try {
+ const config = useRuntimeConfig();
+ const query = getQuery(event);
- console.log('๐ === KEYCLOAK CALLBACK STARTED ===');
- console.log('๐ Query parameters:', query);
+ console.log("๐ === KEYCLOAK CALLBACK STARTED ===");
+ console.log("๐ Query parameters:", query);
- const code = query.code as string;
- const state = query.state as string;
- const error = query.error as string;
- const storedState = getCookie(event, 'oauth_state');
+ const code = query.code as string;
+ const state = query.state as string;
+ const error = query.error as string;
+ const storedState = getCookie(event, "oauth_state");
- if (error) {
- console.error('โ OAuth error from Keycloak:', error);
- const errorDescription = query.error_description as string;
- console.error('โ Error description:', errorDescription);
+ if (error) {
+ console.error("โ OAuth error from Keycloak:", error);
+ const errorDescription = query.error_description as string;
+ console.error("โ Error description:", errorDescription);
- const errorMsg = encodeURIComponent(`Keycloak error: ${error} - ${errorDescription || 'Please try again'}`);
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- console.log('๐ Code received:', !!code);
- console.log('๐ฒ State from URL:', state);
- console.log('๐ฒ State from cookie:', storedState);
- console.log('๐ฒ State validation:', state === storedState);
-
- if (!state || state !== storedState) {
- console.error('โ Invalid state parameter - possible CSRF attack');
- console.error(' Expected:', storedState);
- console.error(' Received:', state);
-
- const errorMsg = encodeURIComponent('Security validation failed. Please try logging in again.');
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- deleteCookie(event, 'oauth_state');
-
- if (!code) {
- console.error('โ Authorization code not provided');
- const errorMsg = encodeURIComponent('No authorization code received from Keycloak.');
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- // Validate Keycloak configuration
- if (!config.keycloakIssuer) {
- console.error('โ KEYCLOAK_ISSUER is not configured');
- const errorMsg = encodeURIComponent('Keycloak server is not configured. Please contact administrator.');
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- if (!config.keycloakClientId || !config.keycloakClientSecret) {
- console.error('โ Keycloak client credentials are not configured');
- const errorMsg = encodeURIComponent('Keycloak client credentials are missing. Please contact administrator.');
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- const tokenUrl = `${config.keycloakIssuer}/protocol/openid-connect/token`;
- const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`;
-
- console.log('๐ Token URL:', tokenUrl);
- console.log('๐ Redirect URI:', redirectUri);
- console.log('๐ Client ID:', config.keycloakClientId ? '***configured***' : 'MISSING');
-
- const tokenPayload = new URLSearchParams({
- grant_type: 'authorization_code',
- client_id: config.keycloakClientId,
- client_secret: config.keycloakClientSecret,
- code,
- redirect_uri: redirectUri,
- });
-
- let tokenResponse;
- try {
- // Create abort controller for timeout (compatible with all Node.js versions)
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
-
- tokenResponse = await fetch(tokenUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: tokenPayload,
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
- } catch (fetchError: any) {
- console.error('โ Fetch error details:');
- console.error(' - Error type:', fetchError.name);
- console.error(' - Error message:', fetchError.message);
- console.error(' - Token URL attempted:', tokenUrl);
-
- // Provide more specific error messages
- let errorMsg = 'Failed to connect to authentication server.';
- if (fetchError.name === 'AbortError' || fetchError.message.includes('timeout')) {
- errorMsg = 'Authentication server timeout. Please try again.';
- } else if (fetchError.message.includes('ENOTFOUND') || fetchError.message.includes('getaddrinfo')) {
- errorMsg = 'Cannot reach authentication server. Please check network connection.';
- } else if (fetchError.message.includes('ECONNREFUSED')) {
- errorMsg = 'Authentication server refused connection. Server may be down.';
- } else if (fetchError.message.includes('certificate') || fetchError.message.includes('SSL')) {
- errorMsg = 'SSL certificate error. Please contact administrator.';
- }
-
- const encodedError = encodeURIComponent(errorMsg);
- return sendRedirect(event, `/LoginPage?error=${encodedError}`);
- }
-
- if (!tokenResponse.ok) {
- const errorText = await tokenResponse.text();
- console.error('โ Token exchange failed:', errorText);
- const errorMsg = encodeURIComponent(`Token exchange failed: ${tokenResponse.status} - Please check Keycloak configuration`);
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- const tokens = await tokenResponse.json();
-
- let idTokenPayload;
- try {
- idTokenPayload = JSON.parse(
- Buffer.from(tokens.id_token.split('.')[1], 'base64').toString()
- );
- } catch (decodeError) {
- console.error('โ Failed to decode ID token:', decodeError);
- const errorMsg = encodeURIComponent('Invalid ID token format');
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
- }
-
- // Store minimal session data in cookie to reduce size
- // The ID token contains user info, so we can decode it when needed
- const sessionData = {
- // Store only essential user info (can be decoded from ID token if needed)
- user: {
- id: idTokenPayload.sub,
- email: idTokenPayload.email,
- name: idTokenPayload.name || idTokenPayload.preferred_username,
- preferred_username: idTokenPayload.preferred_username,
- },
- // Store tokens - these are necessary for API calls
- // Note: These JWT tokens are large, but necessary for authentication
- accessToken: tokens.access_token,
- idToken: tokens.id_token,
- refreshToken: tokens.refresh_token,
- // Session metadata
- expiresAt: Date.now() + (SESSION_DURATION * 1000),
- createdAt: Date.now(),
- };
-
- // Determine if we should use secure cookies
- // For localhost, always use secure: false
- const isSecure = process.env.NODE_ENV === 'production' &&
- event.node.req.headers['x-forwarded-proto'] === 'https';
-
- console.log('๐ Setting session cookie with secure flag:', isSecure);
- console.log('โฑ๏ธ Session duration:', SESSION_DURATION, 'seconds');
- console.log('๐ Request host:', event.node.req.headers.host);
- console.log('๐ Protocol:', event.node.req.headers['x-forwarded-proto'] || 'http');
-
- // Set cookie with proper settings for localhost
- // For localhost HTTP, we need secure: false and sameSite: 'lax'
- // IMPORTANT: Ensure domain is not set for localhost (allows cookie to work)
- const cookieOptions: any = {
- httpOnly: true,
- secure: isSecure,
- sameSite: 'lax' as const,
- maxAge: SESSION_DURATION,
- path: '/',
- // Explicitly don't set domain for localhost - this is important!
- // Setting domain to 'localhost' can cause cookies to not work
- };
-
- // For localhost, don't set domain (allows cookie to work on localhost)
- // Only set domain in production if needed
- if (process.env.NODE_ENV === 'production' && !event.node.req.headers.host?.includes('localhost')) {
- // Optionally set domain in production
- // cookieOptions.domain = '.yourdomain.com';
- }
-
- // Store session in server-side store and use session ID in cookie
- // This avoids cookie size limits (4KB)
- const { createSession } = await import('~/server/utils/sessionStore');
- const sessionId = createSession(sessionData);
-
- console.log('๐พ Session stored server-side with ID:', sessionId.substring(0, 8) + '...');
- console.log('๐ฆ Session ID cookie size: ~64 bytes (much smaller!)');
-
- // Store only the session ID in the cookie (much smaller)
- setCookie(event, 'user_session', sessionId, cookieOptions);
-
- console.log('โ
Session ID cookie set in response headers (will be available in next request)');
-
- console.log('โ
Session cookie created successfully');
- console.log('๐ช Cookie details:');
- console.log(' - Path: /');
- console.log(' - Secure:', isSecure);
- console.log(' - SameSite: lax');
- console.log(' - HttpOnly: true');
- console.log(' - MaxAge:', SESSION_DURATION, 'seconds');
- console.log(' - Host:', event.node.req.headers.host);
-
- // Auto-sync user data to database (first time login check)
- // Pass session createdAt as loginTime to sync function
- try {
- const { syncUserFromTokens } = await import('~/server/utils/userSync');
- const result = syncUserFromTokens(tokens.id_token, tokens.access_token, sessionData.createdAt);
- console.log(`โ
User auto-sync on login: ${result.action} - ${result.message}`);
- } catch (syncError: any) {
- // Don't fail the login if sync fails, just log it
- console.error('โ ๏ธ Failed to auto-sync user on login:', syncError);
- }
-
- // IMPORTANT: Ensure cookie is set before redirect
- // The cookie should be in the Set-Cookie header of the redirect response
- console.log('โช๏ธ Redirecting to dashboard with cookie in response headers...');
-
- // Note: In H3/Nitro, setCookie automatically adds Set-Cookie header to response
- // The cookie will be available in the browser after the redirect
- // We can't verify it in the same request, but it should be set correctly
-
- // Use sendRedirect - it should include the Set-Cookie header
- // The browser will receive the cookie and include it in the next request
- return sendRedirect(event, '/dashboard?authenticated=true', 302);
-
- } catch (error: any) {
- console.error('โ === CALLBACK ERROR ===');
- console.error('โ Error message:', error.message);
- console.error('โ Error stack:', error.stack);
- console.error('โ ==================');
-
- const errorMsg = encodeURIComponent(`Authentication failed: ${error.message}`);
- return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ const errorMsg = encodeURIComponent(
+ `Keycloak error: ${error} - ${errorDescription || "Please try again"}`,
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
}
-});
\ No newline at end of file
+
+ console.log("๐ Code received:", !!code);
+ console.log("๐ฒ State from URL:", state);
+ console.log("๐ฒ State from cookie:", storedState);
+ console.log("๐ฒ State validation:", state === storedState);
+
+ if (!state || state !== storedState) {
+ console.error("โ Invalid state parameter - possible CSRF attack");
+ console.error(" Expected:", storedState);
+ console.error(" Received:", state);
+
+ const errorMsg = encodeURIComponent(
+ "Security validation failed. Please try logging in again.",
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ deleteCookie(event, "oauth_state");
+
+ if (!code) {
+ console.error("โ Authorization code not provided");
+ const errorMsg = encodeURIComponent(
+ "No authorization code received from Keycloak.",
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ // Validate Keycloak configuration
+ if (!config.keycloakIssuer) {
+ console.error("โ KEYCLOAK_ISSUER is not configured");
+ const errorMsg = encodeURIComponent(
+ "Keycloak server is not configured. Please contact administrator.",
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ if (!config.keycloakClientId || !config.keycloakClientSecret) {
+ console.error("โ Keycloak client credentials are not configured");
+ const errorMsg = encodeURIComponent(
+ "Keycloak client credentials are missing. Please contact administrator.",
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ const tokenUrl = `${config.keycloakIssuer}/protocol/openid-connect/token`;
+ const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`;
+
+ console.log("๐ Token URL:", tokenUrl);
+ console.log("๐ Redirect URI:", redirectUri);
+ console.log(
+ "๐ Client ID:",
+ config.keycloakClientId ? "***configured***" : "MISSING",
+ );
+
+ const tokenPayload = new URLSearchParams({
+ grant_type: "authorization_code",
+ client_id: config.keycloakClientId,
+ client_secret: config.keycloakClientSecret,
+ code,
+ redirect_uri: redirectUri,
+ });
+
+ let tokenResponse;
+ try {
+ // Create abort controller for timeout (compatible with all Node.js versions)
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
+
+ tokenResponse = await fetch(tokenUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: tokenPayload,
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+ } catch (fetchError: any) {
+ console.error("โ Fetch error details:");
+ console.error(" - Error type:", fetchError.name);
+ console.error(" - Error message:", fetchError.message);
+ console.error(" - Token URL attempted:", tokenUrl);
+
+ // Provide more specific error messages
+ let errorMsg = "Failed to connect to authentication server.";
+ if (
+ fetchError.name === "AbortError" ||
+ fetchError.message.includes("timeout")
+ ) {
+ errorMsg = "Authentication server timeout. Please try again.";
+ } else if (
+ fetchError.message.includes("ENOTFOUND") ||
+ fetchError.message.includes("getaddrinfo")
+ ) {
+ errorMsg =
+ "Cannot reach authentication server. Please check network connection.";
+ } else if (fetchError.message.includes("ECONNREFUSED")) {
+ errorMsg =
+ "Authentication server refused connection. Server may be down.";
+ } else if (
+ fetchError.message.includes("certificate") ||
+ fetchError.message.includes("SSL")
+ ) {
+ errorMsg = "SSL certificate error. Please contact administrator.";
+ }
+
+ const encodedError = encodeURIComponent(errorMsg);
+ return sendRedirect(event, `/LoginPage?error=${encodedError}`);
+ }
+
+ if (!tokenResponse.ok) {
+ const errorText = await tokenResponse.text();
+ console.error("โ Token exchange failed:", errorText);
+ const errorMsg = encodeURIComponent(
+ `Token exchange failed: ${tokenResponse.status} - Please check Keycloak configuration`,
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ const tokens = await tokenResponse.json();
+
+ // Parse token payloads for immediate availability
+ let accessTokenPayload;
+ let idTokenPayloadFull;
+ try {
+ accessTokenPayload = JSON.parse(
+ Buffer.from(tokens.access_token.split(".")[1], "base64").toString(),
+ );
+ idTokenPayloadFull = JSON.parse(
+ Buffer.from(tokens.id_token.split(".")[1], "base64").toString(),
+ );
+ } catch (parseError) {
+ console.error("โ Failed to parse token payloads:", parseError);
+ const errorMsg = encodeURIComponent("Invalid token format");
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+
+ // Create session data
+ const sessionData = {
+ user: {
+ id: idTokenPayloadFull.sub,
+ email: idTokenPayloadFull.email,
+ name: idTokenPayloadFull.name || idTokenPayloadFull.preferred_username,
+ preferred_username: idTokenPayloadFull.preferred_username,
+ },
+ accessToken: tokens.access_token,
+ idToken: tokens.id_token,
+ refreshToken: tokens.refresh_token,
+ expiresAt: Date.now() + SESSION_DURATION * 1000,
+ createdAt: Date.now(),
+ scope: accessTokenPayload.scope || "openid email profile",
+ status: "authenticated",
+ };
+
+ // Store session in server-side store and get session ID
+ const { createSession } = await import('~/server/utils/sessionStore');
+ const sessionId = createSession(sessionData);
+
+ // Determine if we should use secure cookies
+ const isSecure =
+ process.env.NODE_ENV === "production" &&
+ event.node.req.headers["x-forwarded-proto"] === "https";
+
+ console.log("๐ Setting session ID cookie");
+ console.log("โฑ๏ธ Session duration:", SESSION_DURATION, "seconds");
+
+ const cookieOptions: any = {
+ httpOnly: true,
+ secure: isSecure,
+ sameSite: "lax" as const,
+ maxAge: SESSION_DURATION,
+ path: "/",
+ };
+
+ // Store only session ID in cookie (small size, ~64 bytes)
+ setCookie(event, "user_session", sessionId, cookieOptions);
+
+ console.log("โ
Session ID cookie created successfully");
+ console.log("๐ช Cookie details:");
+ console.log(" - Name: user_session");
+ console.log(" - Size:", sessionId.length, "bytes");
+ console.log(" - Path: /");
+ console.log(" - Secure:", isSecure);
+ console.log(" - SameSite: lax");
+ console.log(" - HttpOnly: true");
+ console.log(" - MaxAge:", SESSION_DURATION, "seconds");
+ console.log(" - Host:", event.node.req.headers.host);
+
+ // Auto-sync user data to database (first time login check)
+ // Pass session createdAt as loginTime to sync function
+ try {
+ const { syncUserFromTokens } = await import("~/server/utils/userSync");
+ const result = syncUserFromTokens(
+ tokens.id_token,
+ tokens.access_token,
+ sessionData.createdAt,
+ );
+ console.log(
+ `โ
User auto-sync on login: ${result.action} - ${result.message}`,
+ );
+ } catch (syncError: any) {
+ // Don't fail the login if sync fails, just log it
+ console.error("โ ๏ธ Failed to auto-sync user on login:", syncError);
+ }
+
+ // IMPORTANT: Ensure cookie is set before redirect
+ // The cookie should be in the Set-Cookie header of the redirect response
+ console.log(
+ "โช๏ธ Redirecting to dashboard with cookie in response headers...",
+ );
+
+ // Note: In H3/Nitro, setCookie automatically adds Set-Cookie header to response
+ // The cookie will be available in the browser after the redirect
+ // We can't verify it in the same request, but it should be set correctly
+
+ // Use sendRedirect - it should include the Set-Cookie header
+ // The browser will receive the cookie and include it in the next request
+ return sendRedirect(event, "/dashboard?authenticated=true", 302);
+ } catch (error: any) {
+ console.error("โ === CALLBACK ERROR ===");
+ console.error("โ Error message:", error.message);
+ console.error("โ Error stack:", error.stack);
+ console.error("โ ==================");
+
+ const errorMsg = encodeURIComponent(
+ `Authentication failed: ${error.message}`,
+ );
+ return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
+ }
+});
diff --git a/server/api/auth/keycloak-login.ts b/server/api/auth/keycloak-login.ts
index 3bf6661..24a53ad 100644
--- a/server/api/auth/keycloak-login.ts
+++ b/server/api/auth/keycloak-login.ts
@@ -7,14 +7,15 @@ export default defineEventHandler(async (event) => {
// === STALE SESSION CLEANUP ===
// Check for existing session and clean up if invalid/expired
- const existingSessionId = getCookie(event, 'user_session')
+ const existingSessionCookie = getCookie(event, 'user_session')
- if (existingSessionId) {
+ if (existingSessionCookie) {
console.log('๐ Existing session cookie found, validating...')
try {
- const { getSession, deleteSession } = await import('~/server/utils/sessionStore')
- const session = getSession(existingSessionId)
+ // Decode JWT-based session from cookie
+ const sessionJson = Buffer.from(existingSessionCookie, 'base64').toString('utf-8');
+ const session = JSON.parse(sessionJson);
if (session) {
// Check if session is expired
@@ -22,22 +23,15 @@ export default defineEventHandler(async (event) => {
if (isExpired) {
console.log('๐งน Cleaning up expired session...')
- deleteSession(existingSessionId)
deleteCookie(event, 'user_session')
deleteCookie(event, 'oauth_state')
console.log('โ
Expired session cleared')
} else {
console.log('โ ๏ธ Valid session exists, clearing to allow fresh login...')
- deleteSession(existingSessionId)
deleteCookie(event, 'user_session')
deleteCookie(event, 'oauth_state')
console.log('โ
Existing session cleared for fresh login')
}
- } else {
- console.log('๐งน Session cookie exists but no session in store, clearing cookie...')
- deleteCookie(event, 'user_session')
- deleteCookie(event, 'oauth_state')
- console.log('โ
Stale cookie cleared')
}
} catch (error) {
console.warn('โ ๏ธ Error during session cleanup:', error)
diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts
index 8236903..ed8e6b7 100644
--- a/server/api/auth/logout.post.ts
+++ b/server/api/auth/logout.post.ts
@@ -4,22 +4,19 @@ export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
console.log('๐ช Logout handler called');
- // Get the current session to retrieve tokens
- const sessionId = getCookie(event, 'user_session');
+ // Get the current session to retrieve ID token for proper Keycloak logout
+ const sessionCookie = getCookie(event, 'user_session');
let idToken = null;
- if (sessionId) {
+ if (sessionCookie) {
try {
- const { getSession, deleteSession } = await import('~/server/utils/sessionStore');
- const session = getSession(sessionId);
- if (session) {
- idToken = session.idToken;
- console.log('๐ ID token found in session:', !!idToken);
- // Delete session from store
- deleteSession(sessionId);
- }
+ // Decode JWT-based session from cookie
+ const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
+ const session = JSON.parse(sessionJson);
+ idToken = session.idToken;
+ console.log('๐ ID token found in session:', !!idToken);
} catch (error) {
- console.warn('โ ๏ธ Could not retrieve session:', error);
+ console.warn('โ ๏ธ Could not parse session cookie:', error);
}
} else {
console.warn('โ ๏ธ No session cookie found');
diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts
index 19ddb3b..c2b0594 100644
--- a/server/api/auth/session.get.ts
+++ b/server/api/auth/session.get.ts
@@ -1,74 +1,73 @@
// server/api/auth/session.get.ts
import type { SessionResponse } from '~/types/auth'
-// Helper function to safely decode the JWT payload (Access Token or ID Token)
-const decodeTokenPayload = (token: string | undefined): any | null => {
- if (!token) return null;
- try {
- // Tokens are base64 encoded and separated by '.'
- const parts = token.split(".");
- if (parts.length < 2) return null; // Not a valid JWT format
-
- const payloadBase64 = parts[1];
-
- // Decode from base64 and parse the JSON
- // Note: Using Buffer.from is standard in Node.js server environments (like Nitro/H3)
- return JSON.parse(Buffer.from(payloadBase64, "base64").toString());
- } catch (e) {
- console.error("โ Failed to decode token payload:", e);
- return null;
- }
-};
-
-// --- START OF THE SINGLE EXPORT DEFAULT HANDLER ---
export default defineEventHandler(async (event) => {
- console.log("๐ Session endpoint called");
+ console.log('๐ Session endpoint called');
- const sessionId = getCookie(event, "user_session");
- console.log("๐ช Session cookie exists:", !!sessionId);
+ const sessionCookie = getCookie(event, 'user_session');
+ console.log('๐ช Session cookie exists:', !!sessionCookie);
- if (!sessionId) {
- console.log("โ No session cookie found");
+ if (!sessionCookie) {
+ console.log('โ No session cookie found');
throw createError({
statusCode: 401,
- statusMessage: "No session cookie found",
+ statusMessage: 'No session cookie found',
});
}
try {
- // Get session from server-side store using session ID
+ // Get session from store using session ID
const { getSession } = await import('~/server/utils/sessionStore');
- const session = getSession(sessionId);
+ const session = getSession(sessionCookie);
if (!session) {
- console.log("โ Session not found or expired");
- deleteCookie(event, "user_session");
+ console.log('โ Session not found in store or expired');
+ deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
- statusMessage: "Session expired or invalid",
+ statusMessage: 'Session not found or expired',
});
}
- console.log("๐ Session retrieved from store successfully");
+ console.log('๐ Session retrieved from store successfully');
+
+ // Parse token payloads on-demand from tokens
+ let accessTokenPayload = null;
+ let idTokenPayload = null;
+
+ try {
+ if (session.accessToken) {
+ const accessParts = session.accessToken.split('.');
+ if (accessParts.length >= 2) {
+ accessTokenPayload = JSON.parse(Buffer.from(accessParts[1], 'base64').toString());
+ }
+ }
+
+ if (session.idToken) {
+ const idParts = session.idToken.split('.');
+ if (idParts.length >= 2) {
+ idTokenPayload = JSON.parse(Buffer.from(idParts[1], 'base64').toString());
+ }
+ }
+ } catch (parseError) {
+ console.warn('โ ๏ธ Failed to parse token payloads:', parseError);
+ }
+
const isExpired = Date.now() > session.expiresAt;
- console.log(" ย Is Expired:", isExpired);
+ console.log(' Is Expired:', isExpired);
- // Check if the token has expired
+ // Check if the session has expired
if (isExpired) {
- console.log("โฐ Session has expired, clearing cookie");
- deleteCookie(event, "user_session");
+ console.log('โฐ Session has expired, clearing cookie');
+ deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
- statusMessage: "Session expired",
+ statusMessage: 'Session expired',
});
}
- // Decode tokens and prepare the enhanced response data
- const idTokenPayload = decodeTokenPayload(session.idToken);
- const accessTokenPayload = decodeTokenPayload(session.accessToken);
-
- // Final response object - ensure it matches SessionResponse interface
+ // Return the full session data (already includes parsed payloads)
const sessionResponse: SessionResponse & {
idTokenPayload?: any
accessTokenPayload?: any
@@ -76,37 +75,55 @@ export default defineEventHandler(async (event) => {
status?: string
remainingSeconds?: number
idToken?: string
+ scope?: string
+ createdAt?: number
} = {
success: true,
// Basic User Info
user: session.user,
- // Raw Tokens (optional in SessionResponse)
+ // Raw Tokens
accessToken: session.accessToken,
refreshToken: session.refreshToken,
+ idToken: session.idToken,
- // Session Timestamps (optional in SessionResponse)
+ // Session Timestamps
expiresAt: session.expiresAt,
+ createdAt: session.createdAt,
remainingSeconds: Math.max(0, Math.floor((session.expiresAt - Date.now()) / 1000)),
- // Additional debug fields (not in SessionResponse interface)
- idToken: session.idToken,
+ // Parsed token payloads (parsed on-demand, not stored in cookie)
idTokenPayload: idTokenPayload,
accessTokenPayload: accessTokenPayload,
+
+ // Full session for debugging
fullSessionObject: session,
- status: "authenticated",
+
+ // OAuth metadata
+ scope: session.scope,
+ status: session.status || 'authenticated',
};
- console.log("โ
Session is valid, returning full session data");
+ console.log('โ
Session is valid, returning full session data');
return sessionResponse;
- } catch (parseError) {
- console.error("โ Failed to parse session cookie:", parseError);
- // If JSON parsing fails or any other error occurs, the session is invalid
- deleteCookie(event, "user_session");
+ } catch (parseError: any) {
+ console.error('โ Failed to parse session cookie:', parseError);
+
+ // Check if this is an old session ID format (not base64 JSON)
+ if (parseError.message?.includes('Unexpected token') || parseError.message?.includes('JSON')) {
+ console.log('๐งน Detected old session format, clearing cookie...');
+ deleteCookie(event, 'user_session');
+ throw createError({
+ statusCode: 401,
+ statusMessage: 'Old session format detected. Please login again.',
+ });
+ }
+
+ // If parsing fails, the session is invalid
+ deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
- statusMessage: "Invalid session data",
+ statusMessage: 'Invalid session data',
});
}
});
-// --- END OF THE SINGLE EXPORT DEFAULT HANDLER ---
diff --git a/server/api/auth/validate-session.post.ts b/server/api/auth/validate-session.post.ts
index 7b759c1..2e28f4a 100644
--- a/server/api/auth/validate-session.post.ts
+++ b/server/api/auth/validate-session.post.ts
@@ -1,22 +1,23 @@
// server/api/auth/validate-session.post.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
- const sessionId = getCookie(event, 'user_session')
+ const sessionCookie = getCookie(event, 'user_session')
console.log('๐ Session validation endpoint called')
- console.log('๐ช Session cookie exists:', !!sessionId)
+ console.log('๐ช Session cookie exists:', !!sessionCookie)
- if (!sessionId) {
+ if (!sessionCookie) {
console.log('โ No session cookie found')
return { valid: false, reason: 'no_session' }
}
try {
- const { getSession, deleteSession } = await import('~/server/utils/sessionStore')
- const session = getSession(sessionId)
+ // Decode JWT-based session from cookie
+ const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
+ const session = JSON.parse(sessionJson);
if (!session) {
- console.log('โ Session not found in store')
+ console.log('โ Session not found or invalid')
deleteCookie(event, 'user_session')
return { valid: false, reason: 'session_not_found' }
}
@@ -24,7 +25,6 @@ export default defineEventHandler(async (event) => {
// Check local expiry
if (Date.now() > session.expiresAt) {
console.log('โฐ Session has expired locally')
- deleteSession(sessionId)
deleteCookie(event, 'user_session')
return { valid: false, reason: 'session_expired' }
}
@@ -53,7 +53,6 @@ export default defineEventHandler(async (event) => {
// If Keycloak returns 401, the session is invalid on their side
if (keycloakError.status === 401 || keycloakError.statusCode === 401) {
console.log('โ Keycloak session has expired (401)')
- deleteSession(sessionId)
deleteCookie(event, 'user_session')
return { valid: false, reason: 'keycloak_session_expired' }
}
diff --git a/server/api/external/validate-token.post.ts b/server/api/external/validate-token.post.ts
new file mode 100644
index 0000000..700111e
--- /dev/null
+++ b/server/api/external/validate-token.post.ts
@@ -0,0 +1,77 @@
+import { defineEventHandler, createError } from 'h3';
+import { getSessionFromCookie } from '~/server/utils/sessionStore';
+
+export default defineEventHandler(async (event) => {
+ console.log('๐ Proxy: External token validation requested');
+
+ const config = useRuntimeConfig();
+
+ try {
+ // 1. Get current session to retrieve access token
+ const session = await getSessionFromCookie(event);
+
+ if (!session || !session.accessToken) {
+ console.log('โ Proxy: No valid session or access token found');
+ throw createError({
+ statusCode: 401,
+ statusMessage: 'Unauthorized: No valid session found',
+ });
+ }
+
+ const accessToken = session.accessToken;
+
+ // Log token details for debugging audience and claims
+ try {
+ const payloadPart = accessToken.split('.')[1];
+ const payload = JSON.parse(Buffer.from(payloadPart, 'base64').toString());
+ console.log('๐ซ Full Token Payload:', JSON.stringify(payload, null, 2));
+ console.log('๐ซ Token Audience (aud):', payload.aud);
+ } catch (e) {
+ console.warn('โ ๏ธ Proxy: Failed to parse token for logging');
+ }
+
+ const externalApiUrl = `${config.externalApiBaseUrl}/api/v1/auth/me`;
+
+ console.log(`๐ก Proxy: Calling external API: ${externalApiUrl}`);
+
+ // 2. Call external API with Bearer token
+ // We use $fetch from ofetch (auto-imported in Nuxt/Nitro)
+ const response = await $fetch(externalApiUrl, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Accept': 'application/json',
+ },
+ timeout: config.externalApiTimeout || 10000,
+ });
+
+ console.log('โ
Proxy: External API call successful');
+
+ return {
+ success: true,
+ data: response,
+ timestamp: new Date().toISOString()
+ };
+
+ } catch (error: any) {
+ console.error('โ Proxy: External API call failed:', error.message);
+
+ // Check if it's an HTTP error from the external API
+ const statusCode = error.response?.status || 500;
+ const statusText = error.response?.statusText || 'External API Error';
+ const errorData = error.response?._data || error.data || null;
+
+ console.error(` Status: ${statusCode} ${statusText}`);
+ if (errorData) {
+ console.error(' Error Data:', JSON.stringify(errorData));
+ }
+
+ return {
+ success: false,
+ error: statusText,
+ statusCode: statusCode,
+ details: errorData || error.message,
+ timestamp: new Date().toISOString()
+ };
+ }
+});
diff --git a/server/utils/sessionStore.ts b/server/utils/sessionStore.ts
index 72bc92a..a1a3a85 100644
--- a/server/utils/sessionStore.ts
+++ b/server/utils/sessionStore.ts
@@ -1,63 +1,91 @@
// server/utils/sessionStore.ts
-// Simple in-memory session store (for development)
-// In production, use Redis or a database
-
-import { getCookie } from 'h3'
-import { randomBytes } from 'crypto'
+// Lightweight in-memory session store with automatic cleanup
interface SessionData {
- user: any;
- accessToken: string;
- idToken: string;
- refreshToken: string;
- expiresAt: number;
- createdAt: number;
+ user: any;
+ accessToken: string;
+ idToken: string;
+ refreshToken: string;
+ expiresAt: number;
+ createdAt: number;
+ scope?: string;
+ status?: string;
}
+// In-memory session storage
const sessions = new Map();
-// Clean up expired sessions every 5 minutes
+// Cleanup expired sessions every 5 minutes
setInterval(() => {
- const now = Date.now();
- for (const [sessionId, session] of sessions.entries()) {
- if (session.expiresAt < now) {
- sessions.delete(sessionId);
- }
+ const now = Date.now();
+ let cleanedCount = 0;
+
+ for (const [sessionId, session] of sessions.entries()) {
+ if (session.expiresAt < now) {
+ sessions.delete(sessionId);
+ cleanedCount++;
}
-}, 5 * 60 * 1000);
+ }
+
+ if (cleanedCount > 0) {
+ console.log(`๐งน Cleaned up ${cleanedCount} expired sessions. Active sessions: ${sessions.size}`);
+ }
+}, 5 * 60 * 1000); // Every 5 minutes
export function createSession(data: SessionData): string {
- // Generate a secure random session ID
- const sessionId = randomBytes(32).toString('hex');
- sessions.set(sessionId, data);
- return sessionId;
+ // Generate random session ID
+ const sessionId = Array.from({ length: 32 }, () =>
+ Math.floor(Math.random() * 16).toString(16)
+ ).join('');
+
+ sessions.set(sessionId, data);
+ console.log(`โ
Session created: ${sessionId.substring(0, 8)}... (Total active: ${sessions.size})`);
+
+ return sessionId;
}
export function getSession(sessionId: string): SessionData | null {
- const session = sessions.get(sessionId);
- if (!session) {
- return null;
- }
-
- // Check if expired
- if (session.expiresAt < Date.now()) {
- sessions.delete(sessionId);
- return null;
- }
-
- return session;
+ const session = sessions.get(sessionId);
+
+ if (!session) {
+ return null;
+ }
+
+ // Check if expired
+ if (session.expiresAt < Date.now()) {
+ sessions.delete(sessionId);
+ return null;
+ }
+
+ return session;
}
export function deleteSession(sessionId: string): void {
- sessions.delete(sessionId);
+ sessions.delete(sessionId);
+ console.log(`๐๏ธ Session deleted: ${sessionId.substring(0, 8)}... (Remaining: ${sessions.size})`);
}
-// Helper function to get session from cookie (for use in API handlers)
+// Helper function to get session from cookie (for API handlers)
export async function getSessionFromCookie(event: any): Promise {
- const sessionId = getCookie(event, 'user_session');
- if (!sessionId) {
- return null;
- }
- return getSession(sessionId);
+ const { getCookie } = await import('h3');
+ const sessionId = getCookie(event, 'user_session');
+
+ if (!sessionId) {
+ return null;
+ }
+
+ return getSession(sessionId);
}
+// Get session stats
+export function getSessionStats() {
+ return {
+ totalSessions: sessions.size,
+ sessions: Array.from(sessions.entries()).map(([id, session]) => ({
+ id: id.substring(0, 8) + '...',
+ user: session.user?.email || session.user?.name,
+ expiresAt: new Date(session.expiresAt).toISOString(),
+ isExpired: session.expiresAt < Date.now()
+ }))
+ };
+}
diff --git a/stores/queueStore.js b/stores/queueStore.js
index 90bf290..d296dd9 100644
--- a/stores/queueStore.js
+++ b/stores/queueStore.js
@@ -4,6 +4,7 @@ import { ref, computed, watch } from 'vue';
import { useClinicStore } from './clinicStore';
import { usePenunjangStore } from './penunjangStore';
import { useLoketStore } from './loketStore';
+import { useWebSocket } from '@/composables/useWebSocket';
export const useQueueStore = defineStore('queue', () => {
const clinicStore = useClinicStore();
@@ -26,6 +27,77 @@ export const useQueueStore = defineStore('queue', () => {
const lastUpdated = ref(Date.now());
// synchronization guard (moved lower)
+ // ============================================
+ // WEBSOCKET INTEGRATION (CENTRALIZED)
+ // ============================================
+ const wsInstance = ref(null);
+ const isWsConnected = ref(false);
+ const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`);
+
+ /**
+ * Initialize Global WebSocket
+ */
+ const initWebSocket = (customClientId = null) => {
+ if (wsInstance.value && isWsConnected.value) {
+ console.log('๐ [queueStore] WebSocket already connected.');
+ return;
+ }
+
+ if (customClientId) {
+ wsClientId.value = customClientId;
+ }
+
+ const config = useRuntimeConfig();
+ const wsBaseUrl = config.public?.wsBaseUrl || "ws://10.10.150.100:8084/api/v1/ws";
+
+ console.log(`๐ [queueStore] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`);
+
+ wsInstance.value = useWebSocket({
+ url: wsBaseUrl,
+ clientId: wsClientId.value,
+ onOpen: () => {
+ console.log('โ
[queueStore] WebSocket connected');
+ isWsConnected.value = true;
+ },
+ onClose: () => {
+ console.log('โ [queueStore] WebSocket disconnected');
+ isWsConnected.value = false;
+ },
+ onError: (err) => {
+ console.error('โ ๏ธ [queueStore] WebSocket error:', err);
+ isWsConnected.value = false;
+ },
+ onMessage: (data) => {
+ console.log('๐จ [queueStore] Global WS Message:', data);
+
+ // TRIGGER STRATEGIC REFRESHES
+ // 1. Refetch patients for all active lokets in the store
+ Object.keys(apiPatientsPerLoket.value).forEach(loketId => {
+ fetchPatientsForLoket(loketId);
+ });
+
+ // 2. Refetch clinics to update quotas/availability
+ clinicStore.fetchRegulerClinics();
+
+ // 3. Ensure base data is synced
+ ensureInitialData();
+ }
+ });
+
+ wsInstance.value.connect();
+ };
+
+ /**
+ * Disconnect Global WebSocket
+ */
+ const disconnectWebSocket = () => {
+ if (wsInstance.value) {
+ wsInstance.value.disconnect();
+ wsInstance.value = null;
+ isWsConnected.value = false;
+ }
+ };
+
/**
* Sync patient status to apiPatientsPerLoket for reactivity
*/
@@ -2485,6 +2557,9 @@ export const useQueueStore = defineStore('queue', () => {
checkAndResetDaily,
isTodayPatient,
getResetThreshold,
+ initWebSocket,
+ disconnectWebSocket,
+ isWsConnected,
};
}, {