setup auth middleware and sync code
This commit is contained in:
parent
e76e361981
commit
a487b73c3b
5 changed files with 684 additions and 0 deletions
198
src/controllers/KeycloakSyncController.ts
Normal file
198
src/controllers/KeycloakSyncController.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
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"
|
||||||
|
: "ทุกอย่างปกติ",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -772,6 +772,70 @@ export async function changeUserPassword(userId: string, newPassword: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user attributes in Keycloak
|
||||||
|
*
|
||||||
|
* @param userId - Keycloak user ID
|
||||||
|
* @param attributes - Object containing attribute names and their values (as arrays)
|
||||||
|
* @returns true if success, false otherwise
|
||||||
|
*/
|
||||||
|
export async function updateUserAttributes(
|
||||||
|
userId: string,
|
||||||
|
attributes: Record<string, string[]>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Get existing user data to preserve other attributes
|
||||||
|
const existingUser = await getUser(userId);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
console.error(`User ${userId} not found in Keycloak`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge existing attributes with new attributes
|
||||||
|
// Keycloak requires id to be present in the payload
|
||||||
|
const updatedAttributes = {
|
||||||
|
id: existingUser.id,
|
||||||
|
enabled: existingUser.enabled ?? true,
|
||||||
|
attributes: {
|
||||||
|
...(existingUser.attributes || {}),
|
||||||
|
...attributes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[updateUserAttributes] Sending to Keycloak:`, JSON.stringify(updatedAttributes, null, 2));
|
||||||
|
|
||||||
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await getToken()}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(updatedAttributes),
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(`[updateUserAttributes] Network error:`, e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
console.error(`[updateUserAttributes] No response from Keycloak`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to reset password
|
// Function to reset password
|
||||||
export async function resetPassword(username: string) {
|
export async function resetPassword(username: string) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@ export async function expressAuthentication(
|
||||||
request.app.locals.logData.userName = payload.name;
|
request.app.locals.logData.userName = payload.name;
|
||||||
request.app.locals.logData.user = payload.preferred_username;
|
request.app.locals.logData.user = payload.preferred_username;
|
||||||
|
|
||||||
|
// เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี)
|
||||||
|
request.app.locals.logData.profileId = payload.profileId ?? "";
|
||||||
|
request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? "";
|
||||||
|
request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? "";
|
||||||
|
request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? "";
|
||||||
|
request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? "";
|
||||||
|
request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? "";
|
||||||
|
request.app.locals.logData.empType = payload.empType ?? "";
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@ export type RequestWithUser = Request & {
|
||||||
preferred_username: string;
|
preferred_username: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string[];
|
role: string[];
|
||||||
|
profileId?: string;
|
||||||
|
orgRootDnaId?: string;
|
||||||
|
orgChild1DnaId?: string;
|
||||||
|
orgChild2DnaId?: string;
|
||||||
|
orgChild3DnaId?: string;
|
||||||
|
orgChild4DnaId?: string;
|
||||||
|
empType?: string;
|
||||||
|
scope?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
405
src/services/KeycloakAttributeService.ts
Normal file
405
src/services/KeycloakAttributeService.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
import { AppDataSource } from "../database/data-source";
|
||||||
|
import { Profile } from "../entities/Profile";
|
||||||
|
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
||||||
|
// import { PosMaster } from "../entities/PosMaster";
|
||||||
|
// import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||||
|
// import { OrgRoot } from "../entities/OrgRoot";
|
||||||
|
import { getUser, updateUserAttributes } from "../keycloak";
|
||||||
|
import { OrgRevision } from "../entities/OrgRevision";
|
||||||
|
|
||||||
|
export interface UserProfileAttributes {
|
||||||
|
profileId: string | null;
|
||||||
|
orgRootDnaId: string | null;
|
||||||
|
orgChild1DnaId: string | null;
|
||||||
|
orgChild2DnaId: string | null;
|
||||||
|
orgChild3DnaId: string | null;
|
||||||
|
orgChild4DnaId: string | null;
|
||||||
|
empType: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak Attribute Service
|
||||||
|
* Service for syncing profileId and orgRootDnaId to Keycloak user attributes
|
||||||
|
*/
|
||||||
|
export class KeycloakAttributeService {
|
||||||
|
private profileRepo = AppDataSource.getRepository(Profile);
|
||||||
|
private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
|
||||||
|
// private posMasterRepo = AppDataSource.getRepository(PosMaster);
|
||||||
|
// private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster);
|
||||||
|
// private orgRootRepo = AppDataSource.getRepository(OrgRoot);
|
||||||
|
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get profile attributes (profileId and orgRootDnaId) from database
|
||||||
|
* Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง)
|
||||||
|
*
|
||||||
|
* @param keycloakUserId - Keycloak user ID
|
||||||
|
* @returns UserProfileAttributes with profileId and orgRootDnaId
|
||||||
|
*/
|
||||||
|
async getUserProfileAttributes(keycloakUserId: string): Promise<UserProfileAttributes> {
|
||||||
|
// First, try to find in Profile (ข้าราชการ)
|
||||||
|
const revisionCurrent = await this.orgRevisionRepo.findOne({
|
||||||
|
where: { orgRevisionIsCurrent: true },
|
||||||
|
});
|
||||||
|
const revisionId = revisionCurrent ? revisionCurrent.id : null;
|
||||||
|
const profileResult = await this.profileRepo
|
||||||
|
.createQueryBuilder("p")
|
||||||
|
.leftJoinAndSelect("p.current_holders", "pm")
|
||||||
|
.leftJoinAndSelect("pm.orgRoot", "orgRoot")
|
||||||
|
.leftJoinAndSelect("pm.orgChild1", "orgChild1")
|
||||||
|
.leftJoinAndSelect("pm.orgChild2", "orgChild2")
|
||||||
|
.leftJoinAndSelect("pm.orgChild3", "orgChild3")
|
||||||
|
.leftJoinAndSelect("pm.orgChild4", "orgChild4")
|
||||||
|
.where("p.keycloak = :keycloakUserId", { keycloakUserId })
|
||||||
|
.andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (
|
||||||
|
profileResult &&
|
||||||
|
profileResult.current_holders &&
|
||||||
|
profileResult.current_holders.length > 0
|
||||||
|
) {
|
||||||
|
const currentPos = profileResult.current_holders[0];
|
||||||
|
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
|
||||||
|
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
|
||||||
|
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
|
||||||
|
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
|
||||||
|
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
|
||||||
|
return {
|
||||||
|
profileId: profileResult.id,
|
||||||
|
orgRootDnaId,
|
||||||
|
orgChild1DnaId,
|
||||||
|
orgChild2DnaId,
|
||||||
|
orgChild3DnaId,
|
||||||
|
orgChild4DnaId,
|
||||||
|
empType: "OFFICER",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in Profile, try ProfileEmployee (ลูกจ้าง)
|
||||||
|
const profileEmployeeResult = await this.profileEmployeeRepo
|
||||||
|
.createQueryBuilder("pe")
|
||||||
|
.leftJoinAndSelect("pe.current_holders", "epm")
|
||||||
|
.leftJoinAndSelect("epm.orgRoot", "org")
|
||||||
|
.leftJoinAndSelect("epm.orgChild1", "orgChild1")
|
||||||
|
.leftJoinAndSelect("epm.orgChild2", "orgChild2")
|
||||||
|
.leftJoinAndSelect("epm.orgChild3", "orgChild3")
|
||||||
|
.leftJoinAndSelect("epm.orgChild4", "orgChild4")
|
||||||
|
.where("pe.keycloak = :keycloakUserId", { keycloakUserId })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (
|
||||||
|
profileEmployeeResult &&
|
||||||
|
profileEmployeeResult.current_holders &&
|
||||||
|
profileEmployeeResult.current_holders.length > 0
|
||||||
|
) {
|
||||||
|
const currentPos = profileEmployeeResult.current_holders[0];
|
||||||
|
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
|
||||||
|
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
|
||||||
|
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
|
||||||
|
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
|
||||||
|
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
|
||||||
|
return {
|
||||||
|
profileId: profileEmployeeResult.id,
|
||||||
|
orgRootDnaId,
|
||||||
|
orgChild1DnaId,
|
||||||
|
orgChild2DnaId,
|
||||||
|
orgChild3DnaId,
|
||||||
|
orgChild4DnaId,
|
||||||
|
empType: profileEmployeeResult.employeeClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null values if no profile found
|
||||||
|
return {
|
||||||
|
profileId: null,
|
||||||
|
orgRootDnaId: null,
|
||||||
|
orgChild1DnaId: null,
|
||||||
|
orgChild2DnaId: null,
|
||||||
|
orgChild3DnaId: null,
|
||||||
|
orgChild4DnaId: null,
|
||||||
|
empType: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get profile attributes by profile ID directly
|
||||||
|
* Used for syncing specific profiles
|
||||||
|
*
|
||||||
|
* @param profileId - Profile ID
|
||||||
|
* @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง
|
||||||
|
* @returns UserProfileAttributes with profileId and orgRootDnaId
|
||||||
|
*/
|
||||||
|
async getAttributesByProfileId(
|
||||||
|
profileId: string,
|
||||||
|
profileType: "PROFILE" | "PROFILE_EMPLOYEE",
|
||||||
|
): Promise<UserProfileAttributes> {
|
||||||
|
const revisionCurrent = await this.orgRevisionRepo.findOne({
|
||||||
|
where: { orgRevisionIsCurrent: true },
|
||||||
|
});
|
||||||
|
const revisionId = revisionCurrent ? revisionCurrent.id : null;
|
||||||
|
if (profileType === "PROFILE") {
|
||||||
|
const profileResult = await this.profileRepo
|
||||||
|
.createQueryBuilder("p")
|
||||||
|
.leftJoinAndSelect("p.current_holders", "pm")
|
||||||
|
.leftJoinAndSelect("pm.orgRoot", "orgRoot")
|
||||||
|
.leftJoinAndSelect("pm.orgChild1", "orgChild1")
|
||||||
|
.leftJoinAndSelect("pm.orgChild2", "orgChild2")
|
||||||
|
.leftJoinAndSelect("pm.orgChild3", "orgChild3")
|
||||||
|
.leftJoinAndSelect("pm.orgChild4", "orgChild4")
|
||||||
|
.where("p.id = :profileId", { profileId })
|
||||||
|
.andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (
|
||||||
|
profileResult &&
|
||||||
|
profileResult.current_holders &&
|
||||||
|
profileResult.current_holders.length > 0
|
||||||
|
) {
|
||||||
|
const currentPos = profileResult.current_holders[0];
|
||||||
|
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
|
||||||
|
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
|
||||||
|
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
|
||||||
|
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
|
||||||
|
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
|
||||||
|
return {
|
||||||
|
profileId: profileResult.id,
|
||||||
|
orgRootDnaId,
|
||||||
|
orgChild1DnaId,
|
||||||
|
orgChild2DnaId,
|
||||||
|
orgChild3DnaId,
|
||||||
|
orgChild4DnaId,
|
||||||
|
empType: "OFFICER",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const profileEmployeeResult = await this.profileEmployeeRepo
|
||||||
|
.createQueryBuilder("pe")
|
||||||
|
.leftJoinAndSelect("pe.current_holders", "epm")
|
||||||
|
.leftJoinAndSelect("epm.orgRoot", "org")
|
||||||
|
.leftJoinAndSelect("pm.orgChild1", "orgChild1")
|
||||||
|
.leftJoinAndSelect("pm.orgChild2", "orgChild2")
|
||||||
|
.leftJoinAndSelect("pm.orgChild3", "orgChild3")
|
||||||
|
.leftJoinAndSelect("pm.orgChild4", "orgChild4")
|
||||||
|
.where("pe.id = :profileId", { profileId })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (
|
||||||
|
profileEmployeeResult &&
|
||||||
|
profileEmployeeResult.current_holders &&
|
||||||
|
profileEmployeeResult.current_holders.length > 0
|
||||||
|
) {
|
||||||
|
const currentPos = profileEmployeeResult.current_holders[0];
|
||||||
|
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
|
||||||
|
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
|
||||||
|
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
|
||||||
|
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
|
||||||
|
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
profileId: profileEmployeeResult.id,
|
||||||
|
orgRootDnaId,
|
||||||
|
orgChild1DnaId,
|
||||||
|
orgChild2DnaId,
|
||||||
|
orgChild3DnaId,
|
||||||
|
orgChild4DnaId,
|
||||||
|
empType: profileEmployeeResult.employeeClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
profileId: null,
|
||||||
|
orgRootDnaId: null,
|
||||||
|
orgChild1DnaId: null,
|
||||||
|
orgChild2DnaId: null,
|
||||||
|
orgChild3DnaId: null,
|
||||||
|
orgChild4DnaId: null,
|
||||||
|
empType: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync user attributes to Keycloak
|
||||||
|
*
|
||||||
|
* @param keycloakUserId - Keycloak user ID
|
||||||
|
* @returns true if sync successful, false otherwise
|
||||||
|
*/
|
||||||
|
async syncUserAttributes(keycloakUserId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const attributes = await this.getUserProfileAttributes(keycloakUserId);
|
||||||
|
|
||||||
|
if (!attributes.profileId) {
|
||||||
|
console.log(`No profile found for Keycloak user ${keycloakUserId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare attributes for Keycloak (must be arrays)
|
||||||
|
const keycloakAttributes: Record<string, string[]> = {
|
||||||
|
profileId: [attributes.profileId],
|
||||||
|
orgRootDnaId: [attributes.orgRootDnaId || ""],
|
||||||
|
orgChild1DnaId: [attributes.orgChild1DnaId || ""],
|
||||||
|
orgChild2DnaId: [attributes.orgChild2DnaId || ""],
|
||||||
|
orgChild3DnaId: [attributes.orgChild3DnaId || ""],
|
||||||
|
orgChild4DnaId: [attributes.orgChild4DnaId || ""],
|
||||||
|
empType: [attributes.empType || ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = await updateUserAttributes(keycloakUserId, keycloakAttributes);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync attributes when organization changes
|
||||||
|
* This is called when a user moves to a different organization
|
||||||
|
*
|
||||||
|
* @param profileId - Profile ID
|
||||||
|
* @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง
|
||||||
|
* @returns true if sync successful, false otherwise
|
||||||
|
*/
|
||||||
|
async syncOnOrganizationChange(
|
||||||
|
profileId: string,
|
||||||
|
profileType: "PROFILE" | "PROFILE_EMPLOYEE",
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Get the keycloak userId from the profile
|
||||||
|
let keycloakUserId: string | null = null;
|
||||||
|
|
||||||
|
if (profileType === "PROFILE") {
|
||||||
|
const profile = await this.profileRepo.findOne({ where: { id: profileId } });
|
||||||
|
keycloakUserId = profile?.keycloak || "";
|
||||||
|
} else {
|
||||||
|
const profileEmployee = await this.profileEmployeeRepo.findOne({
|
||||||
|
where: { id: profileId },
|
||||||
|
});
|
||||||
|
keycloakUserId = profileEmployee?.keycloak || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keycloakUserId) {
|
||||||
|
console.log(`No Keycloak user ID found for profile ${profileId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.syncUserAttributes(keycloakUserId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error syncing organization change for profile ${profileId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch sync multiple users
|
||||||
|
* Useful for initial sync or periodic updates
|
||||||
|
*
|
||||||
|
* @param limit - Maximum number of users to sync (default: 100)
|
||||||
|
* @returns Object with success count and details
|
||||||
|
*/
|
||||||
|
async batchSyncUsers(
|
||||||
|
limit: number = 100,
|
||||||
|
): Promise<{ total: number; success: number; failed: number; details: any[] }> {
|
||||||
|
const result = {
|
||||||
|
total: 0,
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
details: [] as any[],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get profiles with keycloak IDs (ข้าราชการ)
|
||||||
|
const profiles = await this.profileRepo
|
||||||
|
.createQueryBuilder("p")
|
||||||
|
.where("p.keycloak IS NOT NULL")
|
||||||
|
.andWhere("p.keycloak != :empty", { empty: "" })
|
||||||
|
.take(limit)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
// Get profileEmployees with keycloak IDs (ลูกจ้าง)
|
||||||
|
const profileEmployees = await this.profileEmployeeRepo
|
||||||
|
.createQueryBuilder("pe")
|
||||||
|
.where("pe.keycloak IS NOT NULL")
|
||||||
|
.andWhere("pe.keycloak != :empty", { empty: "" })
|
||||||
|
.take(limit)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const allProfiles = [...profiles, ...profileEmployees];
|
||||||
|
result.total = allProfiles.length;
|
||||||
|
|
||||||
|
for (const profile of allProfiles) {
|
||||||
|
const keycloakUserId = profile.keycloak;
|
||||||
|
const profileType = profile instanceof Profile ? "PROFILE" : "PROFILE_EMPLOYEE";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await this.syncOnOrganizationChange(profile.id, profileType);
|
||||||
|
if (success) {
|
||||||
|
result.success++;
|
||||||
|
result.details.push({
|
||||||
|
profileId: profile.id,
|
||||||
|
keycloakUserId,
|
||||||
|
status: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.failed++;
|
||||||
|
result.details.push({
|
||||||
|
profileId: profile.id,
|
||||||
|
keycloakUserId,
|
||||||
|
status: "failed",
|
||||||
|
error: "Sync returned false",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
result.failed++;
|
||||||
|
result.details.push({
|
||||||
|
profileId: profile.id,
|
||||||
|
keycloakUserId,
|
||||||
|
status: "error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in batch sync:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current Keycloak attributes for a user
|
||||||
|
*
|
||||||
|
* @param keycloakUserId - Keycloak user ID
|
||||||
|
* @returns Current attributes from Keycloak
|
||||||
|
*/
|
||||||
|
async getCurrentKeycloakAttributes(
|
||||||
|
keycloakUserId: string,
|
||||||
|
): Promise<UserProfileAttributes | null> {
|
||||||
|
try {
|
||||||
|
const user = await getUser(keycloakUserId);
|
||||||
|
|
||||||
|
if (!user || !user.attributes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
profileId: user.attributes.profileId?.[0] || "",
|
||||||
|
orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "",
|
||||||
|
orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "",
|
||||||
|
orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "",
|
||||||
|
orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "",
|
||||||
|
orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "",
|
||||||
|
empType: user.attributes.empType?.[0] || "",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue