completed sync script update keycloak
This commit is contained in:
parent
a487b73c3b
commit
625885973e
13 changed files with 1867 additions and 466 deletions
|
|
@ -4,7 +4,16 @@ import { ProfileEmployee } from "../entities/ProfileEmployee";
|
|||
// import { PosMaster } from "../entities/PosMaster";
|
||||
// import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
// import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { getUser, updateUserAttributes } from "../keycloak";
|
||||
import {
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByUsername,
|
||||
updateUserAttributes,
|
||||
deleteUser,
|
||||
getRoles,
|
||||
addUserRoles,
|
||||
getAllUsersPaginated,
|
||||
} from "../keycloak";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
|
||||
export interface UserProfileAttributes {
|
||||
|
|
@ -15,6 +24,7 @@ export interface UserProfileAttributes {
|
|||
orgChild3DnaId: string | null;
|
||||
orgChild4DnaId: string | null;
|
||||
empType: string | null;
|
||||
prefix?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,6 +83,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId,
|
||||
orgChild4DnaId,
|
||||
empType: "OFFICER",
|
||||
prefix: profileResult.prefix,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +118,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId,
|
||||
orgChild4DnaId,
|
||||
empType: profileEmployeeResult.employeeClass,
|
||||
prefix: profileEmployeeResult.prefix,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +131,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId: null,
|
||||
orgChild4DnaId: null,
|
||||
empType: null,
|
||||
prefix: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +183,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId,
|
||||
orgChild4DnaId,
|
||||
empType: "OFFICER",
|
||||
prefix: profileResult.prefix,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
|
@ -204,6 +218,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId,
|
||||
orgChild4DnaId,
|
||||
empType: profileEmployeeResult.employeeClass,
|
||||
prefix: profileEmployeeResult.prefix,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +231,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId: null,
|
||||
orgChild4DnaId: null,
|
||||
empType: null,
|
||||
prefix: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +259,7 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId: [attributes.orgChild3DnaId || ""],
|
||||
orgChild4DnaId: [attributes.orgChild4DnaId || ""],
|
||||
empType: [attributes.empType || ""],
|
||||
prefix: [attributes.prefix || ""],
|
||||
};
|
||||
|
||||
const success = await updateUserAttributes(keycloakUserId, keycloakAttributes);
|
||||
|
|
@ -297,15 +314,19 @@ export class KeycloakAttributeService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Batch sync multiple users
|
||||
* Batch sync multiple users with unlimited count and parallel processing
|
||||
* Useful for initial sync or periodic updates
|
||||
*
|
||||
* @param limit - Maximum number of users to sync (default: 100)
|
||||
* @param options - Optional configuration (limit for testing, concurrency for parallel processing)
|
||||
* @returns Object with success count and details
|
||||
*/
|
||||
async batchSyncUsers(
|
||||
limit: number = 100,
|
||||
): Promise<{ total: number; success: number; failed: number; details: any[] }> {
|
||||
async batchSyncUsers(options?: {
|
||||
limit?: number;
|
||||
concurrency?: number;
|
||||
}): Promise<{ total: number; success: number; failed: number; details: any[] }> {
|
||||
const limit = options?.limit;
|
||||
const concurrency = options?.concurrency ?? 5;
|
||||
|
||||
const result = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
|
|
@ -314,57 +335,92 @@ export class KeycloakAttributeService {
|
|||
};
|
||||
|
||||
try {
|
||||
// Get profiles with keycloak IDs (ข้าราชการ)
|
||||
const profiles = await this.profileRepo
|
||||
// Build query for profiles with keycloak IDs (ข้าราชการ)
|
||||
const profileQuery = this.profileRepo
|
||||
.createQueryBuilder("p")
|
||||
.where("p.keycloak IS NOT NULL")
|
||||
.andWhere("p.keycloak != :empty", { empty: "" })
|
||||
.take(limit)
|
||||
.getMany();
|
||||
.andWhere("p.keycloak != :empty", { empty: "" });
|
||||
|
||||
// Get profileEmployees with keycloak IDs (ลูกจ้าง)
|
||||
const profileEmployees = await this.profileEmployeeRepo
|
||||
// Build query for profileEmployees with keycloak IDs (ลูกจ้าง)
|
||||
const profileEmployeeQuery = this.profileEmployeeRepo
|
||||
.createQueryBuilder("pe")
|
||||
.where("pe.keycloak IS NOT NULL")
|
||||
.andWhere("pe.keycloak != :empty", { empty: "" })
|
||||
.take(limit)
|
||||
.getMany();
|
||||
.andWhere("pe.keycloak != :empty", { empty: "" });
|
||||
|
||||
// Apply limit if specified (for testing purposes)
|
||||
if (limit !== undefined) {
|
||||
profileQuery.take(limit);
|
||||
profileEmployeeQuery.take(limit);
|
||||
}
|
||||
|
||||
// Get profiles from both tables
|
||||
const [profiles, profileEmployees] = await Promise.all([
|
||||
profileQuery.getMany(),
|
||||
profileEmployeeQuery.getMany(),
|
||||
]);
|
||||
|
||||
const allProfiles = [
|
||||
...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })),
|
||||
...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })),
|
||||
];
|
||||
|
||||
const allProfiles = [...profiles, ...profileEmployees];
|
||||
result.total = allProfiles.length;
|
||||
|
||||
for (const profile of allProfiles) {
|
||||
const keycloakUserId = profile.keycloak;
|
||||
const profileType = profile instanceof Profile ? "PROFILE" : "PROFILE_EMPLOYEE";
|
||||
// Process in parallel with concurrency limit
|
||||
const processedResults = await this.processInParallel(
|
||||
allProfiles,
|
||||
concurrency,
|
||||
async ({ profile, type }, _index) => {
|
||||
const keycloakUserId = profile.keycloak;
|
||||
|
||||
try {
|
||||
const success = await this.syncOnOrganizationChange(profile.id, profileType);
|
||||
if (success) {
|
||||
result.success++;
|
||||
result.details.push({
|
||||
profileId: profile.id,
|
||||
keycloakUserId,
|
||||
status: "success",
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const success = await this.syncOnOrganizationChange(profile.id, type);
|
||||
if (success) {
|
||||
result.success++;
|
||||
return {
|
||||
profileId: profile.id,
|
||||
keycloakUserId,
|
||||
status: "success",
|
||||
};
|
||||
} else {
|
||||
result.failed++;
|
||||
return {
|
||||
profileId: profile.id,
|
||||
keycloakUserId,
|
||||
status: "failed",
|
||||
error: "Sync returned false",
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
result.failed++;
|
||||
result.details.push({
|
||||
return {
|
||||
profileId: profile.id,
|
||||
keycloakUserId,
|
||||
status: "failed",
|
||||
error: "Sync returned false",
|
||||
});
|
||||
status: "error",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
},
|
||||
);
|
||||
|
||||
// Separate results from errors
|
||||
for (const resultItem of processedResults) {
|
||||
if ("error" in resultItem) {
|
||||
result.failed++;
|
||||
result.details.push({
|
||||
profileId: profile.id,
|
||||
keycloakUserId,
|
||||
profileId: "unknown",
|
||||
keycloakUserId: "unknown",
|
||||
status: "error",
|
||||
error: error.message,
|
||||
error: JSON.stringify(resultItem.error),
|
||||
});
|
||||
} else {
|
||||
result.details.push(resultItem);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in batch sync:", error);
|
||||
}
|
||||
|
|
@ -396,10 +452,477 @@ export class KeycloakAttributeService {
|
|||
orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "",
|
||||
orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "",
|
||||
empType: user.attributes.empType?.[0] || "",
|
||||
prefix: user.attributes.prefix?.[0] || "",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Keycloak user exists for a profile
|
||||
* Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak
|
||||
*
|
||||
* @param profileId - Profile ID
|
||||
* @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE'
|
||||
* @returns Object with status and details
|
||||
*/
|
||||
async ensureKeycloakUser(
|
||||
profileId: string,
|
||||
profileType: "PROFILE" | "PROFILE_EMPLOYEE",
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
action: "created" | "verified" | "skipped" | "error";
|
||||
keycloakUserId?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// Get profile from database
|
||||
let profile: Profile | ProfileEmployee | null = null;
|
||||
|
||||
if (profileType === "PROFILE") {
|
||||
profile = await this.profileRepo.findOne({ where: { id: profileId } });
|
||||
} else {
|
||||
profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } });
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return {
|
||||
success: false,
|
||||
action: "error",
|
||||
error: `Profile ${profileId} not found in database`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if citizenId exists
|
||||
if (!profile.citizenId) {
|
||||
return {
|
||||
success: false,
|
||||
action: "skipped",
|
||||
error: "No citizenId found",
|
||||
};
|
||||
}
|
||||
|
||||
// Case 1: keycloak field is empty -> create new user
|
||||
if (!profile.keycloak || profile.keycloak.trim() === "") {
|
||||
const result = await this.createKeycloakUserFromProfile(profile, profileType);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Case 2: keycloak field is not empty -> verify user exists in Keycloak
|
||||
const existingUser = await getUser(profile.keycloak);
|
||||
|
||||
if (!existingUser) {
|
||||
// User doesn't exist in Keycloak, create new one
|
||||
console.log(
|
||||
`Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`,
|
||||
);
|
||||
const result = await this.createKeycloakUserFromProfile(profile, profileType);
|
||||
return result;
|
||||
}
|
||||
|
||||
// User exists in Keycloak, verified
|
||||
return {
|
||||
success: true,
|
||||
action: "verified",
|
||||
keycloakUserId: profile.keycloak,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
action: "error",
|
||||
error: error.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Keycloak user from profile data
|
||||
*
|
||||
* @param profile - Profile or ProfileEmployee entity
|
||||
* @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE'
|
||||
* @returns Object with status and details
|
||||
*/
|
||||
private async createKeycloakUserFromProfile(
|
||||
profile: Profile | ProfileEmployee,
|
||||
profileType: "PROFILE" | "PROFILE_EMPLOYEE",
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
action: "created" | "verified" | "skipped" | "error";
|
||||
keycloakUserId?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// Check if user already exists by username (citizenId)
|
||||
const existingUserByUsername = await getUserByUsername(profile.citizenId);
|
||||
if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) {
|
||||
// User already exists with this username, update the keycloak field
|
||||
const existingUserId = existingUserByUsername[0].id;
|
||||
console.log(
|
||||
`User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`,
|
||||
);
|
||||
|
||||
// Update the keycloak field in database
|
||||
if (profileType === "PROFILE") {
|
||||
await this.profileRepo.update(profile.id, { keycloak: existingUserId });
|
||||
} else {
|
||||
await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId });
|
||||
}
|
||||
|
||||
// Assign default USER role to existing user
|
||||
const userRole = await getRoles("USER");
|
||||
if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) {
|
||||
const roleAssigned = await addUserRoles(existingUserId, [
|
||||
{ id: String(userRole.id), name: String(userRole.name) },
|
||||
]);
|
||||
if (roleAssigned) {
|
||||
console.log(`Assigned USER role to existing user ${existingUserId}`);
|
||||
} else {
|
||||
console.warn(`Failed to assign USER role to existing user ${existingUserId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`USER role not found in Keycloak`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "verified",
|
||||
keycloakUserId: existingUserId,
|
||||
};
|
||||
}
|
||||
|
||||
// Create new user in Keycloak
|
||||
const createResult = await createUser(profile.citizenId, "P@ssw0rd", {
|
||||
firstName: profile.firstName || "",
|
||||
lastName: profile.lastName || "",
|
||||
email: profile.email || undefined,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
if (!createResult || typeof createResult !== "string") {
|
||||
return {
|
||||
success: false,
|
||||
action: "error",
|
||||
error: "Failed to create user in Keycloak",
|
||||
};
|
||||
}
|
||||
|
||||
const keycloakUserId = createResult;
|
||||
|
||||
// Update the keycloak field in database
|
||||
if (profileType === "PROFILE") {
|
||||
await this.profileRepo.update(profile.id, { keycloak: keycloakUserId });
|
||||
} else {
|
||||
await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId });
|
||||
}
|
||||
|
||||
// Assign default USER role
|
||||
const userRole = await getRoles("USER");
|
||||
if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) {
|
||||
const roleAssigned = await addUserRoles(keycloakUserId, [
|
||||
{ id: String(userRole.id), name: String(userRole.name) },
|
||||
]);
|
||||
if (roleAssigned) {
|
||||
console.log(`Assigned USER role to user ${keycloakUserId}`);
|
||||
} else {
|
||||
console.warn(`Failed to assign USER role to user ${keycloakUserId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`USER role not found in Keycloak`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "created",
|
||||
keycloakUserId,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error creating Keycloak user for profile ${profile.id}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
action: "error",
|
||||
error: error.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process items in parallel with concurrency limit
|
||||
*/
|
||||
private async processInParallel<T, R>(
|
||||
items: T[],
|
||||
concurrencyLimit: number,
|
||||
processor: (item: T, index: number) => Promise<R>,
|
||||
): Promise<Array<R | { error: any }>> {
|
||||
const results: Array<R | { error: any }> = [];
|
||||
|
||||
// Process items in batches
|
||||
for (let i = 0; i < items.length; i += concurrencyLimit) {
|
||||
const batch = items.slice(i, i + concurrencyLimit);
|
||||
|
||||
// Process batch in parallel with error handling
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (item, batchIndex) => {
|
||||
try {
|
||||
return await processor(item, i + batchIndex);
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
results.push(...batchResults);
|
||||
|
||||
// Log progress after each batch
|
||||
const completed = Math.min(i + concurrencyLimit, items.length);
|
||||
console.log(`Progress: ${completed}/${items.length}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch ensure Keycloak users for all profiles
|
||||
* Processes all rows in Profile and ProfileEmployee tables
|
||||
*
|
||||
* @returns Object with total, success, failed counts and details
|
||||
*/
|
||||
async batchEnsureKeycloakUsers(): Promise<{
|
||||
total: number;
|
||||
created: number;
|
||||
verified: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
details: Array<{
|
||||
profileId: string;
|
||||
profileType: string;
|
||||
action: string;
|
||||
keycloakUserId?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
}> {
|
||||
const result = {
|
||||
total: 0,
|
||||
created: 0,
|
||||
verified: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
details: [] as Array<{
|
||||
profileId: string;
|
||||
profileType: string;
|
||||
action: string;
|
||||
keycloakUserId?: string;
|
||||
error?: string;
|
||||
}>,
|
||||
};
|
||||
|
||||
try {
|
||||
// Get all profiles from Profile table (ข้าราชการ)
|
||||
const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles
|
||||
|
||||
// Get all profiles from ProfileEmployee table (ลูกจ้าง)
|
||||
const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles
|
||||
|
||||
const allProfiles = [
|
||||
...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })),
|
||||
...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })),
|
||||
];
|
||||
|
||||
result.total = allProfiles.length;
|
||||
|
||||
// Process in parallel with concurrency limit
|
||||
const CONCURRENCY_LIMIT = 5; // Adjust based on environment
|
||||
|
||||
const processedResults = await this.processInParallel(
|
||||
allProfiles,
|
||||
CONCURRENCY_LIMIT,
|
||||
async ({ profile, type }) => {
|
||||
const ensureResult = await this.ensureKeycloakUser(profile.id, type);
|
||||
|
||||
// Update counters
|
||||
switch (ensureResult.action) {
|
||||
case "created":
|
||||
result.created++;
|
||||
break;
|
||||
case "verified":
|
||||
result.verified++;
|
||||
break;
|
||||
case "skipped":
|
||||
result.skipped++;
|
||||
break;
|
||||
case "error":
|
||||
result.failed++;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
profileId: profile.id,
|
||||
profileType: type,
|
||||
action: ensureResult.action,
|
||||
keycloakUserId: ensureResult.keycloakUserId,
|
||||
error: ensureResult.error,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Separate results from errors
|
||||
for (const resultItem of processedResults) {
|
||||
if ("error" in resultItem) {
|
||||
result.failed++;
|
||||
result.details.push({
|
||||
profileId: "unknown",
|
||||
profileType: "unknown",
|
||||
action: "error",
|
||||
error: JSON.stringify(resultItem.error),
|
||||
});
|
||||
} else {
|
||||
result.details.push(resultItem);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in batch ensure Keycloak users:", error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear orphaned Keycloak users
|
||||
* Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables
|
||||
*
|
||||
* @param skipUsernames - Array of usernames to skip (e.g., ['super_admin'])
|
||||
* @returns Object with counts and details
|
||||
*/
|
||||
async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{
|
||||
totalInKeycloak: number;
|
||||
totalInDatabase: number;
|
||||
orphanedCount: number;
|
||||
deleted: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
details: Array<{
|
||||
keycloakUserId: string;
|
||||
username: string;
|
||||
action: "deleted" | "skipped" | "error";
|
||||
error?: string;
|
||||
}>;
|
||||
}> {
|
||||
const result = {
|
||||
totalInKeycloak: 0,
|
||||
totalInDatabase: 0,
|
||||
orphanedCount: 0,
|
||||
deleted: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
details: [] as Array<{
|
||||
keycloakUserId: string;
|
||||
username: string;
|
||||
action: "deleted" | "skipped" | "error";
|
||||
error?: string;
|
||||
}>,
|
||||
};
|
||||
|
||||
try {
|
||||
// Get all keycloak IDs from database (Profile + ProfileEmployee)
|
||||
const profiles = await this.profileRepo
|
||||
.createQueryBuilder("p")
|
||||
.where("p.keycloak IS NOT NULL")
|
||||
.andWhere("p.keycloak != :empty", { empty: "" })
|
||||
.getMany();
|
||||
|
||||
const profileEmployees = await this.profileEmployeeRepo
|
||||
.createQueryBuilder("pe")
|
||||
.where("pe.keycloak IS NOT NULL")
|
||||
.andWhere("pe.keycloak != :empty", { empty: "" })
|
||||
.getMany();
|
||||
|
||||
// Create a Set of all keycloak IDs in database for O(1) lookup
|
||||
const databaseKeycloakIds = new Set<string>();
|
||||
for (const p of profiles) {
|
||||
if (p.keycloak) databaseKeycloakIds.add(p.keycloak);
|
||||
}
|
||||
for (const pe of profileEmployees) {
|
||||
if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak);
|
||||
}
|
||||
|
||||
result.totalInDatabase = databaseKeycloakIds.size;
|
||||
|
||||
// Get all users from Keycloak with pagination to avoid response size limits
|
||||
const keycloakUsers = await getAllUsersPaginated();
|
||||
if (!keycloakUsers || typeof keycloakUsers !== "object") {
|
||||
throw new Error("Failed to get users from Keycloak");
|
||||
}
|
||||
|
||||
result.totalInKeycloak = keycloakUsers.length;
|
||||
|
||||
// Find orphaned users (in Keycloak but not in database)
|
||||
const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id));
|
||||
result.orphanedCount = orphanedUsers.length;
|
||||
|
||||
// Delete orphaned users (skip protected ones)
|
||||
for (const user of orphanedUsers) {
|
||||
const username = user.username;
|
||||
const userId = user.id;
|
||||
|
||||
// Check if user should be skipped
|
||||
if (skipUsernames.includes(username)) {
|
||||
result.skipped++;
|
||||
result.details.push({
|
||||
keycloakUserId: userId,
|
||||
username,
|
||||
action: "skipped",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete user from Keycloak
|
||||
try {
|
||||
const deleteSuccess = await deleteUser(userId);
|
||||
if (deleteSuccess) {
|
||||
result.deleted++;
|
||||
result.details.push({
|
||||
keycloakUserId: userId,
|
||||
username,
|
||||
action: "deleted",
|
||||
});
|
||||
} else {
|
||||
result.failed++;
|
||||
result.details.push({
|
||||
keycloakUserId: userId,
|
||||
username,
|
||||
action: "error",
|
||||
error: "Failed to delete user from Keycloak",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
result.failed++;
|
||||
result.details.push({
|
||||
keycloakUserId: userId,
|
||||
username,
|
||||
action: "error",
|
||||
error: error.message || "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in clear orphaned Keycloak users:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue