diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 79befb8f..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,134 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -TypeScript REST API for **Bangkok Metropolitan Administration (BMA)** — a Human Resource -Management System (HRMS) module managing organization structure (โครงสร้างอัตรากำลัง), -positions, personnel profiles, salary/tenure calculations, and related HR workflows. - -**Stack:** Express.js · TypeORM (MySQL) · tsoa (controller-first OpenAPI generation) · Keycloak · -RabbitMQ · Redis · Elasticsearch · WebSocket (socket.io) · node-cron - -## Commands - -```sh -npm install # install deps (npm is canonical — CI/Docker use npm install, not pnpm) -npm run dev # dev server with hot reload (nodemon) -npm run build # tsoa spec-and-routes (regenerates src/routes.ts, src/swagger.json) + tsc -npm run check # tsc --noEmit -npm start # run compiled dist/app.js -npm run format # prettier --write . - -npm run migration:generate src/migration/ # generate a TypeORM migration -npm run migration:run # run pending migrations -node scripts/clean-migration-fk-idx.js # strip FK_/idx_ lines from generated migrations (run after every generate) - -npm test # jest -npm run test:watch # jest --watch -npm run test:coverage # jest --coverage -npx jest src/__tests__/unit/OrganizationController.spec.ts # run a single test file -``` - -A repo note (README) documents building/testing the release pipeline locally with `act` -(`act workflow_dispatch -W .github/workflows/release.yaml ...`), but the actual CI is Forgejo -Actions (`.forgejo/workflows/`) and an OneDev buildspec (`.onedev-buildspec.yml`); both run -`npm install && npm run build` and build `docker/Dockerfile`. - -**Note:** despite `pnpm-lock.yaml` being present in the repo, all CI/Docker paths use `npm`. Use -`npm` for scripts and dependency changes unless told otherwise. - -## Architecture - -### Request flow: tsoa controllers → (optional) services → TypeORM repositories - -- Routes are **not** hand-written. `tsoa.json` globs `src/controllers/**/*Controller.ts` and - `npm run build` regenerates `src/routes.ts` (Express route registration, called via - `RegisterRoutes(app)` in `src/app.ts`) and `src/swagger.json` from tsoa decorators. **Any time - you add/change a controller method or its decorators, you must run `npm run build`** (or at - least `tsoa spec-and-routes`) or the route/swagger changes won't take effect. -- In practice most controllers get their repositories directly via - `AppDataSource.getRepository(Entity)` in the constructor/method body rather than going through a - service layer — the codebase is controller-heavy (`OrganizationController.ts`, - `ImportDataController.ts`, `PositionController.ts`, `ReportController.ts` are each hundreds of - KB). `src/services/` exists for genuinely cross-cutting or heavier business logic (org command - execution, salary/tenure batch jobs, RabbitMQ, WebSocket) but is not a strict mandatory layer for - every endpoint. -- All entities (`src/entities/`) extend `EntityBase` (`src/entities/base/Base.ts`), which supplies - `id` (UUID), `createdAt`/`lastUpdatedAt`, and creator/updater id+name audit columns — every table - is audited by default. `src/entities/mis/` holds legacy/external MIS-system table mappings - (read-mostly, prefixed `HR_*`); `src/entities/view/` holds TypeORM entities mapped onto SQL - views (`viewCurrentTenure*`, `viewDirector*`, etc.) used for computed/reporting reads. -- Responses are wrapped in `HttpSuccess` (`src/interfaces/http-success.ts`, `{status, message, - result}`, Thai success message by default) or thrown as `HttpError` (`src/interfaces/ -http-error.ts`, carries an `HttpStatus` + Thai message) — the global error middleware - (`src/middlewares/error.ts`) catches thrown `HttpError`/exceptions and formats the response. - `src/interfaces/http-status.ts` is the status-code enum used everywhere instead of magic numbers. - -### Auth (three schemes, selected per-route via `@Security(...)`) - -All resolved in `expressAuthentication` (`src/middlewares/auth.ts`), which tsoa calls per-request -based on the controller's `@Security` decorator: - -- `bearerAuth` — Keycloak JWT in `Authorization: Bearer `. Verified either **offline** - (`AUTH_PUBLIC_KEY`, local RS256 verification via `fast-jwt`) or **online** (`AUTH_REALM_URL` - userinfo endpoint), selected by `AUTH_PREFERRED_MODE`. Populates `req.app.locals.logData` with - user/org-tree ids from the token for logging. Type: `RequestWithUser` (`src/middlewares/user.ts`). -- `webServiceAuth` — `X-API-Key` header, resolved against the `ApiKey` entity in - `src/middlewares/authWebService.ts` (looks up allowed `apiNames`/org scope). Type: - `RequestWithUserWebService`. -- `internalAuth` — `api-key`/`api_key`/`apikey` header checked against the `API_KEY` env var - (`src/middlewares/authInternal.ts`), for trusted internal services (e.g. the .NET HRMS system). -- If `NODE_ENV !== "production"` and `AUTH_BYPASS` is set, auth is skipped entirely - (`{preferred_username: "bypassed"}`) — dev/test convenience only. -- Role gating within `bearerAuth` routes uses `authRole()` (`src/middlewares/role.ts`) and the - `permission` helper (`src/interfaces/permission.ts`) which calls out to an external `/org/permission` - check via `CallAPI`. - -### Background work - -- **Cron jobs** are all registered inline in `src/app.ts` via `node-cron` (6-field: seconds - included), each wrapping a controller/service call in try/catch that only logs on failure — - daily org revision cache refresh, retirement status updates (Oct 1st), org DNA sync, tenure - recalculation, and posting retirement data to an external "Exprofile" system. -- **RabbitMQ** (`src/services/rabbitmq.ts`) publishes org-structure change events - (`sendToQueueOrg` / `sendToQueueOrgDraft`); connects with infinite retry - (`setTimeout(runMessageQueue, 1000)` on failure) — don't add redundant retry logic around it. - Fire-and-forget from callers; don't block HTTP responses on publish. -- **WebSocket** (`src/services/webSocket.ts`, `initWebSocket()`) for real-time push, initialized - before the HTTP server starts listening. -- **`OrgStructureCache`** (`src/utils/OrgStructureCache.ts`) is an in-memory TTL cache (30 min) for - org-tree reads, keyed by revision+root id, initialized/destroyed alongside the app lifecycle. Use - it instead of re-querying the same org structure repeatedly within a request. -- **`LogMemoryStore`** (`src/utils/LogMemoryStore.ts`) plus `src/middlewares/logs.ts` build a - per-request log sequence (including DB queries logged via the custom TypeORM `Logger` in - `src/database/data-source.ts`) — this is what's shipped to Elasticsearch for auditing. - -### Database - -- MySQL via TypeORM, connection pool configured in `src/database/data-source.ts` - (`connectionLimit`, `poolSize`, `maxQueryExecutionTime` all env-tunable). `synchronize` is - always `false` — schema changes go through migrations only. -- Timezone is pinned to Bangkok (`+07:00`) at the connection level; store/compare datetimes - accordingly rather than assuming UTC. -- After `migration:generate`, always run `node scripts/clean-migration-fk-idx.js` to strip - auto-generated `FK_*`/`idx_*` lines from the migration's `up`/`down` — this is a hard project - convention, not optional cleanup. - -### Data dictionary - -`docs/data-dictionary/` holds a generated schema data dictionary (`.docx`); the `data-dictionary` -skill / `scripts/generate-docx.py` regenerate it from `docs/data-dictionary/generate-prompt.md`. - -## Conventions - -- Files/classes: PascalCase (`OrganizationController.ts`). Variables/functions: camelCase. -- DB column comments and HTTP error/success messages are written in **Thai** — keep this - consistent when adding columns or throwing `HttpError`. -- Services must not import from `controllers/`; keep services HTTP-agnostic (no `HttpError`/ - `HttpSuccess` imports in `src/services/`). -- Don't use `moment` for new code (native `Date`/`Intl` only — `moment` remains for legacy call - sites). Don't bypass tsoa validation with manual `req.body` casting. -- `tsconfig.json` excludes `src/__tests__/**` and `*.spec.ts`/`*.test.ts` from the production - build; tests only run under `ts-jest` via `jest.config.js` (path alias `@/` → `src/`). diff --git a/src/app.ts b/src/app.ts index 2a75e6fe..c2cbe5f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,24 +21,6 @@ import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; import { RetirementService } from "./services/RetirementService"; -// ── Process-level safety nets ────────────────────────────────────────────── -// ตาข่ายกัน service ตายจาก promise ที่ reject แล้วไม่ถูกจับ / exception ที่หลุด -// ออกมา (เช่น bug เรียก async โดยไม่ await ใน PermissionController). -// ลงทะเบียนไว้ให้เร็วที่สุด ก่อนที่ main() จะเริ่มทำงาน -process.on("unhandledRejection", (reason) => { - console.error( - "[APP][unhandledRejection] A promise was rejected but never caught:", - reason, - ); -}); -process.on("uncaughtException", (err) => { - // คง service ไว้เพื่อ availability สูงสุด - // หมายเหตุ: process อาจอยู่ในสถานะไม่สม่ำเสมอ ควร monitor log นี้และ restart - // ในช่วงเวลาที่เหมาะสม ถ้าต้องการ crash-on-error ให้เปลี่ยนเป็น graceful - // shutdown + process.exit(1) ตรงนี้ - console.error("[APP][uncaughtException] An exception escaped all handlers:", err); -}); - async function main() { await AppDataSource.initialize(); diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index c9c45dae..6720f19d 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -23,7 +23,7 @@ interface CachedToken { } const API_URL_BANGKOK = "https://exprofile.bangkok.go.th/API"; const clientId = "e5f6ad6ce374177eef023bf5d0c018b6"; -const clientSecret = "5EhOvN5DwHOKakupqT9FmCk7MOwpT3zLqLPkPh4ZhJpxBN2nMG"; +const clientSecret = "5EhOvN5DwHOKakupqT9FmCk7MOwpT3zLqLPkPh4ZhJpxBN2nMG@2022"; class TokenCache { private static cache: Map = new Map(); @@ -43,35 +43,6 @@ class TokenCache { } } -// network/DNS error codes ที่ถือว่าเป็น transient (ควร retry) -const TRANSIENT_NETWORK_CODES = [ - "EAI_AGAIN", // DNS lookup ล้มเหลวชั่วคราว - "ENOTFOUND", - "ECONNRESET", - "ETIMEDOUT", - "ECONNREFUSED", - "EHOSTUNREACH", - "ENETUNREACH", -]; - -// ตรวจว่า error ควร retry: ครอบ network/DNS (ไม่มี response หรือ code ตรง TRANSIENT) และ HTTP 5xx -function isTransientError(error: any): boolean { - if (!error) return false; - if (!error.response || TRANSIENT_NETWORK_CODES.includes(error.code)) { - return true; - } - return error.response?.status >= 500; -} - -// หน่วงเวลา (backoff) ระหว่าง retry — ใช้รูปแบบ setTimeout เดียวกับ keycloak/index.ts -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// config retry สำหรับเรียก Exprofile -const EXPROFILE_MAX_RETRIES = 3; // จากเดิม 2 -const EXPROFILE_BACKOFF_BASE_MS = 1000; // backoff = base * 2^retryCount → 1s, 2s - @Route("api/v1/org/ex/retirement") @Tags("ExRetirement") @Security("bearerAuth") @@ -89,7 +60,7 @@ export class ExRetirementController extends Controller { }, ) { let retryCount = 0; - const maxRetries = EXPROFILE_MAX_RETRIES; + const maxRetries = 2; while (retryCount < maxRetries) { try { @@ -121,14 +92,11 @@ export class ExRetirementController extends Controller { // return res.data; return new HttpSuccess(res.data.data); } catch (error: any) { - if (isTransientError(error) && retryCount < maxRetries - 1) { + if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); - await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; } - // log error message - console.error('getData getOfficerRetireData error:', error); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } } @@ -137,7 +105,7 @@ export class ExRetirementController extends Controller { @Get("/document/{documentId}") async getDocument(@Path("documentId") officerDocumentID: string, @Request() req: any) { let retryCount = 0; - const maxRetries = EXPROFILE_MAX_RETRIES; + const maxRetries = 2; while (retryCount < maxRetries) { try { const token = await getToken(clientId, clientSecret); @@ -166,13 +134,11 @@ export class ExRetirementController extends Controller { return; } } catch (error: any) { - if (isTransientError(error) && retryCount < maxRetries - 1) { + if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); - await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; } - console.error('getData document error:', error); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } } @@ -202,13 +168,7 @@ async function getToken(ClientID: string, ClientSecret: string): Promise TokenCache.set(cacheKey, token); return token; } catch (error) { - // log แบบกระชับ (ลด log noise จากการ dump object ใหญ่ตามที่เห็นใน log จริง) - console.error( - "getToken error:", - error instanceof Error ? `${error.name}: ${error.message}` : error, - ); - // โยน AxiosError ตัวจริงออกไปให้ caller อ่าน code / response / status เพื่อตัดสินใจ retry - throw error; + return Promise.reject({ message: "Error occurred", error }); } } @@ -234,7 +194,7 @@ export async function PostRetireToExprofile( } let retryCount = 0; - const maxRetries = EXPROFILE_MAX_RETRIES; + const maxRetries = 2; while (retryCount < maxRetries) { try { @@ -271,9 +231,8 @@ export async function PostRetireToExprofile( return res.data; } catch (error: any) { - if (isTransientError(error) && retryCount < maxRetries - 1) { + if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); - await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; } @@ -292,7 +251,6 @@ export async function PostRetireToExprofile( }); } - console.error('importOfficerRetireData error', error); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } } diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 3a03f56a..0814d8a2 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -4792,16 +4792,7 @@ export class OrganizationDotnetController extends Controller { async getProfileByKeycloak(@Path() keycloakId: string) { const profile = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, - relations: [ - "posLevel", - "posType", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - ], + relations: ["posLevel", "posType", "current_holders", "current_holders.orgRoot"], }); if (!profile) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); @@ -4816,16 +4807,11 @@ export class OrganizationDotnetController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแบบร่างโครงสร้าง"); } - const holder = - profile.current_holders == null + const root = + profile.current_holders == null || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null - : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) ?? null; - - const root = holder?.orgRoot ?? null; - const child1 = holder?.orgChild1 ?? null; - const child2 = holder?.orgChild2 ?? null; - const child3 = holder?.orgChild3 ?? null; - const child4 = holder?.orgChild4 ?? null; + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const _profile: any = { profileId: profile.id, @@ -4842,10 +4828,6 @@ export class OrganizationDotnetController extends Controller { root: root == null ? null : root.orgRootName, rootShortName: root == null ? null : root.orgRootShortName, rootDnaId: root == null ? null : root.ancestorDNA, - child1DnaId: child1 == null ? null : child1.ancestorDNA, - child2DnaId: child2 == null ? null : child2.ancestorDNA, - child3DnaId: child3 == null ? null : child3.ancestorDNA, - child4DnaId: child4 == null ? null : child4.ancestorDNA, }; return new HttpSuccess(_profile); } diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index f7a22517..44747b09 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -810,7 +810,7 @@ export class PermissionController extends Controller { }); const getAsync = promisify(redisClient.get).bind(redisClient); - let org = await this.PermissionOrg(request, system, action); + let org = this.PermissionOrg(request, system, action); let reply = await getAsync("user_" + id); if (reply != null) { reply = JSON.parse(reply); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 4d328f99..fbc09201 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -325,33 +325,16 @@ export class PosMasterActController extends Controller { // ลบ Redis cache ของคนที่เป็น acting if (posMasterAct != null && posMasterAct.posMasterChild?.current_holderId) { const profileId = posMasterAct.posMasterChild.current_holderId; - const redisClient = this.redis.createClient({ + const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, }); const delAsync = promisify(redisClient.del).bind(redisClient); - const quitAsync = promisify(redisClient.quit).bind(redisClient); - try { - const [roleDeleted, menuDeleted] = await Promise.all([ - delAsync("role_" + profileId), - delAsync("menu_" + profileId), - ]); + await delAsync("role_" + profileId); + await delAsync("menu_" + profileId); - if (roleDeleted === 0) { - console.warn(`[PosMasterActController] Redis key not found: role_${profileId}`); - } - if (menuDeleted === 0) { - console.warn(`[PosMasterActController] Redis key not found: menu_${profileId}`); - } - } catch (error) { - console.error( - `[PosMasterActController] Redis delete error for profile ${profileId}:`, - error, - ); - } finally { - await quitAsync(); - } + redisClient.quit(); } return new HttpSuccess(); @@ -638,13 +621,13 @@ export class PosMasterActController extends Controller { let conditionGroup = ""; if (body.type.trim().toUpperCase() == "GROUP1.1") { conditionGroup = - "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ชำนาญงาน') OR (posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ปฏิบัติงาน') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ปฏิบัติการ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการ')"; + "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ชำนาญงาน') OR (posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ปฏิบัติงาน') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ปฏิบัติการ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการ')"; } else if (body.type.trim().toUpperCase() == "GROUP1.2") { conditionGroup = - "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'อาวุโส') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการพิเศษ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'ต้น')"; + "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'อาวุโส') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการพิเศษ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'ต้น')"; } else if (body.type.trim().toUpperCase() == "GROUP2") { conditionGroup = - "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ทักษะพิเศษ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'เชี่ยวชาญ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ทรงคุณวุฒิ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'สูง') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'ต้น') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'สูง')"; + "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ทักษะพิเศษ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'เชี่ยวชาญ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ทรงคุณวุฒิ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'สูง') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'ต้น') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'สูง')"; } else { throw new HttpError(HttpStatusCode.NOT_FOUND, "กลุ่มเป้าหมายไม่ถูกต้อง"); } @@ -885,28 +868,10 @@ export class PosMasterActController extends Controller { }); const delAsync = promisify(redisClient.del).bind(redisClient); - const quitAsync = promisify(redisClient.quit).bind(redisClient); + await delAsync("role_" + profileId); + await delAsync("menu_" + profileId); - try { - const [roleDeleted, menuDeleted] = await Promise.all([ - delAsync("role_" + profileId), - delAsync("menu_" + profileId), - ]); - - if (roleDeleted === 0) { - console.warn(`[PosMasterActController] Redis key not found: role_${profileId}`); - } - if (menuDeleted === 0) { - console.warn(`[PosMasterActController] Redis key not found: menu_${profileId}`); - } - } catch (error) { - console.error( - `[PosMasterActController] Redis delete error for profile ${profileId}:`, - error, - ); - } finally { - await quitAsync(); - } + redisClient.quit(); }), ); } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 254f5c46..05ab62f8 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -11451,10 +11451,11 @@ export class ProfileController extends Controller { system?: string; }, ) { - // ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ - let _system: string = "SYS_REGISTRY_OFFICER"; - if (body.system) _system = body.system; - let _data = await new permission().PermissionOrgList(request, _system); + // comment ออกก่อนเพราะยังไม่ได้ใช้ + // // ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ + // let _system: string = "SYS_REGISTRY_OFFICER"; + // if (body.system) _system = body.system; + // let _data = await new permission().PermissionOrgList(request, _system); const findRevision = await this.orgRevisionRepo.findOne({ where: { orgRevisionIsCurrent: true }, }); @@ -11499,50 +11500,10 @@ export class ProfileController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.isActive = :isActive AND profile.isDelete = :isDelete", { - isActive: true, - isDelete: false, + .where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", { + isActive: false, + isDelete: true, }) - .andWhere( - _data.root != undefined && _data.root != null - ? _data.root[0] != null - ? `current_holders.orgRootId IN (:...root)` - : `current_holders.orgRootId is null` - : "1=1", - { root: _data.root }, - ) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `current_holders.orgChild1Id IN (:...child1)` - : `current_holders.orgChild1Id is null` - : "1=1", - { child1: _data.child1 }, - ) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `current_holders.orgChild2Id IN (:...child2)` - : `current_holders.orgChild2Id is null` - : "1=1", - { child2: _data.child2 }, - ) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `current_holders.orgChild3Id IN (:...child3)` - : `current_holders.orgChild3Id is null` - : "1=1", - { child3: _data.child3 }, - ) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `current_holders.orgChild4Id IN (:...child4)` - : `current_holders.orgChild4Id is null` - : "1=1", - { child4: _data.child4 }, - ) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 3dd769ed..21eb9a5d 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -6172,7 +6172,6 @@ export class ProfileEmployeeController extends Controller { */ @Post("search-personal-no-keycloak") async getProfileBySearchKeywordNoKeyCloak( - @Request() request: RequestWithUser, @Query("page") page: number = 1, @Query("pageSize") pageSize: number = 10, @Body() @@ -6182,10 +6181,6 @@ export class ProfileEmployeeController extends Controller { system?: string; }, ) { - // ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ - let _system: string = "SYS_REGISTRY_EMP"; - if (body.system) _system = body.system; - let _data = await new permission().PermissionOrgList(request, _system); const findRevision = await this.orgRevisionRepo.findOne({ where: { orgRevisionIsCurrent: true }, }); @@ -6230,50 +6225,10 @@ export class ProfileEmployeeController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.isActive = :isActive AND profile.isDelete = :isDelete", { - isActive: true, - isDelete: false, + .where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", { + isActive: false, + isDelete: true, }) - .andWhere( - _data.root != undefined && _data.root != null - ? _data.root[0] != null - ? `current_holders.orgRootId IN (:...root)` - : `current_holders.orgRootId is null` - : "1=1", - { root: _data.root }, - ) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `current_holders.orgChild1Id IN (:...child1)` - : `current_holders.orgChild1Id is null` - : "1=1", - { child1: _data.child1 }, - ) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `current_holders.orgChild2Id IN (:...child2)` - : `current_holders.orgChild2Id is null` - : "1=1", - { child2: _data.child2 }, - ) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `current_holders.orgChild3Id IN (:...child3)` - : `current_holders.orgChild3Id is null` - : "1=1", - { child3: _data.child3 }, - ) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `current_holders.orgChild4Id IN (:...child4)` - : `current_holders.orgChild4Id is null` - : "1=1", - { child4: _data.child4 }, - ) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index c4b37b33..4902ce0f 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -38,8 +38,6 @@ import { 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 { RequestWithUser } from "../middlewares/user"; import HttpSuccess from "../interfaces/http-success"; import { Brackets, In, IsNull, Not } from "typeorm"; @@ -49,11 +47,6 @@ import { RoleKeycloak } from "../entities/RoleKeycloak"; import { addLogSequence } from "../interfaces/utils"; import { OrgRevision } from "../entities/OrgRevision"; import { Uuid } from "@elastic/elasticsearch/lib/api/types"; -import { promisify } from "util"; - -const REDIS_HOST = process.env.REDIS_HOST; -const REDIS_PORT = process.env.REDIS_PORT; -const redis = require("redis"); // import * as io from "../lib/websocket"; // import elasticsearch from "../elasticsearch"; // import { StorageFolder } from "../interfaces/storage-fs"; @@ -72,7 +65,6 @@ function stripLeadingSlash(str: string) { export class KeycloakController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private profileEmpRepo = AppDataSource.getRepository(ProfileEmployee); - private posMasterRepository = AppDataSource.getRepository(PosMaster); private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); @Get("user/{id}") @@ -360,8 +352,6 @@ export class KeycloakController extends Controller { where: { keycloak: userId }, relations: ["roleKeycloaks"], }); - let profileId: string | undefined; - if (!profile) { const profileEmp = await this.profileEmpRepo.findOne({ where: { keycloak: userId, employeeClass: "PERM" }, @@ -369,11 +359,10 @@ export class KeycloakController extends Controller { }); if (!profileEmp) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); profileEmp.roleKeycloaks = profileEmp.roleKeycloaks.filter((x) => x.id != roleId); - await this.profileEmpRepo.save(profileEmp); + this.profileEmpRepo.save(profileEmp); } else { profile.roleKeycloaks = profile.roleKeycloaks.filter((x) => x.id != roleId); - await this.profileRepo.save(profile); - profileId = profile.id; + this.profileRepo.save(profile); } const list = await getRoles(); @@ -385,53 +374,6 @@ export class KeycloakController extends Controller { list.filter((v) => roleId === v.id), ); if (!result) throw new Error("Failed. Cannot remove user's role."); - - // delete authRoleId in posMaster if roleId is "f1fff8db-0795-47c1-9952-f3c18d5b6172" - if (profileId && roleId === "f1fff8db-0795-47c1-9952-f3c18d5b6172") { - let _null: any = null; - const authRoleId = _null; - console.log(`Clearing authRoleId on position records for profile ${profileId}`); - try { - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ authRoleId: authRoleId }) - .where("current_holderId = :profileId", { profileId }) - .execute(); - } catch (error) { - console.error(`Failed to clear authRoleId on position records for profile ${profileId}:`, error); - } - - let redisClient: any; - let quitAsync: any; - try { - redisClient = redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const delAsync = promisify(redisClient.del).bind(redisClient); - quitAsync = promisify(redisClient.quit).bind(redisClient); - - const [roleDeleted, menuDeleted] = await Promise.all([ - delAsync("role_" + profileId), - delAsync("menu_" + profileId), - ]); - - if (roleDeleted === 0) { - console.warn(`Redis key not found: role_${profileId}`); - } - if (menuDeleted === 0) { - console.warn(`Redis key not found: menu_${profileId}`); - } - } catch (error) { - console.error(`Failed to delete Redis cache for profile ${profileId}:`, error); - } finally { - if (quitAsync) { - await quitAsync(); - } - } - } - return new HttpSuccess(); } diff --git a/src/keycloak/user-operations.ts b/src/keycloak/user-operations.ts deleted file mode 100644 index e6f50142..00000000 --- a/src/keycloak/user-operations.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * 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 855863c4..f06159dc 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -38,11 +38,15 @@ import { removeProfileInOrganize, setLogDataDiff, } from "../interfaces/utils"; -import { getRoles } from "../keycloak"; import { - syncKeycloakForProfile, - handleKeycloakError, -} from "../keycloak/user-operations"; + addUserRoles, + createUser, + getRoleMappings, + getRoles, + getUserByUsername, + removeUserRoles, + updateUserAttributes, +} from "../keycloak"; import { CreatePosMasterHistoryOfficer } from "./PositionService"; import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; import CallAPI from "../interfaces/call-api"; @@ -137,8 +141,8 @@ export interface ExecutionContext { * ถ้าทุกคนสำเร็จจะ return result รายงาน success count * * ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes) - * ทำหลังจาก DB transaction เพื่อใช้ profile.birthDate ที่ save แล้ว - * Keycloak ไม่สามารถ rollback ได้ - ถ้า sync ล้มเหลว profile จะถูกสร้างแต่ไม่มี Keycloak user + * ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว * * Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ * ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน @@ -481,6 +485,117 @@ 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"], @@ -558,8 +673,9 @@ export class ExecuteOfficerProfileService { profile = Object.assign({ ...item.bodyProfile, ...meta }); profile.dateRetire = _dateRetire; profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; profile.registrationAddress = item.bodyProfile.registrationAddress; profile.registrationProvinceId = registrationProvinceId ? registrationProvinceId.id @@ -602,6 +718,12 @@ 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 }); } //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ @@ -631,8 +753,9 @@ export class ExecuteOfficerProfileService { profile = Object.assign({ ...item.bodyProfile, ...meta }); profile.dateRetire = _dateRetire; profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; profile.registrationAddress = item.bodyProfile.registrationAddress; profile.registrationProvinceId = registrationProvinceId ? registrationProvinceId.id @@ -677,8 +800,9 @@ export class ExecuteOfficerProfileService { console.log( "[ExecuteOfficerProfileService] Updating existing active profile", ); - profile.roleKeycloaks = roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = ""; // ว่างไว้ก่อน - จะ sync Keycloak หลังจาก save แล้ว + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; profile.isProbation = item.bodyProfile.isProbation; profile.isLeave = item.bodyProfile.isLeave; profile.isRetirement = false; @@ -770,36 +894,6 @@ 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:", diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 3a989a5f..227c431c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -176,23 +176,6 @@ function createConsumer( //----> consumer ); } -/** - * สร้าง pseudo Express request สำหรับ service ที่ถูกเรียกจาก RabbitMQ (ไม่ใช่ HTTP) - * ต้องมี `.app.locals.logData.sequence` เพราะ addLogSequence (ใน CallAPI) อ่านค่านี้ - * และ `.headers.authorization` + `.user` สำหรับ audit/auth - */ -function buildPseudoReq(token: string, user: any) { - return { - headers: { authorization: token }, - user, - app: { - locals: { - logData: { sequence: [] as any[] }, - }, - }, - }; -} - async function handler(msg: amqp.ConsumeMessage): Promise { //----> condition before process consumer // const repo = AppDataSource.getRepository(Command); @@ -398,7 +381,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว if (isOrgSelfLinear) { console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`); - const pseudoReq = buildPseudoReq(token, user); + const pseudoReq = { headers: { authorization: token }, user }; const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -446,7 +429,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } if (resultData.length > 0) { - const pseudoReq = buildPseudoReq(token, user); + const pseudoReq = { headers: { authorization: token }, user }; const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -487,7 +470,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } if (resultData.length > 0) { - const pseudoReq = buildPseudoReq(token, user); + const pseudoReq = { headers: { authorization: token }, user }; const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -536,7 +519,10 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // Route ไป service ที่ถูกต้องตาม commandType if (resultData.length > 0 || resultData1.length > 0) { // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) - const pseudoReq = buildPseudoReq(token, user); + const pseudoReq = { + headers: { authorization: token }, + user, + }; const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq,