Aller au contenu

Plain JavaScript Integration

Ce contenu n’est pas encore disponible dans votre langue.

This guide covers how to integrate Skycloak authentication into plain JavaScript applications without any framework dependencies.

  • Modern web browser with ES6+ support
  • Skycloak cluster with configured realm and client
  • Basic web server to serve your application
  • Understanding of JavaScript promises and async/await

Download and include the Keycloak JavaScript adapter:

<!-- Option 1: CDN -->
<script src="https://cdn.jsdelivr.net/npm/keycloak-js@latest/dist/keycloak.min.js"></script>

<!-- Option 2: Local file -->
<script src="js/keycloak.min.js"></script>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Skycloak JavaScript App</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="app">
      <header>
        <h1>My Application</h1>
        <div id="auth-status">
          <button id="login-btn" style="display: none;">Login</button>
          <div id="user-info" style="display: none;">
            <span id="username"></span>
            <button id="logout-btn">Logout</button>
          </div>
        </div>
      </header>

      <main id="content">
        <div id="loading">Initializing authentication...</div>
        <div id="public-content" style="display: none;">
          <h2>Public Content</h2>
          <p>This content is visible to everyone.</p>
        </div>
        <div id="protected-content" style="display: none;">
          <h2>Protected Content</h2>
          <p>This content requires authentication.</p>
        </div>
      </main>
    </div>

    <script src="js/keycloak.min.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

Create js/app.js:

// Keycloak configuration
const keycloakConfig = {
  url: 'https://your-cluster-id.app.skycloak.io',
  realm: 'your-realm',
  clientId: 'your-javascript-app',
};

// Initialize Keycloak instance
const keycloak = new Keycloak(keycloakConfig);

// Authentication state
let authenticated = false;

// Initialize Keycloak
async function initKeycloak() {
  try {
    authenticated = await keycloak.init({
      onLoad: 'check-sso',
      silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
      checkLoginIframe: false,
      pkceMethod: 'S256', // Enable PKCE for public clients
    });

    if (authenticated) {
      console.log('User is authenticated');
      showAuthenticatedUI();
      setupTokenRefresh();
    } else {
      console.log('User is not authenticated');
      showPublicUI();
    }
  } catch (error) {
    console.error('Failed to initialize Keycloak', error);
    showError('Authentication system unavailable');
  }
}

// Show authenticated UI
function showAuthenticatedUI() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('login-btn').style.display = 'none';
  document.getElementById('user-info').style.display = 'block';
  document.getElementById('username').textContent = keycloak.tokenParsed.preferred_username;
  document.getElementById('public-content').style.display = 'block';
  document.getElementById('protected-content').style.display = 'block';
}

// Show public UI
function showPublicUI() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('login-btn').style.display = 'block';
  document.getElementById('user-info').style.display = 'none';
  document.getElementById('public-content').style.display = 'block';
  document.getElementById('protected-content').style.display = 'none';
}

// Show error message
function showError(message) {
  document.getElementById('loading').textContent = message;
}

// Setup event listeners
document.addEventListener('DOMContentLoaded', () => {
  // Login button
  document.getElementById('login-btn').addEventListener('click', () => {
    keycloak.login();
  });

  // Logout button
  document.getElementById('logout-btn').addEventListener('click', () => {
    keycloak.logout();
  });

  // Initialize Keycloak
  initKeycloak();
});

// Token refresh
function setupTokenRefresh() {
  setInterval(() => {
    keycloak
      .updateToken(30)
      .then((refreshed) => {
        if (refreshed) {
          console.log('Token refreshed');
        }
      })
      .catch(() => {
        console.error('Failed to refresh token');
        keycloak.login();
      });
  }, 60000); // Check every minute
}

Create silent-check-sso.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Silent SSO Check</title>
  </head>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>
// js/auth-manager.js
class AuthManager {
  constructor(config) {
    this.keycloak = new Keycloak(config);
    this.authenticated = false;
    this.userInfo = null;
    this.callbacks = {
      onAuthenticated: [],
      onLogout: [],
      onTokenExpired: [],
      onTokenRefreshed: [],
      onAuthError: [],
    };
  }

