327 lines
9.9 KiB
TypeScript
327 lines
9.9 KiB
TypeScript
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<number> {
|
|
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<SyncResult> {
|
|
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"
|