// server/api/users/list.get.ts // Get all users from database and enrich with last access from Keycloak import Database from 'better-sqlite3'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; // Helper to get database path const getDbPath = () => { const dbDir = join(process.cwd(), 'data'); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } return join(dbDir, 'users.db'); }; // Helper to decode JWT token payload const decodeTokenPayload = (token: string | undefined): any | null => { if (!token) return null; try { const parts = token.split("."); if (parts.length < 2) return null; const payloadBase64 = parts[1]; return JSON.parse(Buffer.from(payloadBase64, "base64").toString()); } catch (e) { return null; } }; // Helper to get last access from Keycloak for a user using access token from session const getLastAccessFromKeycloak = async (userId: string, accessToken: string, config: any): Promise => { try { if (!accessToken) { return null; } // Extract realm from issuer const issuerUrl = new URL(config.keycloakIssuer); const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; // Get user sessions from Keycloak Admin API using access token const sessionsUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users/${userId}/sessions`; const sessionsResponse = await fetch(sessionsUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); if (!sessionsResponse.ok) { if (sessionsResponse.status === 404 || sessionsResponse.status === 403) { // User has no sessions or no permission return null; } return null; } const sessions = await sessionsResponse.json() as any[]; if (!sessions || sessions.length === 0) { return null; } // Find the most recent session (highest lastAccess timestamp) let lastAccessTimestamp = 0; sessions.forEach(session => { if (session.lastAccess && session.lastAccess > lastAccessTimestamp) { lastAccessTimestamp = session.lastAccess; } }); // Convert from milliseconds to seconds (Unix timestamp) return lastAccessTimestamp > 0 ? Math.floor(lastAccessTimestamp / 1000) : null; } catch (error: any) { console.warn(`⚠️ Error fetching last access for user ${userId}:`, error.message); return null; } }; // Helper to ensure database schema is up to date const ensureSchema = (db: any) => { try { const tableInfo = db.prepare("PRAGMA table_info(users)").all() as any[]; const columnNames = tableInfo.map(col => col.name); // Add missing columns one by one if (!columnNames.includes('realmRoles')) { db.exec(`ALTER TABLE users ADD COLUMN realmRoles TEXT DEFAULT '[]'`); console.log('✅ Added column: realmRoles'); } if (!columnNames.includes('accountRoles')) { db.exec(`ALTER TABLE users ADD COLUMN accountRoles TEXT DEFAULT '[]'`); console.log('✅ Added column: accountRoles'); } if (!columnNames.includes('resourceRoles')) { db.exec(`ALTER TABLE users ADD COLUMN resourceRoles TEXT DEFAULT '[]'`); console.log('✅ Added column: resourceRoles'); } if (!columnNames.includes('lastLogin')) { db.exec(`ALTER TABLE users ADD COLUMN lastLogin INTEGER`); console.log('✅ Added column: lastLogin'); } if (!columnNames.includes('given_name')) { db.exec(`ALTER TABLE users ADD COLUMN given_name TEXT`); console.log('✅ Added column: given_name'); } if (!columnNames.includes('family_name')) { db.exec(`ALTER TABLE users ADD COLUMN family_name TEXT`); console.log('✅ Added column: family_name'); } } catch (e: any) { console.error('❌ Schema migration error:', e.message); } }; export default defineEventHandler(async (event) => { console.log("📋 Users list endpoint called"); try { const config = useRuntimeConfig(); const dbPath = getDbPath(); // Check if database exists if (!existsSync(dbPath)) { console.log("ℹ️ Database not found, returning empty array"); return []; } // Try to get access token from current user session let accessToken: string | null = null; try { const { getSessionFromCookie } = await import('~/server/utils/sessionStore'); const session = await getSessionFromCookie(event); if (session && session.accessToken) { accessToken = session.accessToken; } } catch (e) { // No session available, will skip Keycloak fetch console.log("ℹ️ No valid session found, will use database values for last access"); } // Open database connection const db = new Database(dbPath); // Ensure schema is up to date before querying ensureSchema(db); // Get all users const users = db.prepare('SELECT * FROM users ORDER BY updatedAt DESC').all() as any[]; // Parse JSON fields and enrich with last access from Keycloak const formattedUsers = await Promise.all(users.map(async (user) => { // Get lastLogin from database (handle null, 0, or undefined) let lastLogin: number | null = null; if (user.lastLogin !== null && user.lastLogin !== undefined && user.lastLogin !== 0) { lastLogin = user.lastLogin; } // Only fetch from Keycloak if we have a valid user ID, access token, and config // And only if we don't have a valid lastLogin in database if (user.id && accessToken && config.keycloakIssuer) { try { const keycloakLastAccess = await getLastAccessFromKeycloak(user.id, accessToken, config); // Use Keycloak last access if available and newer than database value if (keycloakLastAccess) { if (!lastLogin || keycloakLastAccess > lastLogin) { lastLogin = keycloakLastAccess; } } } catch (error) { // Silently fail and use database value console.warn(`⚠️ Could not fetch last access for user ${user.id}, using database value`); } } return { id: user.id, namaLengkap: user.namaLengkap, namaUser: user.namaUser, email: user.email, tipeUser: user.tipeUser || '', lastLogin: lastLogin, // Will be null if never logged in, otherwise timestamp in seconds roles: JSON.parse(user.roles || '[]'), realmRoles: JSON.parse(user.realmRoles || '[]'), accountRoles: JSON.parse(user.accountRoles || '[]'), resourceRoles: JSON.parse(user.resourceRoles || '[]'), groups: JSON.parse(user.groups || '[]'), given_name: user.given_name, family_name: user.family_name, createdAt: user.createdAt, updatedAt: user.updatedAt, }; })); db.close(); console.log(`✅ Retrieved ${formattedUsers.length} users with last access data`); return formattedUsers; } catch (error: any) { console.error("❌ Error fetching users:", error); throw createError({ statusCode: 500, statusMessage: error.message || "Failed to fetch users", }); } });