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, }); } }