// server/api/users/sync-all.post.ts // Sync all users from Keycloak Admin API to database // This endpoint will fetch all users from Keycloak and sync them to the database 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 const getLastAccessFromKeycloak = async (userId: string, accessToken: string, config: any): Promise => { try { if (!accessToken) { return null; } const issuerUrl = new URL(config.keycloakIssuer); const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; 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) { return null; } const sessions = await sessionsResponse.json() as any[]; if (!sessions || sessions.length === 0) { return null; } let lastAccessTimestamp = 0; sessions.forEach(session => { if (session.lastAccess && session.lastAccess > lastAccessTimestamp) { lastAccessTimestamp = session.lastAccess; } }); 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 get user from Keycloak Admin API const getUserFromKeycloak = async (userId: string, accessToken: string, config: any): Promise => { try { if (!accessToken) { return null; } const issuerUrl = new URL(config.keycloakIssuer); const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; const userUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users/${userId}`; const userResponse = await fetch(userUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); if (!userResponse.ok) { return null; } return await userResponse.json(); } catch (error: any) { console.warn(`⚠️ Error fetching user ${userId} from Keycloak:`, error.message); return null; } }; // Helper to get all users from Keycloak Admin API const getAllUsersFromKeycloak = async (accessToken: string, config: any): Promise => { try { if (!accessToken) { return []; } const issuerUrl = new URL(config.keycloakIssuer); const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; const usersUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users?max=1000`; const usersResponse = await fetch(usersUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); if (!usersResponse.ok) { console.warn('⚠️ Failed to fetch users from Keycloak:', usersResponse.status); return []; } return await usersResponse.json(); } catch (error: any) { console.warn(`⚠️ Error fetching all users from Keycloak:`, error.message); return []; } }; // Initialize database const initDb = () => { const dbPath = getDbPath(); const db = new Database(dbPath); // Create table if not exists db.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, namaLengkap TEXT NOT NULL, namaUser TEXT UNIQUE NOT NULL, email TEXT, tipeUser TEXT DEFAULT '', lastLogin INTEGER, roles TEXT DEFAULT '[]', realmRoles TEXT DEFAULT '[]', accountRoles TEXT DEFAULT '[]', resourceRoles TEXT DEFAULT '[]', groups TEXT DEFAULT '[]', given_name TEXT, family_name TEXT, createdAt INTEGER DEFAULT (strftime('%s', 'now')), updatedAt INTEGER DEFAULT (strftime('%s', 'now')) ) `); // Migration: Check and add missing columns one by one 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('❌ Migration error:', e.message); // Don't throw, continue with existing schema } return db; }; export default defineEventHandler(async (event) => { console.log("🔄 Sync all users endpoint called"); // Get session from session store const { getSessionFromCookie } = await import('~/server/utils/sessionStore'); const session = await getSessionFromCookie(event); if (!session) { throw createError({ statusCode: 401, statusMessage: "No session found or session expired", }); } try { const config = useRuntimeConfig(); const accessToken = session.accessToken; if (!accessToken) { throw createError({ statusCode: 401, statusMessage: "No access token found", }); } // Get all users from Keycloak console.log("📥 Fetching all users from Keycloak..."); const keycloakUsers = await getAllUsersFromKeycloak(accessToken, config); console.log(`✅ Found ${keycloakUsers.length} users in Keycloak`); const db = initDb(); let createdCount = 0; let updatedCount = 0; let unchangedCount = 0; // Sync each user for (const kcUser of keycloakUsers) { try { const userId = kcUser.id; const namaLengkap = kcUser.firstName && kcUser.lastName ? `${kcUser.firstName} ${kcUser.lastName}`.trim() : kcUser.firstName || kcUser.lastName || kcUser.username || ''; const namaUser = kcUser.username || kcUser.email?.split('@')[0] || ''; const email = kcUser.email || null; const given_name = kcUser.firstName || null; const family_name = kcUser.lastName || null; if (!userId || !namaUser) { console.warn(`⚠️ Skipping user with missing ID or username: ${userId}`); continue; } // Get user details from Keycloak (including roles and groups) const userDetails = await getUserFromKeycloak(userId, accessToken, config); // Extract roles and groups const realmRoles = userDetails?.realmRoles || []; const groups = userDetails?.groups || []; // Determine tipeUser from groups let tipeUser = ''; if (Array.isArray(groups) && groups.length > 0) { const lastGroup = groups[groups.length - 1]; if (typeof lastGroup === 'string') { const parts = lastGroup.split('/').filter(Boolean); if (parts.length > 0) { tipeUser = parts[parts.length - 1]; } } } // Get last access from Keycloak const lastLogin = await getLastAccessFromKeycloak(userId, accessToken, config); // Check if user exists in database const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any; const rolesJson = JSON.stringify(realmRoles); const realmRolesJson = JSON.stringify(realmRoles); const accountRolesJson = JSON.stringify([]); const resourceRolesJson = JSON.stringify([]); const groupsJson = JSON.stringify(groups); if (existingUser) { // Update existing user - only update if there are changes const needsUpdate = existingUser.namaLengkap !== namaLengkap || existingUser.namaUser !== namaUser || existingUser.email !== email || existingUser.roles !== rolesJson || existingUser.realmRoles !== realmRolesJson || existingUser.groups !== groupsJson || existingUser.given_name !== given_name || existingUser.family_name !== family_name || (existingUser.tipeUser === '' && tipeUser !== '') || (lastLogin && existingUser.lastLogin !== lastLogin); if (needsUpdate) { const updateTipeUser = existingUser.tipeUser === '' ? tipeUser : existingUser.tipeUser; const updateLastLogin = lastLogin || existingUser.lastLogin; db.prepare(` UPDATE users SET namaLengkap = ?, namaUser = ?, email = ?, roles = ?, realmRoles = ?, accountRoles = ?, resourceRoles = ?, groups = ?, given_name = ?, family_name = ?, tipeUser = ?, lastLogin = ?, updatedAt = strftime('%s', 'now') WHERE id = ? `).run( namaLengkap, namaUser, email || null, rolesJson, realmRolesJson, accountRolesJson, resourceRolesJson, groupsJson, given_name, family_name, updateTipeUser, updateLastLogin, userId ); updatedCount++; console.log(`✅ Updated user: ${namaUser}`); } else { unchangedCount++; } } else { // Insert new user db.prepare(` INSERT INTO users ( id, namaLengkap, namaUser, email, roles, realmRoles, accountRoles, resourceRoles, groups, given_name, family_name, tipeUser, lastLogin ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( userId, namaLengkap, namaUser, email || null, rolesJson, realmRolesJson, accountRolesJson, resourceRolesJson, groupsJson, given_name, family_name, tipeUser, lastLogin ); createdCount++; console.log(`✅ Created new user: ${namaUser}`); } } catch (userError: any) { console.error(`❌ Error syncing user ${kcUser.id}:`, userError.message); // Continue with next user } } db.close(); console.log(`✅ Sync completed: ${createdCount} created, ${updatedCount} updated, ${unchangedCount} unchanged`); return { success: true, message: 'All users synced successfully', stats: { created: createdCount, updated: updatedCount, unchanged: unchangedCount, total: keycloakUsers.length } }; } catch (error: any) { console.error("❌ Error syncing all users:", error); throw createError({ statusCode: 500, statusMessage: error.message || "Failed to sync all users", }); } });