fix: sync and script keycloak
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m59s

This commit is contained in:
Warunee Tamkoo 2026-02-26 23:09:22 +07:00
parent 1c629cc6e0
commit d667ad9173
12 changed files with 2444 additions and 33 deletions

View 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
View 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);

View 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
View 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
View 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);