completed sync script update keycloak
This commit is contained in:
parent
a487b73c3b
commit
625885973e
13 changed files with 1867 additions and 466 deletions
149
scripts/KEYCLOAK_SYNC_README.md
Normal file
149
scripts/KEYCLOAK_SYNC_README.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Keycloak Sync Scripts
|
||||
|
||||
This directory contains standalone scripts for managing Keycloak users from the CLI. These scripts are useful for maintenance, setup, and troubleshooting Keycloak synchronization.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and TypeScript installed
|
||||
- Database connection configured in `.env`
|
||||
- Keycloak connection configured in `.env`
|
||||
- Run with `ts-node` or compile first
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Ensure these are set in your `.env` file:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=your_database
|
||||
DB_USER=your_user
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
# Keycloak
|
||||
KC_URL=https://your-keycloak-url
|
||||
KC_REALMS=your-realm
|
||||
KC_SERVICE_ACCOUNT_CLIENT_ID=your-client-id
|
||||
KC_SERVICE_ACCOUNT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
### 1. clear-orphaned-users.ts
|
||||
|
||||
Deletes Keycloak users that don't exist in the database (Profile + ProfileEmployee tables). Useful for cleaning up invalid users.
|
||||
|
||||
**Protected usernames** (never deleted): `super_admin`, `admin_issue`
|
||||
|
||||
```bash
|
||||
# Dry run (preview changes)
|
||||
ts-node scripts/clear-orphaned-users.ts --dry-run
|
||||
|
||||
# Live execution
|
||||
ts-node scripts/clear-orphaned-users.ts
|
||||
```
|
||||
|
||||
**Output:**
|
||||
- Total users in Keycloak
|
||||
- Total users in Database
|
||||
- Orphaned users found
|
||||
- Deleted/Skipped/Failed counts
|
||||
|
||||
### 2. ensure-users.ts
|
||||
|
||||
Checks and creates Keycloak users for all profiles. Includes role assignment (USER role) automatically.
|
||||
|
||||
```bash
|
||||
# Dry run (preview changes)
|
||||
ts-node scripts/ensure-users.ts --dry-run
|
||||
|
||||
# Live execution
|
||||
ts-node scripts/ensure-users.ts
|
||||
|
||||
# Test with limited profiles (for testing)
|
||||
ts-node scripts/ensure-users.ts --dry-run --limit=10
|
||||
```
|
||||
|
||||
**Output:**
|
||||
- Total profiles processed
|
||||
- Created users (new Keycloak accounts)
|
||||
- Verified users (already exists)
|
||||
- Skipped profiles (no citizenId)
|
||||
- Failed count
|
||||
|
||||
### 3. sync-all.ts
|
||||
|
||||
Syncs all attributes to Keycloak for users with existing Keycloak IDs.
|
||||
|
||||
**Attributes synced:**
|
||||
- `profileId`
|
||||
- `orgRootDnaId`
|
||||
- `orgChild1DnaId`
|
||||
- `orgChild2DnaId`
|
||||
- `orgChild3DnaId`
|
||||
- `orgChild4DnaId`
|
||||
- `empType`
|
||||
- `prefix`
|
||||
|
||||
```bash
|
||||
# Dry run (preview changes)
|
||||
ts-node scripts/sync-all.ts --dry-run
|
||||
|
||||
# Live execution
|
||||
ts-node scripts/sync-all.ts
|
||||
|
||||
# Test with limited profiles
|
||||
ts-node scripts/sync-all.ts --dry-run --limit=10
|
||||
|
||||
# Adjust concurrency (default: 5)
|
||||
ts-node scripts/sync-all.ts --concurrency=10
|
||||
```
|
||||
|
||||
**Output:**
|
||||
- Total profiles with Keycloak IDs
|
||||
- Success/Failed counts
|
||||
- Error details for failures
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
For initial setup or full resynchronization:
|
||||
|
||||
1. **clear-orphaned-users** - Clean up invalid users first
|
||||
```bash
|
||||
npx ts-node scripts/clear-orphaned-users.ts
|
||||
```
|
||||
|
||||
2. **ensure-users** - Create missing users and assign roles
|
||||
```bash
|
||||
npx ts-node scripts/ensure-users.ts
|
||||
```
|
||||
|
||||
3. **sync-all** - Sync all attributes to Keycloak
|
||||
```bash
|
||||
npx ts-node scripts/sync-all.ts
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Always run with `--dry-run` first to preview changes
|
||||
- Use `--limit=N` for testing before running on full dataset
|
||||
- Scripts process both Profile (ข้าราชการ) and ProfileEmployee (ลูกจ้าง) tables
|
||||
- Only active profiles (`isLeave: false`) are processed by ensure-users
|
||||
- The `USER` role is automatically assigned to new/verified users
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Database connection error:**
|
||||
- Check `.env` database variables
|
||||
- Ensure database server is running
|
||||
|
||||
**Keycloak connection error:**
|
||||
- Check `KC_URL`, `KC_REALMS` in `.env`
|
||||
- Verify service account credentials
|
||||
- Check network connectivity to Keycloak
|
||||
|
||||
**USER role not found:**
|
||||
- Log in to Keycloak admin console
|
||||
- Create a `USER` role in your realm
|
||||
- Ensure service account has `manage-users` and `view-users` permissions
|
||||
269
scripts/assign-user-role.ts
Normal file
269
scripts/assign-user-role.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import "dotenv/config";
|
||||
import { AppDataSource } from "../src/database/data-source";
|
||||
import { Profile } from "../src/entities/Profile";
|
||||
import { ProfileEmployee } from "../src/entities/ProfileEmployee";
|
||||
import * as keycloak from "../src/keycloak/index";
|
||||
|
||||
const USER_ROLE_NAME = "USER";
|
||||
|
||||
interface AssignOptions {
|
||||
dryRun: boolean;
|
||||
targetUsernames?: string[];
|
||||
}
|
||||
|
||||
interface UserWithKeycloak {
|
||||
keycloakId: string;
|
||||
citizenId: string;
|
||||
source: "Profile" | "ProfileEmployee";
|
||||
}
|
||||
|
||||
interface AssignResult {
|
||||
total: number;
|
||||
assigned: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
errors: Array<{
|
||||
userId: string;
|
||||
username: string;
|
||||
error: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users from database who have Keycloak IDs set
|
||||
*/
|
||||
async function getUsersWithKeycloak(): Promise<UserWithKeycloak[]> {
|
||||
const users: UserWithKeycloak[] = [];
|
||||
const profileRepo = AppDataSource.getRepository(Profile);
|
||||
const profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
|
||||
|
||||
// Get from Profile table
|
||||
const profiles = await profileRepo
|
||||
.createQueryBuilder("profile")
|
||||
.where("profile.keycloak IS NOT NULL")
|
||||
.andWhere("profile.keycloak != ''")
|
||||
.getMany();
|
||||
|
||||
for (const profile of profiles) {
|
||||
users.push({
|
||||
keycloakId: profile.keycloak,
|
||||
citizenId: profile.citizenId || profile.id,
|
||||
source: "Profile",
|
||||
});
|
||||
}
|
||||
|
||||
// Get from ProfileEmployee table
|
||||
const employees = await profileEmployeeRepo
|
||||
.createQueryBuilder("profileEmployee")
|
||||
.where("profileEmployee.keycloak IS NOT NULL")
|
||||
.andWhere("profileEmployee.keycloak != ''")
|
||||
.getMany();
|
||||
|
||||
for (const employee of employees) {
|
||||
// Avoid duplicates - check if keycloak ID already exists
|
||||
if (!users.some((u) => u.keycloakId === employee.keycloak)) {
|
||||
users.push({
|
||||
keycloakId: employee.keycloak,
|
||||
citizenId: employee.citizenId || employee.id,
|
||||
source: "ProfileEmployee",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign USER role to users who don't have it
|
||||
*/
|
||||
async function assignUserRoleToUsers(
|
||||
users: UserWithKeycloak[],
|
||||
userRoleId: string,
|
||||
options: AssignOptions,
|
||||
): Promise<AssignResult> {
|
||||
const result: AssignResult = {
|
||||
total: users.length,
|
||||
assigned: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
console.log(`Processing ${result.total} users...`);
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
const index = i + 1;
|
||||
|
||||
try {
|
||||
// Get user's current roles
|
||||
const userRoles = await keycloak.getUserRoles(user.keycloakId);
|
||||
|
||||
if (!userRoles || typeof userRoles === "boolean") {
|
||||
console.log(
|
||||
`[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Failed to get roles`,
|
||||
);
|
||||
result.failed++;
|
||||
result.errors.push({
|
||||
userId: user.keycloakId,
|
||||
username: user.citizenId,
|
||||
error: "Failed to get user roles",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle both array and single object return types
|
||||
// getUserRoles can return an array or a single object
|
||||
const rolesArray = Array.isArray(userRoles) ? userRoles : [userRoles];
|
||||
|
||||
// Check if user already has USER role
|
||||
const hasUserRole = rolesArray.some((role: { id: string; name: string }) =>
|
||||
role.name === USER_ROLE_NAME,
|
||||
);
|
||||
|
||||
if (hasUserRole) {
|
||||
console.log(
|
||||
`[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Already has USER role`,
|
||||
);
|
||||
result.skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assign USER role
|
||||
if (options.dryRun) {
|
||||
console.log(
|
||||
`[${index}/${result.total}] [DRY-RUN] Would assign USER role: ${user.citizenId} (source: ${user.source})`,
|
||||
);
|
||||
result.assigned++;
|
||||
} else {
|
||||
const assignResult = await keycloak.addUserRoles(user.keycloakId, [
|
||||
{ id: userRoleId, name: USER_ROLE_NAME },
|
||||
]);
|
||||
|
||||
if (assignResult) {
|
||||
console.log(
|
||||
`[${index}/${result.total}] Assigned USER role: ${user.citizenId} (source: ${user.source})`,
|
||||
);
|
||||
result.assigned++;
|
||||
} else {
|
||||
console.log(
|
||||
`[${index}/${result.total}] Failed: ${user.citizenId} (source: ${user.source}) - Could not assign role`,
|
||||
);
|
||||
result.failed++;
|
||||
result.errors.push({
|
||||
userId: user.keycloakId,
|
||||
username: user.citizenId,
|
||||
error: "Failed to assign USER role",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
if (index % 50 === 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.log(
|
||||
`[${index}/${result.total}] Error: ${user.citizenId} (source: ${user.source}) - ${errorMessage}`,
|
||||
);
|
||||
result.failed++;
|
||||
result.errors.push({
|
||||
userId: user.keycloakId,
|
||||
username: user.citizenId,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 Role Assignment Script");
|
||||
console.log("=".repeat(60));
|
||||
console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`);
|
||||
console.log("");
|
||||
|
||||
// Initialize database
|
||||
try {
|
||||
await AppDataSource.initialize();
|
||||
console.log("Database connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to database:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Keycloak connection
|
||||
try {
|
||||
await keycloak.getToken();
|
||||
console.log("Keycloak connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to Keycloak:", error);
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get USER role from Keycloak
|
||||
console.log("");
|
||||
const userRole = await keycloak.getRoles(USER_ROLE_NAME);
|
||||
|
||||
// Check if USER role exists and is valid (has id property)
|
||||
if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) {
|
||||
console.error(`ERROR: ${USER_ROLE_NAME} role not found in Keycloak`);
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Type assertion via unknown to bypass union type issues
|
||||
const role = userRole as unknown as { id: string; name: string; description?: string };
|
||||
const roleId = role.id;
|
||||
console.log(`${USER_ROLE_NAME} role found: ${roleId}`);
|
||||
console.log("");
|
||||
|
||||
// Get users from database
|
||||
console.log("Fetching users from database...");
|
||||
let users = await getUsersWithKeycloak();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log("No users with Keycloak IDs found in database");
|
||||
await AppDataSource.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${users.length} users with Keycloak IDs`);
|
||||
console.log("");
|
||||
|
||||
// Assign USER role
|
||||
const result = await assignUserRoleToUsers(users, roleId, { dryRun });
|
||||
|
||||
// Summary
|
||||
console.log("");
|
||||
console.log("=".repeat(60));
|
||||
console.log("Summary:");
|
||||
console.log(` Total users: ${result.total}`);
|
||||
console.log(` Assigned: ${result.assigned}`);
|
||||
console.log(` Skipped: ${result.skipped}`);
|
||||
console.log(` Failed: ${result.failed}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log("");
|
||||
console.log("Errors:");
|
||||
result.errors.forEach((e) => {
|
||||
console.log(` ${e.username}: ${e.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await AppDataSource.destroy();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
80
scripts/clear-orphaned-users.ts
Normal file
80
scripts/clear-orphaned-users.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import "dotenv/config";
|
||||
import { AppDataSource } from "../src/database/data-source";
|
||||
import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService";
|
||||
import * as keycloak from "../src/keycloak/index";
|
||||
|
||||
const PROTECTED_USERNAMES = ["super_admin", "admin_issue"];
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const skipUsernames = [...PROTECTED_USERNAMES];
|
||||
|
||||
console.log("=".repeat(60));
|
||||
console.log("Clear Orphaned Keycloak Users Script");
|
||||
console.log("=".repeat(60));
|
||||
console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`);
|
||||
console.log(`Protected usernames: ${skipUsernames.join(", ")}`);
|
||||
console.log("");
|
||||
|
||||
// Initialize database
|
||||
try {
|
||||
await AppDataSource.initialize();
|
||||
console.log("Database connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to database:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Keycloak connection
|
||||
try {
|
||||
await keycloak.getToken();
|
||||
console.log("Keycloak connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to Keycloak:", error);
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
|
||||
// Run the orphaned user cleanup
|
||||
const service = new KeycloakAttributeService();
|
||||
const result = await service.clearOrphanedKeycloakUsers(skipUsernames);
|
||||
|
||||
// Summary
|
||||
console.log("");
|
||||
console.log("=".repeat(60));
|
||||
console.log("Summary:");
|
||||
console.log(` Total users in Keycloak: ${result.totalInKeycloak}`);
|
||||
console.log(` Total users in Database: ${result.totalInDatabase}`);
|
||||
console.log(` Orphaned users: ${result.orphanedCount}`);
|
||||
console.log(` Deleted: ${result.deleted}`);
|
||||
console.log(` Skipped: ${result.skipped}`);
|
||||
console.log(` Failed: ${result.failed}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
if (result.details.length > 0) {
|
||||
console.log("");
|
||||
console.log("Details:");
|
||||
for (const detail of result.details) {
|
||||
const status =
|
||||
detail.action === "deleted"
|
||||
? "[DELETED]"
|
||||
: detail.action === "skipped"
|
||||
? "[SKIPPED]"
|
||||
: "[ERROR]";
|
||||
console.log(
|
||||
` ${status} ${detail.username} (${detail.keycloakUserId})${detail.error ? ": " + detail.error : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await AppDataSource.destroy();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
91
scripts/ensure-users.ts
Normal file
91
scripts/ensure-users.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import "dotenv/config";
|
||||
import { AppDataSource } from "../src/database/data-source";
|
||||
import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService";
|
||||
import * as keycloak from "../src/keycloak/index";
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const limitArg = args.find((arg) => arg.startsWith("--limit="));
|
||||
const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined;
|
||||
|
||||
console.log("=".repeat(60));
|
||||
console.log("Ensure Keycloak Users Script");
|
||||
console.log("=".repeat(60));
|
||||
console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`);
|
||||
if (limit !== undefined) {
|
||||
console.log(`Limit: ${limit} profiles per table (for testing)`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// Initialize database
|
||||
try {
|
||||
await AppDataSource.initialize();
|
||||
console.log("Database connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to database:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Keycloak connection
|
||||
try {
|
||||
await keycloak.getToken();
|
||||
console.log("Keycloak connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to Keycloak:", error);
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
|
||||
// Verify USER role exists
|
||||
console.log("Verifying USER role in Keycloak...");
|
||||
const userRole = await keycloak.getRoles("USER");
|
||||
|
||||
if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) {
|
||||
console.error("ERROR: USER role not found in Keycloak");
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("USER role found");
|
||||
console.log("");
|
||||
|
||||
// Run the ensure users operation
|
||||
const service = new KeycloakAttributeService();
|
||||
console.log("Ensuring Keycloak users for all profiles...");
|
||||
console.log("");
|
||||
|
||||
const result = await service.batchEnsureKeycloakUsers();
|
||||
|
||||
// Summary
|
||||
console.log("");
|
||||
console.log("=".repeat(60));
|
||||
console.log("Summary:");
|
||||
console.log(` Total profiles: ${result.total}`);
|
||||
console.log(` Created: ${result.created}`);
|
||||
console.log(` Verified: ${result.verified}`);
|
||||
console.log(` Skipped: ${result.skipped}`);
|
||||
console.log(` Failed: ${result.failed}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
if (result.failed > 0) {
|
||||
console.log("");
|
||||
console.log("Failed Details:");
|
||||
const failedDetails = result.details.filter((d) => d.action === "error" || !!d.error);
|
||||
for (const detail of failedDetails) {
|
||||
console.log(
|
||||
` [${detail.profileType}] ${detail.profileId}: ${detail.error || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await AppDataSource.destroy();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
93
scripts/sync-all.ts
Normal file
93
scripts/sync-all.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import "dotenv/config";
|
||||
import { AppDataSource } from "../src/database/data-source";
|
||||
import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService";
|
||||
import * as keycloak from "../src/keycloak/index";
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const limitArg = args.find((arg) => arg.startsWith("--limit="));
|
||||
const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined;
|
||||
const concurrencyArg = args.find((arg) => arg.startsWith("--concurrency="));
|
||||
const concurrency = concurrencyArg ? parseInt(concurrencyArg.split("=")[1], 10) : undefined;
|
||||
|
||||
console.log("=".repeat(60));
|
||||
console.log("Sync All Attributes to Keycloak Script");
|
||||
console.log("=".repeat(60));
|
||||
console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`);
|
||||
if (limit !== undefined) {
|
||||
console.log(`Limit: ${limit} profiles per table (for testing)`);
|
||||
}
|
||||
if (concurrency !== undefined) {
|
||||
console.log(`Concurrency: ${concurrency}`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
console.log("Attributes to sync:");
|
||||
console.log(" - profileId");
|
||||
console.log(" - orgRootDnaId");
|
||||
console.log(" - orgChild1DnaId");
|
||||
console.log(" - orgChild2DnaId");
|
||||
console.log(" - orgChild3DnaId");
|
||||
console.log(" - orgChild4DnaId");
|
||||
console.log(" - empType");
|
||||
console.log(" - prefix");
|
||||
console.log("");
|
||||
|
||||
// Initialize database
|
||||
try {
|
||||
await AppDataSource.initialize();
|
||||
console.log("Database connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to database:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Keycloak connection
|
||||
try {
|
||||
await keycloak.getToken();
|
||||
console.log("Keycloak connected");
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to Keycloak:", error);
|
||||
await AppDataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
|
||||
// Run the sync operation
|
||||
const service = new KeycloakAttributeService();
|
||||
console.log("Syncing attributes for all profiles with Keycloak IDs...");
|
||||
console.log("");
|
||||
|
||||
const result = await service.batchSyncUsers({ limit, concurrency });
|
||||
|
||||
// Summary
|
||||
console.log("");
|
||||
console.log("=".repeat(60));
|
||||
console.log("Summary:");
|
||||
console.log(` Total profiles: ${result.total}`);
|
||||
console.log(` Success: ${result.success}`);
|
||||
console.log(` Failed: ${result.failed}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
if (result.failed > 0) {
|
||||
console.log("");
|
||||
console.log("Failed Details:");
|
||||
for (const detail of result.details.filter(
|
||||
(d) => d.status === "failed" || d.status === "error",
|
||||
)) {
|
||||
console.log(
|
||||
` ${detail.profileId} (${detail.keycloakUserId}): ${detail.error || "Sync failed"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await AppDataSource.destroy();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue