All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Get,
|
|
Route,
|
|
Security,
|
|
Tags,
|
|
Path,
|
|
Request,
|
|
Response,
|
|
Query,
|
|
Body,
|
|
} from "tsoa";
|
|
import { KeycloakAttributeService } from "../services/KeycloakAttributeService";
|
|
import HttpSuccess from "../interfaces/http-success";
|
|
import HttpStatus from "../interfaces/http-status";
|
|
import HttpError from "../interfaces/http-error";
|
|
import { RequestWithUser } from "../middlewares/user";
|
|
|
|
@Route("api/v1/org/keycloak-sync")
|
|
@Tags("Keycloak Sync")
|
|
@Security("bearerAuth")
|
|
@Response(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง",
|
|
)
|
|
export class KeycloakSyncController extends Controller {
|
|
private keycloakAttributeService = new KeycloakAttributeService();
|
|
|
|
/**
|
|
* Sync attributes for the current logged-in user
|
|
*
|
|
* @summary Sync profileId and rootDnaId to Keycloak for current user
|
|
*/
|
|
@Post("sync-me")
|
|
async syncCurrentUser(@Request() request: RequestWithUser) {
|
|
const keycloakUserId = request.user.sub;
|
|
|
|
if (!keycloakUserId) {
|
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID");
|
|
}
|
|
|
|
// Get attributes from database before sync
|
|
const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId);
|
|
|
|
const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId);
|
|
|
|
if (!success) {
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ",
|
|
);
|
|
}
|
|
|
|
// Verify sync by fetching attributes from Keycloak after update
|
|
const kcAttrsAfter =
|
|
await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId);
|
|
|
|
return new HttpSuccess({
|
|
message: "Sync ข้อมูลสำเร็จ",
|
|
syncedToKeycloak: !!kcAttrsAfter?.profileId,
|
|
databaseAttributes: dbAttrs,
|
|
keycloakAttributesAfter: kcAttrsAfter,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get current attributes of the logged-in user
|
|
*
|
|
* @summary Get current profileId and rootDnaId from Keycloak
|
|
*/
|
|
@Get("my-attributes")
|
|
async getMyAttributes(@Request() request: RequestWithUser) {
|
|
const keycloakUserId = request.user.sub;
|
|
|
|
if (!keycloakUserId) {
|
|
throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID");
|
|
}
|
|
|
|
const keycloakAttributes =
|
|
await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId);
|
|
const dbAttributes =
|
|
await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId);
|
|
|
|
return new HttpSuccess({
|
|
keycloakAttributes,
|
|
databaseAttributes: dbAttributes,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sync attributes for a specific profile (Admin only)
|
|
*
|
|
* @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN)
|
|
*
|
|
* @param {string} profileId Profile ID
|
|
* @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE)
|
|
*/
|
|
@Post("sync-profile/:profileId")
|
|
async syncByProfileId(
|
|
@Path() profileId: string,
|
|
@Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE",
|
|
) {
|
|
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
|
|
throw new HttpError(
|
|
HttpStatus.BAD_REQUEST,
|
|
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
|
|
);
|
|
}
|
|
|
|
const success = await this.keycloakAttributeService.syncOnOrganizationChange(
|
|
profileId,
|
|
profileType,
|
|
);
|
|
|
|
if (!success) {
|
|
throw new HttpError(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile",
|
|
);
|
|
}
|
|
|
|
return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" });
|
|
}
|
|
|
|
/**
|
|
* Batch sync attributes for multiple profiles (Admin only)
|
|
*
|
|
* @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN)
|
|
*
|
|
* @param {request} request Request body containing profileIds array and profileType
|
|
*/
|
|
@Post("sync-profiles-batch")
|
|
async syncByProfileIds(
|
|
@Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
|
|
) {
|
|
const { profileIds, profileType } = request;
|
|
|
|
// Validate profileIds
|
|
if (!profileIds || profileIds.length === 0) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
|
|
}
|
|
|
|
// Validate profileType
|
|
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
|
|
throw new HttpError(
|
|
HttpStatus.BAD_REQUEST,
|
|
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
|
|
);
|
|
}
|
|
|
|
const result = {
|
|
total: profileIds.length,
|
|
success: 0,
|
|
failed: 0,
|
|
details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
|
|
};
|
|
|
|
// Process each profileId
|
|
for (const profileId of profileIds) {
|
|
try {
|
|
const success = await this.keycloakAttributeService.syncOnOrganizationChange(
|
|
profileId,
|
|
profileType,
|
|
);
|
|
|
|
if (success) {
|
|
result.success++;
|
|
result.details.push({ profileId, status: "success" });
|
|
} else {
|
|
result.failed++;
|
|
result.details.push({
|
|
profileId,
|
|
status: "failed",
|
|
error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID",
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
result.failed++;
|
|
result.details.push({ profileId, status: "failed", error: error.message });
|
|
}
|
|
}
|
|
|
|
return new HttpSuccess({
|
|
message: "Batch sync เสร็จสิ้น",
|
|
...result,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear org DNA attributes for profiles (Admin only)
|
|
*
|
|
* @summary Clear org DNA attributes in Keycloak for given profiles (ADMIN)
|
|
*
|
|
* @description
|
|
* This endpoint will:
|
|
* - Clear all org DNA fields (orgRootDnaId, orgChild1-4DnaId) by setting them to empty strings
|
|
* - Use when an employee leaves their position (current_holderId becomes null)
|
|
*
|
|
* @param {request} request Request body containing profileIds array and profileType
|
|
*/
|
|
@Post("clear-org-dna")
|
|
async clearOrgDna(
|
|
@Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
|
|
) {
|
|
const { profileIds, profileType } = request;
|
|
|
|
// Validate profileIds
|
|
if (!profileIds || profileIds.length === 0) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
|
|
}
|
|
|
|
// Validate profileType
|
|
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
|
|
throw new HttpError(
|
|
HttpStatus.BAD_REQUEST,
|
|
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
|
|
);
|
|
}
|
|
|
|
const result = await this.keycloakAttributeService.clearOrgDnaAttributes(
|
|
profileIds,
|
|
profileType,
|
|
);
|
|
|
|
return new HttpSuccess({
|
|
message: "Clear org DNA attributes เสร็จสิ้น",
|
|
...result,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Batch sync all users (Admin only)
|
|
*
|
|
* @summary Batch sync all users to Keycloak without limit (ADMIN)
|
|
*
|
|
* @description Syncs profileId and orgRootDnaId to Keycloak for all users
|
|
* that have a keycloak ID. Uses parallel processing for better performance.
|
|
*
|
|
* Features:
|
|
* - Resume from checkpoint after failures (use resume=true)
|
|
* - Automatic retry with exponential backoff
|
|
* - Rate limiting to avoid overwhelming Keycloak
|
|
* - Progress tracking and persistence
|
|
*
|
|
* @param resume - Resume from last checkpoint (default: false)
|
|
* @param maxRetries - Maximum retry attempts for failed operations (default: 3)
|
|
* @param rateLimit - Requests per second rate limit (default: 10)
|
|
* @param clearProgress - Clear existing progress and start fresh (default: false)
|
|
*/
|
|
@Post("sync-all")
|
|
async syncAll(
|
|
@Query() resume: boolean = false,
|
|
@Query() maxRetries: number = 3,
|
|
@Query() rateLimit: number = 10,
|
|
@Query() clearProgress: boolean = false,
|
|
) {
|
|
const result = await this.keycloakAttributeService.batchSyncUsers({
|
|
resume,
|
|
maxRetries,
|
|
rateLimit,
|
|
clearProgress,
|
|
});
|
|
|
|
return new HttpSuccess({
|
|
message: "Batch sync เสร็จสิ้น",
|
|
total: result.total,
|
|
success: result.success,
|
|
failed: result.failed,
|
|
details: result.details,
|
|
resumed: result.resumed,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ensure Keycloak users exist for all profiles (Admin only)
|
|
*
|
|
* @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN)
|
|
*
|
|
* @description
|
|
* This endpoint will:
|
|
* - Create new Keycloak users for profiles without a keycloak ID
|
|
* - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak
|
|
* - Verify existing Keycloak users
|
|
* - Skip profiles without a citizenId
|
|
*/
|
|
@Post("ensure-users")
|
|
async ensureAllUsers() {
|
|
const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers();
|
|
return new HttpSuccess({
|
|
message: "Batch ensure Keycloak users เสร็จสิ้น",
|
|
...result,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear orphaned Keycloak users (Admin only)
|
|
*
|
|
* @summary Delete Keycloak users that are not in the database (ADMIN)
|
|
*
|
|
* @description
|
|
* This endpoint will:
|
|
* - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables
|
|
* - Delete those orphaned users from Keycloak
|
|
* - Skip protected users (super_admin, admin_issue)
|
|
*
|
|
* @param {request} request Request body containing skipUsernames array
|
|
*/
|
|
@Post("clear-orphaned-users")
|
|
async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) {
|
|
const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"];
|
|
const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames);
|
|
return new HttpSuccess({
|
|
message: "Clear orphaned Keycloak users เสร็จสิ้น",
|
|
...result,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sync profiles with missing empType for a specific month (Admin only)
|
|
*
|
|
* @summary Find profiles updated in specified month with missing empType in Keycloak and sync them (ADMIN)
|
|
*
|
|
* @description
|
|
* This endpoint will:
|
|
* - List profiles from Profile table where lastUpdatedAt falls within the specified month
|
|
* - For each profile, check Keycloak if empType attribute is empty/null
|
|
* - If empType is empty, sync the profile using existing sync logic
|
|
* - Return summary of sync results
|
|
*
|
|
* Features:
|
|
* - Dry run mode (dryRun=true) to check without syncing
|
|
* - Configurable concurrency for parallel processing
|
|
* - Rate limiting to avoid overwhelming Keycloak
|
|
* - Detailed error reporting
|
|
* - Idempotent (can be safely re-run)
|
|
*
|
|
* @param {request} request Request body containing month parameter
|
|
* @param dryRun - If true, only check without syncing (default: false)
|
|
* @param concurrency - Number of parallel operations (default: 5)
|
|
* @param rateLimit - Requests per second limit (default: 10)
|
|
*/
|
|
@Post("sync-missing-emptype")
|
|
@Response<HttpError>(HttpStatus.BAD_REQUEST, "Invalid month format")
|
|
@Response<HttpError>(HttpStatus.INTERNAL_SERVER_ERROR, "Sync operation failed")
|
|
async syncMissingEmpType(
|
|
@Body() request: {
|
|
month: string;
|
|
profileType?: "PROFILE" | "PROFILE_EMPLOYEE";
|
|
},
|
|
@Query() dryRun: boolean = false,
|
|
@Query() concurrency: number = 5,
|
|
@Query() rateLimit: number = 10,
|
|
) {
|
|
const { month, profileType = "PROFILE" } = request;
|
|
|
|
// Validate month format (YYYY-MM)
|
|
const monthRegex = /^\d{4}-\d{2}$/;
|
|
if (!monthRegex.test(month)) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "รูปแบบเดือนไม่ถูกต้อง ต้องเป็น YYYY-MM");
|
|
}
|
|
|
|
// Validate profileType
|
|
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
|
|
throw new HttpError(
|
|
HttpStatus.BAD_REQUEST,
|
|
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
|
|
);
|
|
}
|
|
|
|
// Validate concurrency
|
|
if (concurrency < 1 || concurrency > 20) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "concurrency ต้องอยู่ระหว่าง 1 ถึง 20");
|
|
}
|
|
|
|
// Validate rateLimit
|
|
if (rateLimit < 1 || rateLimit > 50) {
|
|
throw new HttpError(HttpStatus.BAD_REQUEST, "rateLimit ต้องอยู่ระหว่าง 1 ถึง 50");
|
|
}
|
|
|
|
// Execute sync
|
|
const result = await this.keycloakAttributeService.syncMissingEmpTypeByMonth({
|
|
month,
|
|
profileType,
|
|
dryRun,
|
|
concurrency,
|
|
rateLimit,
|
|
});
|
|
|
|
return new HttpSuccess({
|
|
message: `Sync ${dryRun ? "check " : ""}เสร็จสิ้น`,
|
|
...result,
|
|
});
|
|
}
|
|
}
|