import "dotenv/config"; import { AppDataSource } from "../src/database/data-source"; import { Profile } from "../src/entities/Profile"; import { RoleKeycloak } from "../src/entities/RoleKeycloak"; import * as keycloak from "../src/keycloak/index"; // Default role for users without roles const DEFAULT_ROLE = "USER"; interface SyncOptions { dryRun: boolean; delay: number; } interface SyncResult { total: number; deleted: number; created: number; failed: number; skipped: number; errors: Array<{ profileId: string; citizenId: string; error: string; }>; } /** * Delete all Keycloak users (except super_admin) */ async function deleteAllKeycloakUsers(dryRun: boolean): Promise { const users = await keycloak.getUserList("0", "-1"); let count = 0; let skipped = 0; if (!users || typeof users === "boolean") { return 0; } for (const user of users) { // Skip super_admin user if (user.username === "super_admin") { console.log(`Skipped super_admin user (protected)`); skipped++; continue; } if (!dryRun) { await keycloak.deleteUser(user.id); } count++; console.log(`[${count}/${users.length}] Deleted user: ${user.username}`); } console.log(`Skipped ${skipped} protected user(s)`); return count; } /** * Sync profiles to Keycloak */ async function syncProfiles(options: SyncOptions): Promise { const result: SyncResult = { total: 0, deleted: 0, created: 0, failed: 0, skipped: 0, errors: [], }; // Fetch all Keycloak roles first const keycloakRoles = await keycloak.getRoles(); if (!keycloakRoles || typeof keycloakRoles === "boolean") { throw new Error("Failed to get Keycloak roles"); } const roleMap = new Map(keycloakRoles.map((r) => [r.name, r.id])); // Log all available Keycloak roles for debugging console.log("Available Keycloak roles:", Array.from(roleMap.keys()).sort()); console.log(""); // Get repositories const profileRepo = AppDataSource.getRepository(Profile); const roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); // Query all active profiles (not just those with keycloak set) const profiles = await profileRepo .createQueryBuilder("profile") .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloak") .andWhere("profile.isActive = :isActive", { isActive: true }) .getMany(); result.total = profiles.length; for (const profile of profiles) { const index = result.created + result.failed + result.skipped + 1; try { // Validate required fields if (!profile.citizenId) { console.log(`[${index}/${result.total}] Skipped: Missing citizenId`); result.skipped++; continue; } // Check if user already exists const existingUser = await keycloak.getUserByUsername(profile.citizenId); if (existingUser && existingUser.length > 0) { const existingUserId = existingUser[0].id; console.log( `[${index}/${result.total}] User ${profile.citizenId} already exists in Keycloak (ID: ${existingUserId})`, ); // Update profile.keycloak with existing Keycloak ID if (!options.dryRun) { await profileRepo.update(profile.id, { keycloak: existingUserId }); console.log(` -> Updated profile.keycloak: ${existingUserId}`); } result.skipped++; continue; } // Handle roles: assign USER role if no roles exist let rolesToAssign = profile.roleKeycloaks || []; let needsDefaultRole = false; if (rolesToAssign.length === 0) { needsDefaultRole = true; console.log( `[${index}/${result.total}] No roles found for ${profile.citizenId}, will assign ${DEFAULT_ROLE}`, ); // Check if USER role exists in Keycloak if (!roleMap.has(DEFAULT_ROLE)) { console.log( `[${index}/${result.total}] ERROR: ${DEFAULT_ROLE} role not found in Keycloak`, ); result.failed++; result.errors.push({ profileId: profile.id, citizenId: profile.citizenId, error: `${DEFAULT_ROLE} role not found in Keycloak`, }); continue; } // In live mode, create role in database if (!options.dryRun) { // Check if USER role record exists in database const userRoleRecord = await roleKeycloakRepo.findOne({ where: { name: DEFAULT_ROLE }, }); // Assign role to profile in database if (userRoleRecord) { profile.roleKeycloaks = [userRoleRecord]; await profileRepo.save(profile); rolesToAssign = [userRoleRecord]; } } } if (!options.dryRun) { // Create user in Keycloak const userId = await keycloak.createUser(profile.citizenId, "P@ssw0rd", { firstName: profile.firstName || "", lastName: profile.lastName || "", // email: profile.email || undefined, enabled: true, }); if (typeof userId === "string") { // Update profile.keycloak with new ID await profileRepo.update(profile.id, { keycloak: userId }); // Assign roles to user in Keycloak // Track which roles exist in Keycloak and which don't const validRoles: { id: string; name: string }[] = []; const missingRoles: string[] = []; for (const rk of rolesToAssign) { const roleId = roleMap.get(rk.name); if (roleId) { validRoles.push({ id: roleId, name: rk.name }); } else { missingRoles.push(rk.name); } } // Warn about missing roles if (missingRoles.length > 0) { console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); } if (validRoles.length > 0) { const addRolesResult = await keycloak.addUserRoles(userId, validRoles); if (addRolesResult === false) { console.log(` [WARNING] Failed to assign roles to user ${profile.citizenId}`); } const roleNames = validRoles.map((r) => r.name).join(", "); console.log( `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [Roles: ${roleNames}]`, ); } else { console.log( `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [No roles assigned]`, ); } result.created++; } else { console.log(`[${index}/${result.total}] Failed: ${profile.citizenId}`, userId); result.failed++; result.errors.push({ profileId: profile.id, citizenId: profile.citizenId, error: JSON.stringify(userId), }); } } else { // Dry-run mode - check which roles are valid const validRoles: string[] = []; const missingRoles: string[] = []; for (const rk of rolesToAssign) { if (roleMap.has(rk.name)) { validRoles.push(rk.name); } else { missingRoles.push(rk.name); } } if (needsDefaultRole && roleMap.has(DEFAULT_ROLE)) { validRoles.push(DEFAULT_ROLE); } else if (needsDefaultRole) { missingRoles.push(DEFAULT_ROLE); } const roleNames = validRoles.length > 0 ? validRoles.join(", ") : "None"; console.log( `[DRY-RUN ${index}/${result.total}] Would create: ${profile.citizenId} [Roles: ${roleNames}]`, ); if (missingRoles.length > 0) { console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); } result.created++; } // Delay to avoid rate limiting if (options.delay > 0) { await new Promise((resolve) => setTimeout(resolve, options.delay)); } } catch (error) { console.log(`[${index}/${result.total}] Error: ${profile.citizenId}`, error); result.failed++; result.errors.push({ profileId: profile.id, citizenId: profile.citizenId, error: String(error), }); } } return result; } /** * Main function */ async function main() { const args = process.argv.slice(2); const dryRun = args.includes("--dry-run"); console.log("=".repeat(60)); console.log("Keycloak User Sync Script"); console.log("=".repeat(60)); console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); console.log(""); // Initialize database await AppDataSource.initialize(); console.log("Database connected"); // Validate Keycloak connection await keycloak.getToken(); console.log("Keycloak connected"); console.log(""); // Step 1: Delete existing users console.log("Step 1: Deleting existing Keycloak users..."); const deletedCount = await deleteAllKeycloakUsers(dryRun); console.log(`Deleted ${deletedCount} users\n`); // Step 2: Sync profiles console.log("Step 2: Creating users from profiles..."); const result = await syncProfiles({ dryRun, delay: 100 }); console.log(""); // Summary console.log("=".repeat(60)); console.log("Summary:"); console.log(` Total profiles: ${result.total}`); console.log(` Deleted users: ${deletedCount}`); console.log(` Created users: ${result.created}`); console.log(` Failed: ${result.failed}`); console.log(` Skipped: ${result.skipped}`); console.log("=".repeat(60)); if (result.errors.length > 0) { console.log("\nErrors:"); result.errors.forEach((e) => { console.log(` ${e.citizenId}: ${e.error}`); }); } await AppDataSource.destroy(); } main().catch(console.error); // add this line to package.json scripts section: // "sync-keycloak": "ts-node scripts/sync-users-to-keycloak-null-only.ts", // "sync-keycloak:dry": "ts-node scripts/sync-users-to-keycloak-null-only.ts --dry-run"