  // Initialize authentication
  async init(options = {}) {
    const defaultOptions = {
      onLoad: 'check-sso',
      checkLoginIframe: false,
      pkceMethod: 'S256',
      silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
    };

    try {
      this.authenticated = await this.keycloak.init({ ...defaultOptions, ...options });

      if (this.authenticated) {
        await this.loadUserInfo();
        this.setupTokenManagement();
        this.trigger('onAuthenticated', this.userInfo);
      }

      this.setupEventHandlers();
      return this.authenticated;
    } catch (error) {
      this.trigger('onAuthError', error);
      throw error;
    }
  }

  // Load user information
  async loadUserInfo() {
    try {
      this.userInfo = await this.keycloak.loadUserInfo();
      this.userInfo.roles = this.getRoles();
      this.userInfo.permissions = this.getPermissions();
      return this.userInfo;
    } catch (error) {
      console.error('Failed to load user info:', error);
      throw error;
    }
  }

  // Get user roles
  getRoles() {
    const token = this.keycloak.tokenParsed;
    if (!token) return { realm: [], client: {} };

    return {
      realm: token.realm_access?.roles || [],
      client: token.resource_access || {},
    };
  }

  // Get user permissions
  getPermissions() {
    // Extract permissions from token if available
    const token = this.keycloak.tokenParsed;
    return token?.permissions || [];
  }

  // Check if user has a specific role
  hasRole(role, clientId = null) {
    const roles = this.getRoles();

    if (clientId) {
      const clientRoles = roles.client[clientId]?.roles || [];
      return clientRoles.includes(role);
    }

    return roles.realm.includes(role);
  }

  // Check if user has any of the specified roles
  hasAnyRole(...roles) {
    const userRoles = this.getRoles().realm;
    return roles.some((role) => userRoles.includes(role));
  }

  // Check if user has all specified roles
  hasAllRoles(...roles) {
    const userRoles = this.getRoles().realm;
    return roles.every((role) => userRoles.includes(role));
  }

  // Login
  async login(options = {}) {
    return this.keycloak.login(options);
  }

  // Logout
  async logout(options = {}) {
    return this.keycloak.logout(options);
  }

  // Register
  async register(options = {}) {
    return this.keycloak.register(options);
  }

  // Get access token
  async getToken() {
    if (this.keycloak.isTokenExpired(5)) {
      await this.refreshToken();
    }
    return this.keycloak.token;
  }

  // Refresh token
  async refreshToken() {
    try {
      const refreshed = await this.keycloak.updateToken(5);
      if (refreshed) {
        this.trigger('onTokenRefreshed', this.keycloak.token);
      }
      return refreshed;
    } catch (error) {
      this.trigger('onTokenExpired');
      throw error;
    }
  }

  // Setup token management
  setupTokenManagement() {
    // Auto-refresh tokens
    setInterval(async () => {
      try {
        await this.refreshToken();
      } catch (error) {
        console.error('Token refresh failed:', error);
      }
    }, 30000); // Every 30 seconds

    // Refresh on token expiry
    this.keycloak.onTokenExpired = () => {
      this.trigger('onTokenExpired');
      this.refreshToken().catch(() => {
        this.login();
      });
    };
  }

  // Setup event handlers
  setupEventHandlers() {
    this.keycloak.onAuthSuccess = () => {
      console.log('Auth success');
    };

    this.keycloak.onAuthError = (error) => {
      console.error('Auth error:', error);
      this.trigger('onAuthError', error);
    };

    this.keycloak.onAuthLogout = () => {
      this.authenticated = false;
      this.userInfo = null;
      this.trigger('onLogout');
    };
  }

  // Event handling
  on(event, callback) {
    if (this.callbacks[event]) {
      this.callbacks[event].push(callback);
    }
  }

  trigger(event, data) {
    if (this.callbacks[event]) {
      this.callbacks[event].forEach((callback) => callback(data));
    }
  }

  // Make authenticated API call
  async fetch(url, options = {}) {
    const token = await this.getToken();

    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
      },
    });
  }
}

// Export for use
window.AuthManager = AuthManager;
// js/ui-components.js
class UIComponents {
  constructor(authManager) {
    this.auth = authManager;
  }

