setup auth middleware and sync code
This commit is contained in:
parent
e76e361981
commit
a487b73c3b
5 changed files with 684 additions and 0 deletions
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