From cd057bf81d9fed3bc738dac757e5eee824a7a844 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 14 Jul 2026 11:41:04 +0700 Subject: [PATCH] =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B8=9B=E0=B8=B1?= =?UTF-8?q?=E0=B8=8D=E0=B8=AB=E0=B8=B2=20password=20inconsistency=20?= =?UTF-8?q?=E0=B8=A3=E0=B8=B0=E0=B8=AB=E0=B8=A7=E0=B9=88=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=AA=E0=B8=A3=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=87=20user=20=E0=B9=83=E0=B8=AB=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=81=E0=B8=B1=E0=B8=9A=20reset-password=20endpoint=20?= =?UTF-8?q?=E0=B9=82=E0=B8=94=E0=B8=A2=E0=B9=83=E0=B8=8A=E0=B9=89=20birthD?= =?UTF-8?q?ate=20=E0=B8=88=E0=B8=B2=E0=B8=81=20profile=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=20save=20=E0=B9=81=E0=B8=A5=E0=B9=89?= =?UTF-8?q?=E0=B8=A7=E0=B9=81=E0=B8=97=E0=B8=99=20input=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/keycloak/user-operations.ts | 204 +++++++++++++++++++ src/services/ExecuteOfficerProfileService.ts | 178 ++++------------ 2 files changed, 246 insertions(+), 136 deletions(-) create mode 100644 src/keycloak/user-operations.ts diff --git a/src/keycloak/user-operations.ts b/src/keycloak/user-operations.ts new file mode 100644 index 00000000..e6f50142 --- /dev/null +++ b/src/keycloak/user-operations.ts @@ -0,0 +1,204 @@ +/** + * Helper functions for Keycloak user operations + * + * สร้างเพื่อแยก Keycloak operations ออกจาก DB transaction + * และใช้ profile data ที่ save แล้วแทน input data + */ + +import { + createUser, + addUserRoles, + removeUserRoles, + updateUserAttributes, + getUserByUsername, + getRoleMappings, +} from "./index"; +import { Profile } from "../entities/Profile"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; + +/** + * สร้าง password จาก profile.birthDate เหมือน reset-password endpoint + * + * @param profile - Profile object ที่มี birthDate + * @returns password string ในรูปแบบ ddmmyyyy (เช่น "31122563") + * + * Reference: UserController.ts reset-password (บรรทัด 867-876) + */ +export function generatePasswordFromProfile(profile: Profile): string { + if (!profile.birthDate) { + throw new Error("Profile birthDate is required for password generation"); + } + + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = ( + new Date(profile.birthDate.toDateString()).getMonth() + 1 + ) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + + return `${_date}${_month}${_year}`; +} + +/** + * Sync Keycloak user สำหรับ profile ที่ save แล้ว + * + * รวมทุก Keycloak operations: + * - ตรวจสอบว่ามี user อยู่แล้วหรือไม่ + * - สร้าง user ใหม่ (ถ้ายังไม่มี) ด้วย password จาก profile.birthDate + * - กำหนด role USER + * - อัปเดต attributes (profileId, prefix) + * + * @param profile - Profile object ที่ถูก save แล้ว (ต้องมี id และ birthDate) + * @param roleList - List of roles จาก Keycloak (ผลลัพธ์จาก getRoles()) + * @returns userKeycloakId string หรือ throws HttpError + * + * @throws HttpError ถ้า createUser หรือ role operations ล้มเหลว + */ +export async function syncKeycloakForProfile( + profile: Profile, + roleList: any[], +): Promise { + console.log( + `[syncKeycloakForProfile] Starting for citizenId: ${profile.citizenId}, profileId: ${profile.id}`, + ); + + if (!profile.id) { + throw new Error("Profile ID is required for Keycloak sync"); + } + + // ตรวจสอบว่ามี user อยู่แล้วหรือไม่ + const checkUser = await getUserByUsername(profile.citizenId); + console.log( + `[syncKeycloakForProfile] Keycloak user exists: ${checkUser.length > 0}`, + ); + + let userKeycloakId: string; + + if (checkUser.length === 0) { + // สร้าง user ใหม่ + console.log( + `[syncKeycloakForProfile] Creating new Keycloak user for citizenId: ${profile.citizenId}`, + ); + + // สร้าง password จาก profile.birthDate (NOT input birthDate) + const password = generatePasswordFromProfile(profile); + + console.log( + `[syncKeycloakForProfile] Generated password from profile.birthDate: ${profile.birthDate}`, + ); + + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + + console.log( + `[syncKeycloakForProfile] Creating user - firstName: ${sanitizedFirstName}, lastName: ${profile.lastName}`, + ); + + const createUserResult: any = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName ?? "", + }); + + // ตรวจสอบ createUser error + if ( + createUserResult && + typeof createUserResult === "object" && + createUserResult.errorMessage + ) { + console.error( + `[syncKeycloakForProfile] createUser FAILED - field: ${createUserResult.field}, errorMessage: ${createUserResult.errorMessage}`, + ); + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + `Keycloak validation failed: ${createUserResult.field} - ${createUserResult.errorMessage}`, + ); + } + + userKeycloakId = createUserResult; + + console.log( + `[syncKeycloakForProfile] User created successfully, userKeycloakId: ${userKeycloakId}`, + ); + } else { + // ใช้ user ที่มีอยู่แล้ว + console.log( + `[syncKeycloakForProfile] Using existing Keycloak user, userKeycloakId: ${checkUser[0].id}`, + ); + userKeycloakId = checkUser[0].id; + + // ลบ roles เดิมแล้วกำหนด role USER ใหม่ + const rolesData = await getRoleMappings(userKeycloakId); + if (rolesData) { + const _delRole = rolesData.map((x: any) => ({ + id: x.id, + name: x.name, + })); + console.log( + `[syncKeycloakForProfile] Removing old roles: ${_delRole.length}`, + ); + await removeUserRoles(userKeycloakId, _delRole); + } + } + + // กำหนด role USER + console.log(`[syncKeycloakForProfile] Assigning USER role`); + const result = await addUserRoles( + userKeycloakId, + roleList + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + + console.log(`[syncKeycloakForProfile] USER role assigned, result: ${result}`); + + // อัปเดต attributes + console.log(`[syncKeycloakForProfile] Updating user attributes`); + await updateUserAttributes(userKeycloakId, { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + + console.log( + `[syncKeycloakForProfile] Completed successfully for citizenId: ${profile.citizenId}`, + ); + + return userKeycloakId; +} + +/** + * จัดการ error จาก Keycloak operations + * + * @param error - Error object จาก Keycloak operations + * @param context - Context object { citizenId, profileId, operation } + * + * @returns HttpError ที่ formatted สำหรับ throw กลับไป + */ +export function handleKeycloakError( + error: any, + context: { + citizenId?: string; + profileId?: string; + operation: string; + }, +): HttpError { + console.error( + `[KeycloakError] ${context.operation} failed for citizenId: ${context.citizenId}, profileId: ${context.profileId}`, + error, + ); + + const errorMessage = + error instanceof Error ? error.message : "Unknown Keycloak error"; + + return new HttpError( + HttpStatusCode.INTERNAL_SERVER_ERROR, + `Keycloak ${context.operation} failed: ${errorMessage}. Profile created but Keycloak sync failed - manual fix required.`, + ); +} diff --git a/src/services/ExecuteOfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts index f06159dc..855863c4 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -38,15 +38,11 @@ import { removeProfileInOrganize, setLogDataDiff, } from "../interfaces/utils"; +import { getRoles } from "../keycloak"; import { - addUserRoles, - createUser, - getRoleMappings, - getRoles, - getUserByUsername, - removeUserRoles, - updateUserAttributes, -} from "../keycloak"; + syncKeycloakForProfile, + handleKeycloakError, +} from "../keycloak/user-operations"; import { CreatePosMasterHistoryOfficer } from "./PositionService"; import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; import CallAPI from "../interfaces/call-api"; @@ -141,8 +137,8 @@ export interface ExecutionContext { * ถ้าทุกคนสำเร็จจะ return result รายงาน success count * * ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes) - * ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ - * ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว + * ทำหลังจาก DB transaction เพื่อใช้ profile.birthDate ที่ save แล้ว + * Keycloak ไม่สามารถ rollback ได้ - ถ้า sync ล้มเหลว profile จะถูกสร้างแต่ไม่มี Keycloak user * * Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ * ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน @@ -485,117 +481,6 @@ export class ExecuteOfficerProfileService { ? _null : calculateRetireLaw(item.bodyProfile.birthDate); - let userKeycloakId: any; - let result: any; - console.log( - "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", - item.bodyProfile.citizenId, - ); - const checkUser = await getUserByUsername(item.bodyProfile.citizenId); - console.log( - "[ExecuteOfficerProfileService] Keycloak user exists:", - checkUser.length > 0, - ); - if (checkUser.length == 0) { - console.log("[ExecuteOfficerProfileService] Creating new Keycloak user"); - let password = item.bodyProfile.citizenId; - if (item.bodyProfile.birthDate != null) { - const _date = new Date(item.bodyProfile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = ( - new Date(item.bodyProfile.birthDate.toDateString()).getMonth() + 1 - ) - .toString() - .padStart(2, "0"); - const _year = new Date(item.bodyProfile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - console.log( - "[ExecuteOfficerProfileService] Calling createUser for:", - item.bodyProfile.citizenId, - ); - console.log( - "[ExecuteOfficerProfileService] createUser data - firstName:", - item.bodyProfile.firstName, - "lastName:", - item.bodyProfile.lastName, - ); - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak (ป้องกัน . หรืออักขระอื่นๆ) - const sanitizedFirstName = item.bodyProfile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(item.bodyProfile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: item.bodyProfile.lastName, - }); - if ( - userKeycloakId && - typeof userKeycloakId === "object" && - userKeycloakId.errorMessage - ) { - console.error( - "[ExecuteOfficerProfileService] createUser FAILED - field:", - userKeycloakId.field, - "errorMessage:", - userKeycloakId.errorMessage, - "params:", - userKeycloakId.params, - ); - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`, - ); - } - console.log( - "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", - userKeycloakId, - ); - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to new user, result:", - result, - ); - } else { - console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); - userKeycloakId = checkUser[0].id; - console.log( - "[ExecuteOfficerProfileService] Existing userKeycloakId:", - userKeycloakId, - ); - const rolesData = await getRoleMappings(userKeycloakId); - if (rolesData) { - const _delRole = rolesData.map((x: any) => ({ - id: x.id, - name: x.name, - })); - console.log( - "[ExecuteOfficerProfileService] Removing old roles:", - _delRole.length, - ); - await removeUserRoles(userKeycloakId, _delRole); - } - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to existing user", - ); - } - let profile: any = await profileRepository.findOne({ where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], @@ -673,9 +558,8 @@ export class ExecuteOfficerProfileService { profile = Object.assign({ ...item.bodyProfile, ...meta }); profile.dateRetire = _dateRetire; profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว profile.registrationAddress = item.bodyProfile.registrationAddress; profile.registrationProvinceId = registrationProvinceId ? registrationProvinceId.id @@ -718,12 +602,6 @@ export class ExecuteOfficerProfileService { "[ExecuteOfficerProfileService] New profile saved, profileId:", profile.id, ); - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); setLogDataDiff(req, { before, after: profile }); } //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ @@ -753,9 +631,8 @@ export class ExecuteOfficerProfileService { profile = Object.assign({ ...item.bodyProfile, ...meta }); profile.dateRetire = _dateRetire; profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว profile.registrationAddress = item.bodyProfile.registrationAddress; profile.registrationProvinceId = registrationProvinceId ? registrationProvinceId.id @@ -800,9 +677,8 @@ export class ExecuteOfficerProfileService { console.log( "[ExecuteOfficerProfileService] Updating existing active profile", ); - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว profile.isProbation = item.bodyProfile.isProbation; profile.isLeave = item.bodyProfile.isLeave; profile.isRetirement = false; @@ -894,6 +770,36 @@ export class ExecuteOfficerProfileService { } } + // ───────────────────────────────────────────────────────────── + // Keycloak Operations - ทำหลังจาก profile save สำเร็จแล้ว + // ใช้ profile.birthDate ที่ save แล้ว เพื่อให้ตรงกับ reset-password endpoint + // ───────────────────────────────────────────────────────────── + if (profile && profile.id && !profile.keycloak) { + console.log( + "[ExecuteOfficerProfileService] Syncing Keycloak user for profileId:", + profile.id, + ); + try { + const userKeycloakId = await syncKeycloakForProfile(profile, list); + profile.keycloak = userKeycloakId; + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] Keycloak sync completed, profile.keycloak updated:", + profile.keycloak, + ); + } catch (error) { + console.error( + `[ExecuteOfficerProfileService] Keycloak sync failed for profileId: ${profile.id}, citizenId: ${profile.citizenId}`, + error, + ); + throw handleKeycloakError(error, { + citizenId: profile.citizenId, + profileId: profile.id, + operation: "sync", + }); + } + } + if (profile && profile.id) { console.log( "[ExecuteOfficerProfileService] Processing additional data for profileId:",