254 lines
8.2 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|