แก้ปัญหา password inconsistency ระหว่างการสร้าง user ใหม่กับ reset-password endpoint โดยใช้ birthDate จาก profile ที่ save แล้วแทน input data
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m0s
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m0s
This commit is contained in:
parent
af88dffbbe
commit
cd057bf81d
2 changed files with 246 additions and 136 deletions
204
src/keycloak/user-operations.ts
Normal file
204
src/keycloak/user-operations.ts
Normal file
|
|
@ -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<string> {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
|
@ -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:",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue