2way-api — Repository Map
บทนี้เป็นแผนที่ไฟล์ของ 2way-api (branch: prod, 178 tracked files) — NestJS 9 + TypeORM + MSSQL สำหรับฟีเจอร์สื่อสารสองทาง (ประกาศ / แบบสำรวจ / เรื่องร้องเรียน) กับพนักงานผ่าน LINE และ backoffice
ชื่อ package ใน package.json คือ vtrc-restfull-v2 (2way-api/package.json:2) — ไม่ตรงกับชื่อ repo; เป็นร่องรอย scaffold ร่วมกับ 2way-batch / 2way-line-service
แผนที่โฟลเดอร์ระดับ repo
2way-api/ (branch: prod)
├── package.json # name=vtrc-restfull-v2, NestJS ^9, TypeORM ^0.3.11
├── yarn.lock # package manager จริงใน CI คือ yarn
├── nest-cli.json
├── tsconfig.json / tsconfig.build.json
├── .env.example / .env.uat / .env.prod
├── Dockerfile / Dockerfile.prod / Dockerfile.uat
├── docker-compose.yml / docker-compose.cloud.yml
├── bitbucket-pipelines.yml # CI/CD (ไม่ใช่ GitHub Actions)
├── endpoint.txt # รายการ endpoint มือเขียน (อ้างอิงเท่านั้น)
├── config/
│ └── configuration.ts # ConfigModule factory (port, db, line tokens, status codes)
├── docs/
│ └── document.config.ts # Swagger DocumentBuilder
├── libs/
│ ├── jwt/
│ │ ├── jwt-auth.guard.ts # global JwtAuthGuard + @Public()
│ │ └── jwt.strategy.ts # passport-jwt Strategy name 'jwt'
│ ├── exception/
│ │ └── http-exception.filter.ts
│ ├── log/
│ │ └── logs.middleware.ts # I3GatewayLogger
│ └── common/
│ ├── base64-to-file.ts
│ ├── ganarate-random-string.ts
│ └── validator-date.ts
├── nginx/ # conf / conf.d (reverse proxy snippets)
├── sql/
│ ├── 20230412_create_structure_table_prod.sql
│ └── backup/
├── uploads/ # static files served ที่ prefix /2way-api/
├── test/
└── src/
├── main.ts # bootstrap
├── app.module.ts # root wiring
├── app.controller.ts # empty @Controller()
├── app.service.ts
├── constants/
├── dto/
├── entities/ # 41 entity files (+ base.ts)
├── modules/
├── crontab/
└── utils/Boot sequence — จาก yarn start:prod ถึง listen
Production entry คือ node dist/src/main (2way-api/package.json:18). ลำดับใน main.ts:
1. NestFactory.create(AppModule, { logger: I3GatewayLogger('System') })
│ `2way-api/src/main.ts:16-18`
▼
2. อ่าน ConfigService → port = config.get('port') || 3000
│ `2way-api/src/main.ts:21-22`
│ (.env.example ตั้ง PORT=8200 ที่ `2way-api/.env.example:5`)
▼
3. setGlobalPrefix(`${BASE_PATH}api/v1`)
│ `2way-api/src/main.ts:24-26`
│ BASE_PATH=2way-api/ → prefix = 2way-api/api/v1
▼
4. useStaticAssets(uploads) prefix '/2way-api/'
│ `2way-api/src/main.ts:27-29`
▼
5. bodyParser json/urlencoded limit 50mb
│ `2way-api/src/main.ts:30-31`
▼
6. useGlobalFilters(HttpExceptionFilter)
useGlobalGuards(JwtAuthGuard)
useGlobalPipes(ValidationPipe whitelist+forbidNonWhitelisted)
│ `2way-api/src/main.ts:32-34`
▼
7. enableCors({ origin: '*', allowedHeaders: '*', ... })
│ `2way-api/src/main.ts:35-39`
▼
8. SwaggerModule.setup(`${BASE_PATH}docs`, ...)
│ `2way-api/src/main.ts:41-42`
▼
9. app.listen(port, '0.0.0.0')
`2way-api/src/main.ts:43-46`ข้อควรจำเรื่องพอร์ต: fallback ในโค้ดคือ 3000 แต่ค่าที่ทีมตั้งใน .env.example คือ 8200 — ห้ามสรุปว่า "ฟังที่ 3000" โดยไม่มีเงื่อนไข
AppModule — สิ่งที่ถูกโหลดจริง
AppModule (2way-api/src/app.module.ts:20-64) import ตามลำดับ:
| ลำดับ | Import | บทบาท |
|---|---|---|
| 1 | ConfigModule.forRoot({ load: [configuration], isGlobal: true }) | env + status-code maps |
| 2 | TypeOrmModule.forRootAsync name 'main' | MSSQL connection |
| 3 | AppModule (ตัวเอง) | self-import ผิดปกติ — ดูด้านล่าง |
| 4 | AnnouncesModule | ประกาศ |
| 5 | SurveysModule | แบบสำรวจ |
| 6 | ComplaintsModule | เรื่องร้องเรียน |
| 7 | EmployeesModule | proxy พนักงาน → centralize-api |
| 8 | LineModule | LINE push/multicast |
| 9 | UploadModule | อัปโหลดไฟล์ base64 |
| 10 | AuthModule | login / bypass / verifyToken |
| 11 | ComplaintTaskModule | cron ร้องเรียน 07:00 |
| 12 | SurveyTaskModule | cron แบบสำรวจ 07:00 |
| 13 | AnnounceTaskModule | cron ประกาศ 07:00 |
| 14 | NotificationPmsTaskModule | โมดูล PMS (cron handler ถูก comment) |
providers ระดับ root: AppService, JwtStrategy (2way-api/src/app.module.ts:61-62)
controllers ระดับ root: AppController ว่าง (2way-api/src/app.controller.ts:3)
Self-import ของ AppModule
imports: [
// ...
AppModule, // บรรทัด 47
AnnouncesModule,
// ...
]AppModule import ตัวเองเข้า imports (2way-api/src/app.module.ts:47) — pattern ผิดปกติ น่าจะเป็น copy-paste; ไม่มีหลักฐานว่าตั้งใจทำ circular provider
Dead code — โมดูลที่มีไฟล์ครบแต่ไม่ถูก wire
ยืนยันจาก app.module.ts ว่า ไม่มี import ของโฟลเดอร์เหล่านี้:
| โฟลเดอร์ | มี controller/service/module | ถูก import ใน AppModule? |
|---|---|---|
src/modules/announces-new/ | ใช่ (5 ไฟล์ รวม spec) | ไม่ |
src/modules/employees-new/ | ใช่ (5 ไฟล์ รวม spec) | ไม่ |
src/modules/surveys/old_surveys/ | ใช่ (module + repository + controller) | ไม่ |
src/modules/complaints/old_complaints/ | มี complaints.repository.ts (ส่วนใหญ่ comment) | ไม่ |
src/crontab/complaint/old_complaint/ | มี task module/service | ไม่ |
src/crontab/survey/old_survey/ | มี task module/service | ไม่ |
announces-new และ employees-new มี @Public() ครบชุดบน controller แต่ ไม่ถูกโหลด จึงไม่มี HTTP surface จากโฟลเดอร์เหล่านี้ใน runtime
Module breakdown — โมดูลที่ทำงานจริง
auth (src/modules/auth/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
auth.controller.ts | 30 | 3 routes ทั้งหมด @Public() |
auth.service.ts | 358 | SSO login, backdoor i3admin, redirect bypass, verifyToken |
auth.module.ts | 9 | controllers + providers เท่านั้น ไม่มี TypeOrmFeature |
Routes (2way-api/src/modules/auth/auth.controller.ts:13-28):
| Method | Path (ภายใต้ prefix) | Handler |
|---|---|---|
| POST | auth/ | signInNonEmp |
| POST | auth/redirectbypass | signInRedirectByPass |
| POST | auth/verifyToken | verifyToken |
announces (src/modules/announces/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
announces.controller.ts | 103 | list/detail/CRUD/read/know + CU variants |
announces.service.ts | 1489 | raw SQL list + TypeORM write + LINE side-effect |
announces.module.ts | 18 | HttpModule + UploadService + LineService + ProfileUtil |
Routes สำคัญ (announces.controller.ts):
| Method | Path | @Public()? |
|---|---|---|
| GET | announces/cu/list | ใช่ |
| GET | announces/cu/detail/:contentId | ใช่ |
| GET | announces/types | ไม่ (ต้อง JWT) |
| GET | announces | ใช่ |
| GET | announces/:contentId | ใช่ |
| POST | announces | ไม่ |
| PUT | announces/:contentId | ไม่ |
| POST | announces/readContent | ใช่ |
| POST | announces/knowContent | ใช่ |
| DELETE | announces/:contentId | ไม่ |
| POST | announces/test/noti/:id | ไม่ |
surveys (src/modules/surveys/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
surveys.controller.ts | 136 | types/create/summary/list/detail/approve/reject/read |
surveys.service.ts | 1199 | workflow แบบสำรวจ + raw SQL บางจุด |
surveys.module.ts | — | wire ปกติ |
@Public() ที่เปิดอยู่จริงมีแค่ GET /surveys และ GET /surveys/:id/:empCode (surveys.controller.ts:31-50); create/approve/reject ต้อง JWT
complaints (src/modules/complaints/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
complaints.controller.ts | 213 | เกือบทุก route เป็น @Public() รวม workflow states |
complaints.service.ts | 2012 | ใหญ่สุดใน repo — state machine ร้องเรียน |
Routes ที่ @Public() รวมถึง POST complaints, adminAssign, adminReject, assigneeManage, secretaryApprove, secretaryDecline, superAdminApproval, superAdminReject (complaints.controller.ts:13-114)
employees (src/modules/employees/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
employees.controller.ts | 168 | proxy REST ไป centralize-api + stub CRUD manager |
employees.service.ts | 273 | axios → ${CENTRALIZE_SERVER}api/employee/... |
EmpCodeMiddleware | ใน controller file | ถูก comment ทั้งก้อน — next() ตรงๆ |
Controller path คือ @Controller('employee') ไม่มี s (employees.controller.ts:55)
line (src/modules/line/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
line.controller.ts | 36 | @Controller('Line') — L ใหญ่ |
line.service.ts | 93 | push / multicast / nextPage / previousPage |
common/index.ts | — | fetchAlways retry helper |
ทุก route เป็น @Public() (line.controller.ts:13-34)
uploadFiles (src/modules/uploadFiles/)
| ไฟล์ | บรรทัดโดยประมาณ | หน้าที่ |
|---|---|---|
upload.controller.ts | 42 | GET :id, POST create, POST complaints (ซ้ำ path @Post()) |
upload.service.ts | 233 | base64 → /uploads/{prefix}/{YYYY-MM-DD} |
MIME ที่อนุญาต: jpeg/jpg/png/pdf/mp4 (upload.service.ts:44-45)
Crontab modules
ทุก task module เรียก ScheduleModule.forRoot() ภายในโมดูลตัวเอง (ไม่ใช่ครั้งเดียวที่ root):
| Module | Cron expression | Handler | สถานะ |
|---|---|---|---|
ComplaintTaskModule | EVERY_DAY_AT_7AM | handleCron → 4 งานแจ้งเตือน | เปิด |
SurveyTaskModule | EVERY_DAY_AT_7AM | in-scope + out-scope notify | เปิด |
AnnounceTaskModule | EVERY_DAY_AT_7AM | notificationOfferDate | เปิด |
NotificationPmsTaskModule | — | handleCron ถูก comment ทั้งก้อน | ไม่รัน |
หลักฐาน PMS cron ปิด: notification-pms.task.service.ts:19-23 — method notificationLinePms() ยังมีโค้ด SQL ข้าม schema แต่ไม่มี @Cron ที่ active
Complaint cron ลำดับงาน (complaint.task.service.ts:21-38):
noActionAssignment— แอดมินไม่มอบหมายใน 3 วันnoActionIsKnow3Days— ผู้รับมอบหมายไม่กดรับทราบ 3 วันnoActionIsKnow7Days— ไม่กดรับทราบ 7 วันnoActionAskApproval— กดรับทราบแล้วไม่ส่งกลับเกิน 14 วัน
Entities — 41 ไฟล์ + base
BaseEntity (src/entities/base.ts) ให้คอลัมน์ร่วม: id, create_date, create_by, update_date, update_by
TypeORM โหลดด้วย glob __dirname + '/**/*.entity{.ts,.js}' (app.module.ts:38) — ครอบคลุมทุกไฟล์ใต้ src/entities/
Master (m_*)
| Entity file | Table | ใช้โดย |
|---|---|---|
m_config.entity.ts | m_config | config key/value |
m_content_flow.entity.ts | m_content_flow | flow stage ของ content |
m_content_type.entity.ts | m_content_type | ประเภท content (Announce/...) |
m_error_message.entity.ts | m_error_message | ข้อความ error |
m_reuse_survey.entity.ts | m_reuse_survey | ประเภท reuse survey |
m_role.entity.ts | m_role | role + action_view |
m_status.entity.ts | m_status | status กลาง |
m_status_complaint.entity.ts | m_status_complaint | status ร้องเรียน |
m_status_survey.entity.ts | m_status_survey | status แบบสำรวจ |
m_subject_complaint.entity.ts | m_subject_complaint | หัวข้อร้องเรียน |
m_topic.entity.ts | m_topic | หัวข้อประกาศ |
m_type_complaint.entity.ts | m_type_complaint | ประเภทร้องเรียน |
m_type_survey.entity.ts | m_type_survey | ประเภทแบบสำรวจ |
m_type_survey_ignore.entity.ts | m_type_survey_ignore | ignore type |
m_user_manager.entity.ts | m_user_manager | รายชื่อ manager กรองออกจาก employee list |
Transaction (t_*)
| Entity file | Table | Domain |
|---|---|---|
t_content.entity.ts | t_content | ประกาศ (content กลาง) |
t_content_assign.entity.ts | t_content_assign | มอบหมายรายคน |
t_content_assign_all.entity.ts | t_content_assign_all | มอบหมายแบบ ALL |
t_content_assign_survey.entity.ts | t_content_survey_all | survey assign-all |
t_content_complaint.entity.ts | t_content_complaint | ร้องเรียน |
t_content_flow_history.entity.ts | t_content_flow_history | ประวัติ flow |
t_content_manager.entity.ts | t_content_manager | manager ของ content |
t_content_status.entity.ts | t_content_status | สถานะ content |
t_content_survey.entity.ts | t_content_survey | แบบสำรวจ |
t_content_survey_ignore.entity.ts | t_content_survey_ignore | ignore list |
t_assign_complaint.entity.ts | t_assign_complaint | มอบหมายร้องเรียน |
t_assign_survey.entity.ts | t_assign_survey | มอบหมายแบบสำรวจ |
t_attachment_announce.entity.ts | t_attachment_announce | ไฟล์แนบประกาศ |
t_attachment_complaint.entity.ts | t_attachment_complaint | ไฟล์แนบร้องเรียน |
t_thumbnail_announce.entity.ts | t_thumbnail_announce | thumbnail |
t_history_complaint.entity.ts | t_history_complaint | ประวัติร้องเรียน |
t_history_survey.entity.ts | t_history_survey | ประวัติแบบสำรวจ |
t_reuse_survey.entity.ts | t_reuse_survey | รอบ reuse |
t_send_notify.entity.ts | t_send_notify | คิวแจ้งเตือน |
t_logs_notification.entity.ts | t_logs_notification | audit log การส่ง LINE |
t_change_divicode_complaints.entity.ts | t_change_divicode_complaints | เปลี่ยนฝ่าย |
temp_complaint_role.entity.ts | temp_complaint_role | temp role |
temp_survey.entity.ts | temp_survey | temp survey |
User / LINE
| Entity file | Table | คอลัมน์สำคัญ |
|---|---|---|
userLine.entity.ts | userLine | userID, identityCard, lineID, richMenuID, isActive |
userLineProfile.entity.ts | (profile table) | โปรไฟล์ LINE ขยาย — ไฟล์ใหญ่ (~6 KB) |
โมเดลข้อมูลหลักของ announces ใช้ t_content + m_content_type แยกโดเมนด้วย name_en (เช่น 'Announce') — surveys/complaints มีตาราง t_content_survey / t_content_complaint แยกต่างหาก ไม่ได้แชร์ t_content เหมือนกันทุกกรณี
DTO / constants / utils
src/dto/ (9 ไฟล์)
| ไฟล์ | ใช้โดย |
|---|---|
announce.dto.ts | announces |
auth.dto.ts | auth (username, password + @IsString) |
common.dto.ts | shared |
complaint.dto.ts | complaints (ใหญ่) |
employee.dto.ts | employees |
line.dto.ts | line |
survey.dto.ts | surveys (ใหญ่) |
upload.dto.ts | upload |
user.dto.ts | user-related |
src/constants/
| ไฟล์ | เนื้อหา |
|---|---|
complaints.constant.ts | action mapping (SECRETARY_*, config keys) |
surveys.constant.ts | survey action / status constants |
src/utils/
| ไฟล์ | หน้าที่ |
|---|---|
profiles.util.ts | ถอด JWT จาก Authorization ด้วย SECRET_JWT_TWOWAY |
encrypt.util.ts | AES ผ่าน crypto-js; default secret 'CHANGE_ME_PLS' |
surveys.util.ts | helper + raw query สำหรับ survey |
complaints.util.ts | helper mapping action/status |
ramdomText.util.ts | random string (ชื่อสะกดผิดในไฟล์) |
libs/ — cross-cutting
JwtAuthGuard + @Public()
canActivate(context) {
const isPublic = this.reflector.getAllAndOverride<boolean>(isPublicKey, [...]);
if (isPublic) return true;
return super.canActivate(context);
}แหล่ง: 2way-api/libs/jwt/jwt-auth.guard.ts:15-24
decorator: export const Public = () => SetMetadata(isPublicKey, true) (jwt-auth.guard.ts:41)
JwtStrategy
- Extract จาก
Authorization: Bearer ignoreExpiration: falseที่ strategy (jwt.strategy.ts:13)secretOrKey: process.env.SECRET_JWT_TWOWAY(jwt.strategy.ts:14)validateคืน payload ทั้งก้อน (jwt.strategy.ts:19-22)
หมายเหตุ: AuthService.verifyJwt ใช้ ignoreExpiration: true คนละที่กับ strategy (auth.service.ts:34) — เส้นทาง verifyToken / redirectbypass จึงยอมรับ token หมดอายุได้
HttpExceptionFilter
จับเฉพาะ HttpException แล้วตอบ { statusCode, message } (http-exception.filter.ts:5-19) — error ที่ throw เป็น string ธรรมดา (เช่น 'No Bearer token') จะไม่ผ่าน filter นี้ในรูปแบบเดียวกับ Nest exception
Config และ env
config/configuration.ts map:
port,jwtSecretKey←SECRET_KEYdb.host/port/user/password/databaseNamelineToken,lineCUTokencontentStatus.announcement|survey|complaint— status code tables ฝังในโค้ด
env ที่ service อ่านตรงจาก process.env (ไม่อยู่ใน configuration factory):
| ตัวแปร | ใช้ที่ |
|---|---|
BASE_PATH | main.ts prefix |
SECRET_JWT_TWOWAY | JWT sign/verify |
SECRET_KEY | verify token จาก VTRC ใน redirectbypass |
CENTRALIZE_SERVER | employees → NestJS centralize-api REST |
CENTRALIZE_API_URL | auth → cu-central-api GraphQL |
VTRC_BASE_CONNECT | GraphQL ไป vtrc-api (ถูก comment ใน caller) |
API_KEY_OUTBOUND_VTRC | apiKey header ไป vtrc-api |
SSO_* | SSO login (มี hardcoded fallback ใน AuthService) |
ดูรายละเอียด integration ที่ 03-persistence-integrations
Deploy / CI
| ไฟล์ | หมายเหตุ |
|---|---|
Dockerfile.prod | multi-stage: build บน node:16-alpine, run บน node:18-alpine; copy .env.prod → .env |
Dockerfile.uat | โครงเดียวกันสำหรับ UAT |
Dockerfile | default/dev |
bitbucket-pipelines.yml | image node:16; yarn build/lint; docker build → GCR |
docker-compose.yml | local compose |
docker-compose.cloud.yml | cloud compose สั้น |
engines.node ใน package.json คือ >=18.0.0 (package.json:27) แต่ build stage และ Bitbucket image ยังเป็น Node 16
แผนที่ความสัมพันธ์กับ repo อื่นในกลุ่ม 2way
2way-meeting-backoffice
APP_CONNECT_API=.../2way-api
│
▼
2way-api (repo นี้)
├── REST → centralize-api (CENTRALIZE_SERVER)
├── GraphQL → cu-central-api (CENTRALIZE_API_URL)
├── GraphQL → vtrc-api (VTRC_BASE_CONNECT) [caller comment ปิด]
├── HTTPS → api.line.me
└── HTTP → 2way-line-service ที่ IP:8200 (hardcoded)Consumer ที่ยืนยันจาก frontend env: 2way-meeting-backoffice ตั้ง APP_CONNECT_API ชี้มาที่ /2way-api — ดูรายละเอียดในบท 03
สิ่งที่บทนี้ไม่ครอบคลุม
- Trace request เต็มเส้นทาง → 02-core-pipeline
- Schema คอลัมน์ละเอียด + integration secrets → 03-persistence-integrations
- Severity ranking ของช่องโหว่ → 04-scorecard
Checklist สำหรับผู้อ่านที่เพิ่งเข้า repo
- Checkout branch prod ก่อนอ่านโค้ด (default
masterอาจว่าง/ไม่ครบ) - อ่าน
app.module.tsเพื่อรู้ว่าโมดูลไหน live - อย่าสมมติว่า
announces-new/employees-newทำงาน — พวกมัน dead - ทุก request ผ่าน
JwtAuthGuardยกเว้น@Public()— และ@Public()ถูกใช้กว้างมาก - DB คือ MSSQL schema
2way(ชื่อจาก.env.example); PMS อยู่ schemapmsบน instance เดียวกัน