S9 — Web Portal — Backend Implementation Blueprint¶
เอกสารนี้คืออะไร
แผน implement บน portal เดิม (Node.js + MSSQL + Keycloak) — ขยายต่อ ไม่ใช่เขียนใหม่ · ผูก REQ → endpoint → service → ตาราง (schema) · อ้างอิง Traceability · Data Dictionary · ไม่ใช่โค้ดจริง (repo เป็น KB)
1. Stack เดิม & จุดที่ต่อเติม¶
| Layer | ของเดิม | สิ่งที่ทำใน Phase 2 |
|---|---|---|
| Backend | Node.js / RESTful (OpenAPI 3.0) | เพิ่ม service: membership, SSO, security-policy, service-center, survey, dashboard |
| DB | PORTAL @ PSDBPRDDB (48 ตาราง) |
+23 ตารางใหม่ / ALTER 1 (ดู schema) |
| Auth/IdP | Keycloak (UserProfile.Id = Keycloak UUID) |
ใช้ Keycloak เป็น identity broker ของ ThaiD/Digital ID/AD + SSO |
| API Gateway | Reverse proxy | SSL termination รวมศูนย์ (REQ-WPT-004, infra) |
| Query Builder | Phase 1 (inherit) — ApiTokens.AllowedQuery, OpenApiExecuteLog |
ต่อยอดเป็น PublishedQuery (CR-3) |
หลักสำคัญ: Keycloak ทำ auth/SSO, ตาราง PORTAL ทำ mapping + audit
UserProfile.Id เป็น UUID จาก Keycloak อยู่แล้ว → อย่า reinvent SSO · ให้ Keycloak federate ThaiD/Digital ID (OIDC) + AD (LDAP/SAML) และทำ SSO ข้ามระบบ ส่วนตาราง AuthProvider/UserIdentity/SsoSession เก็บ mapping และ audit ฝั่ง portal
2. Layering (ตามของเดิม)¶
Route (Express/Nest) → Controller → Service (rule) → Repository (mssql) → PORTAL.*
Cross-cutting: Keycloak middleware (verify JWT) · AuthZ (UserRole/ModuleRoles) ·
AuditLog middleware · SecurityPolicy guard · Notification dispatcher
3. REQ-by-REQ Implementation¶
REQ-WPT-001 — Integration + OpenAPI + Service Center + Query Builder¶
- OpenAPI ทุก endpoint: ลงทะเบียน endpoint ของระบบย่อยใน
PORTAL.EndpointRegistry(path/method/specVersion/openApiUrl) → หน้า API catalog - Service Center (CR-1):
POST /api/sc/intakeรับจาก สป.ยธ./MSC → insertPORTAL.ServiceCenterRequest (Origin='EXTERNAL')→RoutingServiceแตกPORTAL.ServiceCenterRoutingราย subsystem - Multi-case (CR-6): portal submit หลายเรื่อง →
ServiceCenterRequest (Origin='PORTAL')+ หลาย routing rows; เจ้าหน้าที่ปลายทางPATCH /api/sc/routing/{id}กดรับ/ไม่รับ (Accepted) - Query Builder (CR-3): ผู้ใช้ภายนอกสร้าง query →
PORTAL.PublishedQuery(QueryDefinition) → publish เป็น endpoint ผูกApiTokens(OAuth/Token) ; รัน → logOpenApiExecuteLog(เดิม)
REQ-WPT-002 — Central User–Subsystem Membership¶
MembershipService:GET /api/me/memberships— หลัง login ด้วย ThaiD เอาCitizenId(=UserProfile.Pid) → queryPORTAL.UserSubsystemMembership WHERE CitizenId=@pid AND MembershipStatus='MEMBER'POST /api/memberships(จากระบบย่อยตอนอนุมัติสมาชิก) → upsert membership อัตโนมัติ- เป็น source of truth ของ "ผู้ใช้เป็นสมาชิกระบบไหนบ้าง" สำหรับ Card (WPT-003)
REQ-WPT-003 — Card UI ตามสมาชิก¶
GET /api/portal/cards→ joinPORTAL.SubsystemCard×UserSubsystemMembership(ตามCitizenId)Visibility='MEMBER_ONLY'→ แสดงเฉพาะระบบที่เป็นสมาชิก ;PUBLIC→ ทุกคน- Breadcrumb:
SubsystemCard.BreadcrumbRoot→ frontend ต่อเป็น "OCIPA › คำขอเยียวยา"
REQ-WPT-004 — SSL/TLS รวมศูนย์¶
- ไม่มีงาน backend code — ตั้งที่ API Gateway / reverse proxy (wildcard cert, SSL termination)
- เอกสาร config + ตรวจ TLS 1.2+ (NFR-S01) เท่านั้น
REQ-WPT-005 — Authorization รวมศูนย์¶
- ฟอร์มขอสิทธิ์:
POST /api/permission-requests→PORTAL.PermissionRequest(reuse) +RequestedPermissions(ราย system+roles) - ช่อง "ขอสิทธิ์ AD เท่านั้น" (O-8) → set
PermissionRequest.AdOnlyAccess=1(ไม่บังคับเลือกระบบย่อย) - Routing + approval:
ApprovalServiceแตกPORTAL.PermissionApprovalรายSystemCode→ หา owner จากPORTAL.SubsystemOwner→ ส่งPORTAL.Notification - เจ้าของระบบ
PATCH /api/approvals/{id}(APPROVED/REJECTED) — อนุมัติได้ใน Portal จุดเดียว - เมื่อทุก approval เสร็จ → อัปเดต
PermissionRequest.Status+ เขียน role ลงUserRole(reuse) และ sync เป็น Keycloak client roles (ผ่าน Keycloak Admin API) - รวม role ทุกระบบ: อ่าน
ModuleRoles(reuse) มาแสดงให้เลือกในฟอร์มเดียว - Portal = single authorization authority ของทุกระบบ (รวม S8):
- ทุกระบบย่อยลงทะเบียน role catalog ที่
PORTAL.ModuleRoles(SystemCodeของระบบนั้น เช่นS8) + เป็น row ในRLPDSystems - การมอบสิทธิ์ทำที่นี่จุดเดียว → sync ไป Keycloak (client role ต่อระบบ เช่น client
s8-cms) - ทุกระบบ enforce จาก JWT (
resource_access[client].roles) — ไม่มีระบบไหนเก็บ user-role ของตัวเอง
REQ-WPT-006 — ThaiD/Digital ID/AD + SSO¶
- Keycloak realm กำหนด Identity Providers: ThaiD (OIDC), Digital ID (OIDC), AD (LDAP federation / SAML)
- หลัง broker login → upsert
PORTAL.UserProfile(เดิม) + บันทึกPORTAL.UserIdentity(provider + external subject) ;IsPrimary= ThaiD - Alt credential (O-2, CONSULTANT_USER): ผู้ที่ใช้ ThaiD ไม่ได้ → provider
LOCALในAuthProvider/UserIdentity - SSO + dynamic downstream (O-3): Keycloak ออก SSO token ; portal บันทึก
PORTAL.SsoSession; กด Card → ออกPORTAL.SsoDownstreamGrant(assertion ไปยังระบบปลายทาง) เข้าระบบย่อยโดยไม่ login ซ้ำ (OIDC/SAML)
REQ-WPT-007 — Login Security Policy (config ได้)¶
- Config back-office:
PORTAL.SecurityPolicy(1 แถว) —GET/PUT /api/admin/security-policy(PORTAL_ADMIN) → มีผลทันที ไม่ต้อง deploy (NFR-M06) - Lockout: ทุก login → insert
PORTAL.LoginAttempt; ถ้า fail ≥MaxFailedAttempts(4) ใน window → ตั้งUserSecurityStatus.LockedUntil = now + LockoutMinutes(5) - Password aging: เช็ค
UserSecurityStatus.PasswordChangedAt>PasswordMaxAgeDays(60) → บังคับเปลี่ยน (MustChangePassword=1) - Inactive:
LastLoginAtห่าง >InactiveDays(45) → บังคับเปลี่ยนรหัสตอน login ครั้งถัดไป - No reuse: ตอนเปลี่ยนรหัส เทียบ hash กับ
PORTAL.PasswordHistory(ReuseHistoryCount) ; admin toggleBypassPasswordReuse(CR-4) ข้ามได้ - หมายเหตุ: ถ้า password lifecycle อยู่ใน Keycloak → ตั้ง policy ที่ Keycloak ด้วย และ mirror ค่าจาก
SecurityPolicy(single config source)
REQ-WPT-008 — Dashboard + สถิติ + Log + Export¶
- Audit middleware: ทุก action สำคัญ →
PORTAL.AuditLog; ทุก login/เข้าระบบ →SystemAccessLog(reuse) - Rollup job (cron รายวัน): aggregate
SystemAccessLog/AuditLog→PORTAL.UsageStatDaily(per system/วัน) → dashboard ดึงเร็ว (NFR-P02 ≤5s/30วัน) GET /api/dashboard?from&to&granularity(รายวัน/สัปดาห์/เดือน/ปี) → อ่านUsageStatDaily- User/Audit Log search:
GET /api/audit?userId&from&to(AUDITOR) →AuditLog/SystemAccessLog - Export:
GET /api/dashboard/export?format=xlsx|csv(app layer สร้างไฟล์ ≤10s)
REQ-WPT-009 — Satisfaction Survey¶
- แสดงแบบประเมิน: icon บนเว็บ / popup หลังใช้งาน (เช่น หลังยื่น OCIPA สำเร็จ) →
GET /api/surveys/active?system= - บันทึก:
POST /api/surveys/{id}/responses→PORTAL.SurveyResponse(ผู้ตอบ: เพศ/อายุ/วุฒิ/สถานภาพ) +SurveyAnswer(rating 1–5/choice/text) - คำถาม:
PORTAL.Survey+SurveyQuestion(SectionRESPONDENT/SYSTEM/FORMAT/CONTENT/OVERALL) - รายงานหลังบ้าน: สรุปภาพรวม + แยกตามระบบ (
SurveyResponse.SystemCode) + Export Excel/CSV
4. API Surface (สรุป)¶
| Endpoint | Method | Auth | ตารางหลัก | REQ |
|---|---|---|---|---|
/api/me/memberships |
GET | ThaiD | UserSubsystemMembership | 002 |
/api/portal/cards |
GET | login | SubsystemCard (+membership) | 003 |
/api/permission-requests |
POST | login | PermissionRequest, RequestedPermissions | 005 |
/api/approvals/{id} |
PATCH | SubsystemOwner | PermissionApproval, Notification, UserRole | 005 |
/api/auth/callback (Keycloak) |
GET | broker | UserProfile, UserIdentity, SsoSession | 006 |
/api/sso/launch/{system} |
POST | SSO | SsoDownstreamGrant | 006 |
/api/admin/security-policy |
GET/PUT | PORTAL_ADMIN | SecurityPolicy | 007 |
/api/sc/intake |
POST | API key | ServiceCenterRequest, ServiceCenterRouting | 001 |
/api/sc/routing/{id} |
PATCH | staff | ServiceCenterRouting | 001 |
/api/published-queries |
CRUD | EXTERNAL_AGENCY | PublishedQuery, ApiTokens | 001 |
/api/dashboard, /api/audit, /export |
GET | EXEC/AUDITOR | UsageStatDaily, AuditLog, SystemAccessLog | 008 |
/api/surveys/* |
GET/POST | public/admin | Survey, SurveyQuestion, SurveyResponse, SurveyAnswer | 009 |
5. AuthZ (Roles)¶
7 roles (PORTAL_ADMIN, EXECUTIVE, AUDITOR, INTERNAL_USER, EXTERNAL_USER, EXTERNAL_AGENCY, CONSULTANT_USER) — map เป็น Keycloak roles/groups + UserRole(reuse, JSON ราย system) ; ตรวจสิทธิ์ที่ middleware
6. NFR Implementation¶
| NFR | วิธีทำ |
|---|---|
| S06 lockout / S07 aging | SecurityPolicy + LoginAttempt + UserSecurityStatus + PasswordHistory |
| S08 auth | Keycloak (ThaiD/Digital ID/AD) + SSO (OIDC/SAML) |
| S01 TLS / WPT-004 | API Gateway SSL termination (infra) |
| M02 logging | structured log → ELK ; AuditLog/SystemAccessLog |
| P02 dashboard ≤5s | pre-aggregate UsageStatDaily |
| M03 API doc | OpenAPI 3.0 ทุก endpoint + EndpointRegistry |
7. ลำดับงาน (Build Order)¶
- ALTER
PermissionRequest ADD AdOnlyAccess+ สร้าง 23 ตารางใหม่ (ตามลำดับ FK ใน schema.sql) - Auth/SSO: Keycloak IdP config +
AuthProvider/UserIdentity/SsoSession(WPT-006) - Membership + Cards (WPT-002/003)
- Security policy + lockout/aging (WPT-007) + back-office config
- Permission approval routing + Notification (WPT-005)
- Service Center + routing + PublishedQuery (WPT-001)
- Audit/AuditLog + rollup job + Dashboard/Export (WPT-008)
- Survey (WPT-009)
8. Rollout (extension — ไม่ใช่ migration ใหญ่)¶
- รัน
schema.sqlบนPSDBPRDDB(ALTER ปลอดภัย:AdOnlyAccessมี DEFAULT) - Seed:
AuthProvider(THAID/DIGITAL_ID/AD/LOCAL),SecurityPolicy(ค่า O-7: 4/5/60/45),SubsystemCard(รายRLPDSystems),SubsystemOwner - Backfill:
UserSecurityStatusจากUserProfileที่มีอยู่ ;UserSubsystemMembershipจากข้อมูลสมาชิกเดิมของแต่ละระบบย่อย - ตรวจ collation ของคอลัมน์ string FK ให้ตรงกับ
PSDBPRDDB(ดู note ใน schema.sql)
9. SSO Integration Status¶
| ส่วน | สถานะ | หมายเหตุ |
|---|---|---|
Keycloak (realm rlpd, sso.rlpd.go.th) |
✅ พร้อม (Phase 1) | OIDC/OAuth2 — portal ผูกอยู่แล้ว (UserProfile.Id = Keycloak UUID) |
| ThaiD / Digital ID | ✅ พร้อม | login ประชาชนผ่าน ThaiD ใช้ได้จริง (ไม่ใช่ open item แล้ว) |
| AD (เจ้าหน้าที่) | ✅ พร้อม | Keycloak ↔ LDAP federation |
| SSO ไปทุกระบบใหม่ Phase 2 | ⬜ onboard เพิ่ม | สร้าง OIDC client ต่อระบบย่อย (REQ-WPT-001/006) |
| Dynamic downstream SSO (O-3) | 🟡 ออกแบบ | ระบบย่อยต้องรองรับ SAML/OIDC SP — เก็บ grant ใน SsoDownstreamGrant |
หมายเหตุ: DXC SSO (
sso.dxc.go.th) แยกต่างหากสำหรับ WPIS/OCIPA (ตรวจประวัติ) — การรวม SSO ต้องครอบคลุมกรณีนี้ด้วย
10. Dependencies / Open items¶
- นิยาม "เคส" + สิทธิ์เจ้าหน้าที่ปลายทาง สำหรับ multi-case (O-5)
- ขอบเขตการ inherit Query Builder จาก Phase 1 (O-6)
- ระบบย่อยแต่ละตัวต้องรองรับ SAML/OIDC SP สำหรับ dynamic downstream SSO (O-3)
- ยืนยันค่า default นโยบาย login (O-7) — ตั้งผ่าน
SecurityPolicyได้ (มีค่าเริ่มต้นแล้ว)