hrms-api-org/src/controllers/KeycloakSyncController.ts

254 lines
8.2 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,
});
}
/**
* 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.
*/
// @Post("sync-all")
async syncAll() {
const result = await this.keycloakAttributeService.batchSyncUsers();
return new HttpSuccess({
message: "Batch sync เสร็จสิ้น",
total: result.total,
success: result.success,
failed: result.failed,
details: result.details,
});
}
/**
* 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,
});
}
}