  // Create login button
  createLoginButton(options = {}) {
    const button = document.createElement('button');
    button.textContent = options.text || 'Login';
    button.className = options.className || 'btn btn-primary';
    button.onclick = () => this.auth.login(options.loginOptions);
    return button;
  }

  // Create logout button
  createLogoutButton(options = {}) {
    const button = document.createElement('button');
    button.textContent = options.text || 'Logout';
    button.className = options.className || 'btn btn-secondary';
    button.onclick = () => this.auth.logout(options.logoutOptions);
    return button;
  }

  // Create user profile widget
  createUserProfile() {
    const container = document.createElement('div');
    container.className = 'user-profile';

    if (this.auth.authenticated && this.auth.userInfo) {
      container.innerHTML = `
                <div class="user-avatar">
                    ${this.getInitials(this.auth.userInfo.name)}
                </div>
                <div class="user-details">
                    <div class="user-name">${this.auth.userInfo.name}</div>
                    <div class="user-email">${this.auth.userInfo.email}</div>
                </div>
            `;
    }

    return container;
  }

  // Create role-based element
  createRoleBasedElement(roles, element, options = {}) {
    const { requireAll = false, fallback = null } = options;

    const hasAccess = requireAll ? this.auth.hasAllRoles(...roles) : this.auth.hasAnyRole(...roles);

    if (hasAccess) {
      return element;
    } else if (fallback) {
      return fallback;
    } else {
      return document.createComment('Insufficient permissions');
    }
  }

  // Get user initials
  getInitials(name) {
    if (!name) return '?';
    return name
      .split(' ')
      .map((part) => part[0])
      .join('')
      .toUpperCase()
      .substring(0, 2);
  }

  // Show notification
  showNotification(message, type = 'info') {
    const notification = document.createElement('div');
    notification.className = `notification notification-${type}`;
    notification.textContent = message;

    document.body.appendChild(notification);

    setTimeout(() => {
      notification.classList.add('fade-out');
      setTimeout(() => notification.remove(), 300);
    }, 3000);
  }
}
// js/api-client.js
class APIClient {
  constructor(authManager, baseURL) {
    this.auth = authManager;
    this.baseURL = baseURL;
  }

  // Generic request method
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;

