import { Controller, Post, Get, Route, Security, Tags, Path, Request, Response, Query } 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"; import { AppDataSource } from "../database/data-source"; import { Profile } from "../entities/Profile"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @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(); private profileRepo = AppDataSource.getRepository(Profile); private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); /** * 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 all users (Admin only) * * @summary Batch sync all users to Keycloak (ADMIN) * * @param {number} limit Maximum number of users to sync */ @Post("sync-all") async syncAll(@Query() limit: number = 100) { if (limit > 500) { throw new HttpError(HttpStatus.BAD_REQUEST, "limit ต้องไม่เกิน 500"); } const result = await this.keycloakAttributeService.batchSyncUsers(limit); return new HttpSuccess({ message: "Batch sync เสร็จสิ้น", total: result.total, success: result.success, failed: result.failed, details: result.details, }); } /** * ตรวจสอบสถานะ Keycloak Mapper * * @summary ตรวจสอบว่า profileId และ rootDnaId ออกมาใน token หรือไม่ */ @Get("check-mapper") async checkMapperStatus(@Request() request: RequestWithUser) { const keycloakUserId = request.user.sub; if (!keycloakUserId) { throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); } // 1. ตรวจสอบ attributes ใน Keycloak const kcAttrs = await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); // 2. ตรวจสอบ attributes ใน Database const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); // 3. ตรวจสอบ token payload ปัจจุบัน const tokenPayload = request.user; return new HttpSuccess({ keycloakAttributes: kcAttrs, databaseAttributes: dbAttrs, tokenHasProfileId: !!tokenPayload.profileId, tokenHasOrgRootDnaId: !!tokenPayload.orgRootDnaId, tokenScopes: tokenPayload.scope?.split(" ") || [], diagnosis: { kcHasProfileId: !!kcAttrs?.profileId, kcHasOrgRootDnaId: !!kcAttrs?.orgRootDnaId, kcHasOrgChild1DnaId: !!kcAttrs?.orgChild1DnaId, kcHasOrgChild2DnaId: !!kcAttrs?.orgChild2DnaId, kcHasOrgChild3DnaId: !!kcAttrs?.orgChild3DnaId, kcHasOrgChild4DnaId: !!kcAttrs?.orgChild4DnaId, kcHasEmpType: !!kcAttrs?.empType, dbHasProfileId: !!dbAttrs?.profileId, dbHasOrgRootDnaId: !!dbAttrs?.orgRootDnaId, dbHasOrgChild1DnaId: !!dbAttrs?.orgChild1DnaId, dbHasOrgChild2DnaId: !!dbAttrs?.orgChild2DnaId, dbHasOrgChild3DnaId: !!dbAttrs?.orgChild3DnaId, dbHasOrgChild4DnaId: !!dbAttrs?.orgChild4DnaId, dbHasEmpType: !!dbAttrs?.empType, issue: !tokenPayload.profileId && kcAttrs?.profileId ? "Attribute มีใน Keycloak แต่ไม่ออกมาใน token - แก้ไข Mapper หรือ Client Scope" : !kcAttrs?.profileId && dbAttrs?.profileId ? "Attribute มีใน Database แต่ไม่มีใน Keycloak - ต้อง sync ซ้ำ" : !dbAttrs?.profileId ? "ไม่พบ profile ใน database - ตรวจสอบ keycloak field" : "ทุกอย่างปกติ", }, }); } }