    try {
      const response = await this.auth.fetch(url, {
        ...options,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.statusText}`);
      }

      const contentType = response.headers.get('content-type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      }

      return await response.text();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }

  // GET request
  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return this.request(url, { method: 'GET' });
  }

  // POST request
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // PUT request
  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  // DELETE request
  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }

  // PATCH request
  async patch(endpoint, data) {
    return this.request(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }
}
// js/main.js
(async function () {
  // Configuration
  const config = {
    keycloak: {
      url: 'https://your-cluster-id.app.skycloak.io',
      realm: 'your-realm',
      clientId: 'your-javascript-app',
    },
    api: {
      baseURL: 'https://api.example.com',
    },
  };

  // Initialize auth manager
  const authManager = new AuthManager(config.keycloak);
  const ui = new UIComponents(authManager);
  const api = new APIClient(authManager, config.api.baseURL);

  // Setup event handlers
  authManager.on('onAuthenticated', async (userInfo) => {
    console.log('User authenticated:', userInfo);
    await loadUserDashboard();
  });

  authManager.on('onLogout', () => {
    console.log('User logged out');
    showPublicContent();
  });

  authManager.on('onTokenRefreshed', () => {
    console.log('Token refreshed');
  });

  authManager.on('onAuthError', (error) => {
    console.error('Authentication error:', error);
    ui.showNotification('Authentication failed', 'error');
  });

  // Initialize authentication
  try {
    const authenticated = await authManager.init();

    if (authenticated) {
      await loadUserDashboard();
    } else {
      showPublicContent();
    }
  } catch (error) {
    console.error('Failed to initialize auth:', error);
    showErrorPage();
  }

  // Load user dashboard
  async function loadUserDashboard() {
    const container = document.getElementById('app-content');
    container.innerHTML = '';

    // Add user profile
    container.appendChild(ui.createUserProfile());

    // Add logout button
    container.appendChild(ui.createLogoutButton());

    // Load user-specific content
    try {
      const userData = await api.get('/user/profile');
      displayUserData(userData);
    } catch (error) {
      ui.showNotification('Failed to load user data', 'error');
    }

    // Show role-based content
    if (authManager.hasRole('admin')) {
      container.appendChild(createAdminPanel());
    }

    if (authManager.hasAnyRole('editor', 'moderator')) {
      container.appendChild(createContentManagement());
    }
  }

  // Show public content
  function showPublicContent() {
    const container = document.getElementById('app-content');
    container.innerHTML = `
            <h1>Welcome to Our Application</h1>
            <p>Please log in to access your dashboard.</p>
        `;

    container.appendChild(
      ui.createLoginButton({
        text: 'Sign In with Skycloak',
        className: 'btn btn-primary btn-lg',
      })
    );
  }

  // Show error page
  function showErrorPage() {
    const container = document.getElementById('app-content');
    container.innerHTML = `
            <h1>Error</h1>
            <p>Sorry, something went wrong. Please try again later.</p>
        `;
  }

  // Display user data
  function displayUserData(userData) {
    const dataContainer = document.createElement('div');
    dataContainer.className = 'user-data';
    dataContainer.innerHTML = `
            <h2>Your Profile</h2>
            <dl>
                <dt>Name</dt>
                <dd>${userData.name || 'Not set'}</dd>
                <dt>Email</dt>
                <dd>${userData.email}</dd>
                <dt>Member Since</dt>
                <dd>${new Date(userData.createdAt).toLocaleDateString()}</dd>
            </dl>
        `;
    document.getElementById('app-content').appendChild(dataContainer);
  }

  // Create admin panel
  function createAdminPanel() {
    const panel = document.createElement('div');
    panel.className = 'admin-panel';
    panel.innerHTML = `
            <h2>Admin Panel</h2>
            <button onclick="manageUsers()">Manage Users</button>
            <button onclick="viewSystemStats()">System Stats</button>
        `;
    return panel;
  }

  // Create content management
  function createContentManagement() {
    const panel = document.createElement('div');
    panel.className = 'content-management';
    panel.innerHTML = `
            <h2>Content Management</h2>
            <button onclick="createContent()">Create New</button>
            <button onclick="viewContent()">View All</button>
        `;
    return panel;
  }

  // Global functions for demo
  window.manageUsers = async () => {
    try {
      const users = await api.get('/admin/users');
      console.log('Users:', users);
      ui.showNotification('Loaded user list', 'success');
    } catch (error) {
      ui.showNotification('Failed to load users', 'error');
    }
  };

  window.viewSystemStats = async () => {
    try {
      const stats = await api.get('/admin/stats');
      console.log('Stats:', stats);
      ui.showNotification('Loaded system stats', 'success');
    } catch (error) {
      ui.showNotification('Failed to load stats', 'error');
    }
  };

  window.createContent = () => {
    ui.showNotification('Create content feature coming soon', 'info');
  };

  window.viewContent = async () => {
    try {
      const content = await api.get('/content');
      console.log('Content:', content);
      ui.showNotification('Loaded content list', 'success');
    } catch (error) {
      ui.showNotification('Failed to load content', 'error');
    }
  };
})();
// js/spa-router.js
class SPARouter {
  constructor(authManager) {
    this.auth = authManager;
    this.routes = new Map();
    this.currentRoute = null;

    // Listen for popstate events
    window.addEventListener('popstate', () => this.handleRoute());
  }

  // Register a route
  register(path, handler, options = {}) {
    this.routes.set(path, {
      handler,
      requiresAuth: options.requiresAuth || false,
      roles: options.roles || [],
    });
  }

  // Navigate to a route
  navigate(path) {
    window.history.pushState(null, '', path);
    this.handleRoute();
  }

  // Handle current route
  async handleRoute() {
    const path = window.location.pathname;
    const route = this.routes.get(path) || this.routes.get('*');

    if (!route) {
      console.error('No route found for:', path);
      return;
    }

    // Check authentication
    if (route.requiresAuth && !this.auth.authenticated) {
      // Store intended route
      sessionStorage.setItem('redirectPath', path);
      this.auth.login();
      return;
    }

    // Check roles
    if (route.roles.length > 0) {
      const hasAccess = route.roles.some((role) => this.auth.hasRole(role));
      if (!hasAccess) {
        this.navigate('/unauthorized');
        return;
      }
    }

    // Execute route handler
    this.currentRoute = path;
    await route.handler();
  }

  // Get current route
  getCurrentRoute() {
    return this.currentRoute;
  }
}

// Usage example
const router = new SPARouter(authManager);

// Register routes
router.register('/', () => {
  document.getElementById('content').innerHTML = '<h1>Home Page</h1>';
});

router.register(
  '/profile',
  async () => {
    const userData = await api.get('/user/profile');
    document.getElementById('content').innerHTML = `
        <h1>User Profile</h1>
        <p>Name: ${userData.name}</p>
        <p>Email: ${userData.email}</p>
    `;
  },
  { requiresAuth: true }
);

router.register(
  '/admin',
  () => {
    document.getElementById('content').innerHTML = '<h1>Admin Dashboard</h1>';
  },
  { requiresAuth: true, roles: ['admin'] }
);

router.register('/unauthorized', () => {
  document.getElementById('content').innerHTML = '<h1>Access Denied</h1>';
});

router.register('*', () => {
  document.getElementById('content').innerHTML = '<h1>404 - Page Not Found</h1>';
});

// Handle initial route
router.handleRoute();
/* styles.css */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f5f5f5;
}

#app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

header {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-primary:hover {
  background-color: #0056b3;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.btn-secondary:hover {
  background-color: #545b62;
}

.user-profile {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  margin-bottom: 20px;
}

.user-avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background-color: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.notification {
  position: fixed;
  top: 20px;
  right: 20px;
  padding: 15px 20px;
  border-radius: 4px;
  color: white;
  animation: slideIn 0.3s ease-out;
  z-index: 1000;
}

.notification-info {
  background-color: #17a2b8;
}

.notification-success {
  background-color: #28a745;
}

.notification-error {
  background-color: #dc3545;
}

.notification.fade-out {
  animation: fadeOut 0.3s ease-out forwards;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes fadeOut {
  to {
    opacity: 0;
    transform: translateY(-20px);
  }
}

#loading {
  text-align: center;
  padding: 40px;
  color: #666;
}

.admin-panel,
.content-management {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  margin-top: 20px;
}

.admin-panel button,
.content-management button {
  margin-right: 10px;
}
// js/security.js
class SecurityManager {
  constructor() {
    this.setupCSP();
    this.preventClickjacking();
    this.sanitizer = this.createSanitizer();
  }

  // Setup Content Security Policy
  setupCSP() {
    const meta = document.createElement('meta');
    meta.httpEquiv = 'Content-Security-Policy';
    meta.content = `
            default-src 'self';
            script-src 'self' https://cdn.jsdelivr.net;
            style-src 'self' 'unsafe-inline';
            connect-src 'self' https://*.skycloak.io https://api.example.com;
            frame-src 'self' https://*.skycloak.io;
            img-src 'self' data: https:;
        `.trim();
    document.head.appendChild(meta);
  }

  // Prevent clickjacking
  preventClickjacking() {
    if (window.top !== window.self) {
      window.top.location = window.self.location;
    }
  }

  // Create HTML sanitizer
  createSanitizer() {
    const config = {
      allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
      allowedAttributes: {
        a: ['href', 'title', 'target'],
      },
      allowedSchemes: ['http', 'https', 'mailto'],
    };

    return (html) => {
      // Basic sanitization - use DOMPurify in production
      const div = document.createElement('div');
      div.textContent = html;
      return div.innerHTML;
    };
  }

  // Validate URL
  isValidUrl(url) {
    try {
      const parsed = new URL(url);
      return ['http:', 'https:'].includes(parsed.protocol);
    } catch {
      return false;
    }
  }

  // Generate nonce for inline scripts
  generateNonce() {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array));
  }
}
// js/performance.js
class PerformanceOptimizer {
  constructor() {
    this.cache = new Map();
    this.pendingRequests = new Map();
  }

  // Cache API responses
  async cachedFetch(url, options = {}, ttl = 300000) {
    // 5 minutes default
    const cacheKey = `${url}-${JSON.stringify(options)}`;

    // Check cache
    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.data;
    }

    // Check for pending request
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey);
    }

    // Make request
    const promise = fetch(url, options)
      .then((response) => response.json())
      .then((data) => {
        this.cache.set(cacheKey, {
          data,
          timestamp: Date.now(),
        });
        this.pendingRequests.delete(cacheKey);
        return data;
      })
      .catch((error) => {
        this.pendingRequests.delete(cacheKey);
        throw error;
      });

    this.pendingRequests.set(cacheKey, promise);
    return promise;
  }

  // Debounce function calls
  debounce(func, delay) {
    let timeoutId;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
  }

  // Throttle function calls
  throttle(func, limit) {
    let inThrottle;
    return (...args) => {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;
        setTimeout(() => (inThrottle = false), limit);
      }
    };
  }

  // Lazy load images
  lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]');
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.removeAttribute('data-src');
          imageObserver.unobserve(img);
        }
      });
    });

    images.forEach((img) => imageObserver.observe(img));
  }

  // Preload critical resources
  preloadResources() {
    const resources = [
      { href: '/css/critical.css', as: 'style' },
      { href: '/js/keycloak.min.js', as: 'script' },
      {
        href: 'https://your-cluster-id.app.skycloak.io/realms/your-realm/.well-known/openid-configuration',
        as: 'fetch',
        crossOrigin: 'anonymous',
      },
    ];

    resources.forEach((resource) => {
      const link = document.createElement('link');
      link.rel = 'preload';
      Object.assign(link, resource);
      document.head.appendChild(link);
    });
  }
}
// js/troubleshoot.js
class TroubleshootingHelper {
  constructor(authManager) {
    this.auth = authManager;
    this.issues = [];
  }

  // Run diagnostics
  async runDiagnostics() {
    console.log('Running authentication diagnostics...');

    // Check browser compatibility
    this.checkBrowserCompatibility();

    // Check third-party cookies
    await this.checkThirdPartyCookies();

    // Check Keycloak connectivity
    await this.checkKeycloakConnectivity();

    // Check token status
    this.checkTokenStatus();

    // Report results
    this.reportResults();
  }

  checkBrowserCompatibility() {
    const required = ['Promise', 'fetch', 'localStorage', 'sessionStorage'];
    const missing = required.filter((feature) => !(feature in window));

    if (missing.length > 0) {
      this.issues.push({
        type: 'error',
        message: `Browser missing required features: ${missing.join(', ')}`,
      });
    }
  }

  async checkThirdPartyCookies() {
    try {
      // Test if third-party cookies are enabled
      const testKey = 'cookie-test';
      const iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      iframe.src = `${this.auth.keycloak.authServerUrl}/test-cookies.html`;

      document.body.appendChild(iframe);

      // Clean up
      setTimeout(() => iframe.remove(), 1000);
    } catch (error) {
      this.issues.push({
        type: 'warning',
        message: 'Third-party cookies may be disabled',
      });
    }
  }

  async checkKeycloakConnectivity() {
    try {
      const configUrl = `${this.auth.keycloak.authServerUrl}/realms/${this.auth.keycloak.realm}/.well-known/openid-configuration`;
      const response = await fetch(configUrl);

      if (!response.ok) {
        this.issues.push({
          type: 'error',
          message: `Keycloak server returned ${response.status}`,
        });
      }
    } catch (error) {
      this.issues.push({
        type: 'error',
        message: `Cannot connect to Keycloak: ${error.message}`,
      });
    }
  }

  checkTokenStatus() {
    if (this.auth.authenticated) {
      const token = this.auth.keycloak.tokenParsed;
      const now = Math.floor(Date.now() / 1000);
      const expiresIn = token.exp - now;

      if (expiresIn < 300) {
        // Less than 5 minutes
        this.issues.push({
          type: 'warning',
          message: `Token expires in ${Math.floor(expiresIn / 60)} minutes`,
        });
      }
    }
  }

  reportResults() {
    if (this.issues.length === 0) {
      console.log('✅ All diagnostics passed');
    } else {
      console.group('⚠️ Diagnostics found issues:');
      this.issues.forEach((issue) => {
        console[issue.type === 'error' ? 'error' : 'warn'](issue.message);
      });
      console.groupEnd();
    }
  }
}

// Usage
const troubleshooter = new TroubleshootingHelper(authManager);
// Run diagnostics on demand or when issues occur
window.runAuthDiagnostics = () => troubleshooter.runDiagnostics();