S8 — Official Website — Backend Implementation Blueprint¶
เอกสารนี้คืออะไร
แผน implement backend สำหรับทีมพัฒนา — ผูก REQ → endpoint → service → ตารางใน schema ใหม่ บน backend เดิมของกรมฯ · ไม่ใช่โค้ดจริง (repo นี้เป็น KB) · อ้างอิง Traceability · Data Dictionary
1. Stack เดิม & จุดที่ต่อเติม¶
| Layer | ของเดิม (SRS Tech Stack) | สิ่งที่ทำใน Phase 2 |
|---|---|---|
| Backend | PHP / .NET (เดิม) | ปรับปรุง CMS + เพิ่ม RESTful Web API layer (REQ-WEB-007) |
| CMS | ระบบจัดการเนื้อหาเดิม | WYSIWYG ใหม่ + Code Mode + Workflow + Auto-resize |
| DB | RLPD_WEB (legacy, .226) |
→ redesign WEB บน PSDBPRDDB (ดู migration) |
| Auth (admin) | Username/Password + Session (เดิม) | เปลี่ยนเป็น Keycloak SSO (ThaiD/AD, realm rlpd) — เลิก local password (ดู §9) |
| Frontend | Responsive (Vertical Template) | WCAG 2.2 AA, 5 templates |
สถาปัตยกรรมเป้าหมาย: แยกเป็น 3 ส่วน — (1) CMS Backend (admin, server-rendered/SPA), (2) Public Web (อ่านเนื้อหา + cache), (3) Web API (/api/v1/* สำหรับระบบภายนอก) ทั้งหมดอ่าน/เขียน schema WEB เดียวกัน
2. Layering (แนะนำ)¶
Controller / API endpoint
└─ Service (business rule + workflow + validation)
└─ Repository (1 ตัวต่อ aggregate: Content, Banner, Media, EmbedCode, ...)
└─ WEB.* (MSSQL)
Cross-cutting: Keycloak SSO middleware (verify JWT, realm `rlpd`) · AuthZ (Role/RoleMenu) · AuditLog middleware · Cache · MediaProcessor · Sanitizer
ทุก write ผ่าน Service เพื่อบังคับ audit (WEB.AuditLog) + soft-delete (IsDeleted) + workflow status
3. REQ-by-REQ Implementation¶
REQ-WEB-001 — WCAG 2.2 AA¶
- ส่วนใหญ่ frontend (contrast, keyboard nav, ARIA, alt text) — ไม่ใช่งาน backend
- Backend ที่ต้องทำ:
- บังคับ
altไม่ว่าง ตอนบันทึก Banner/Media (validation) → เก็บWEB.BannerTranslation.Alt,WEB.MediaAsset.AltText - Accessibility admin module: CRUD
WEB.AccessibilityIssue(Known-Issues register) + หน้า Accessibility Statement - (option) รับผล axe/WAVE scan อัตโนมัติ → insert
AccessibilityIssue(Severity=CRITICAL) - เกณฑ์ผ่าน: รายงาน
AccessibilityIssue WHERE Severity='CRITICAL' AND Status='OPEN'= 0
REQ-WEB-002 — Template & Navbar¶
GET /api/departments— Department Selection (ต้อง cache + ≤2s) →WEB.DepartmentTemplate module(Super Admin): list/seed 5 templates →WEB.Template(Kind,IsStandard)DepartmentTheme service: department เลือก template+สี → upsertWEB.DepartmentTheme(active 1 แถว/department) + logWEB.DepartmentThemeHistory- Navbar CRUD (แก้เองไม่ต้องแก้โค้ด):
WEB.Menu+WEB.MenuTranslation(hierarchical viaParentMenuId), drag-order =Sequence - หน้า public อ่าน template+menu+theme จาก cache
REQ-WEB-003 — CMS (หัวใจของระบบ)¶
WYSIWYG + Code Mode: ฝัง editor (TinyMCE/CKEditor) ฝั่ง frontend; backend เก็บ HTML ดิบใน WEB.ContentArticle.HtmlContent (nvarchar(max), รองรับ ≥1MB) — ห้าม strip tag ตอนสลับ Visual/HTML
Content workflow (ContentWorkflowService):
CONTENT_EDITOR สร้าง/แก้ → WEB.ContentRevision (Status=DRAFT) → submit (PENDING)
CONTENT_APPROVER review → APPROVED/REJECTED (+ReviewNote)
publish → copy revision ล่าสุดที่ APPROVED ลง WEB.ContentArticle + WEB.Content.Status=PUBLISHED
ContentRevision (VersionNo)
Media upload + Auto-resize (MediaProcessor — งานสำคัญ):
upload → validate ชนิดไฟล์กับ WEB.MediaType (jpg/png/gif/webp/pdf/docx/xlsx)
→ เก็บต้นฉบับ WEB.MediaAsset (Width/Height/Checksum)
→ ถ้าเป็นรูป: gen renditions ตามขนาด Section ของ Template
(Banner: Template.BannerMain/ServiceWidth/Height ; Article: ขนาด article)
→ insert หลายแถวใน WEB.MediaVariant (VariantLabel='banner-main'|'article'|'thumb')
Upload/Embed รวมในหน้าเดียวกับสร้างบทความ: endpoint เดียว POST /cms/content/{id}/media ผูก WEB.ContentImage/WEB.ContentAttachFile
REQ-WEB-004 — Embed Code¶
EmbedCode admin(CRUD) →WEB.EmbedCode(PositionHEADER/FOOTER/SECTION,Scope,Provider,IsEnabled)- กติกาสิทธิ์:
Position='HEADER'⇒ ต้องเป็นSUPER_ADMIN(ตั้งRequiresSuperAdmin=1, เช็คใน service) - XSS protection: sanitize/allow-list ก่อนบันทึก + render ผ่าน CSP (NFR-S07) — เก็บ snippet ดิบแต่ render อย่างควบคุม
- ทุกการเปลี่ยนแปลง → insert
WEB.EmbedCodeAudit(Action/OldValue/NewValue) - เปิด/ปิดรายเว็บ/หน่วยงานผ่าน
DepartmentId+IsEnabled - Public render: query
EmbedCode WHERE IsEnabled=1 AND (DepartmentId=@a OR DepartmentId IS NULL)ตามPosition
REQ-WEB-005 — Backend Performance¶
- Article Search (
GET /api/articles/search?q=): ใช้ SQL Server Full-Text Catalog บนWEB.Content(Name),WEB.ContentArticle(Title,HtmlContent)(หรือ Elasticsearch) — เป้า ≤1s/100k บทความ; log →WEB.SearchLog (SearchScope='ARTICLE') - Caching: cache template/menu/published content (Redis/in-memory), invalidate ตอน publish
- Login เร็ว ≤2s: SSO ผ่าน Keycloak — optimize callback/JWT verify + โหลด profile+role (cache), หลีกเลี่ยง N+1
- Perf monitoring: middleware เก็บ response time → log/APM (NFR-P0x)
REQ-WEB-006 — Internal Search (Admin)¶
GET /cms/search?q=ค้น เมนูตั้งค่า (WEB.AdminMenu) + ไฟล์อัปโหลด (WEB.MediaAsset)- จัดกลุ่มผลตามประเภท, รองรับ TH/EN + substring + fuzzy (app-side หรือ FTS)
- กรองตามสิทธิ์: join
WEB.RoleMenuของ role ผู้ใช้ → เห็นเฉพาะที่มีสิทธิ์ - log →
WEB.SearchLog (SearchScope='INTERNAL', UserId)
REQ-WEB-007 — Web API (Data Integration)¶
/api/v1/news,/api/v1/articles,/api/v1/articles/{shortUrl}(read-only, JSON, OpenAPI 3.0)- Auth middleware: API Key (hash เทียบ
WEB.ApiClient.ApiKeyHash) หรือ OAuth2 ; เช็คAllowedIps - Rate limit: ตาม
WEB.ApiClient.RateLimitPerMin(token-bucket; เกิน → 429) - Scope: เช็ค
WEB.ApiClientScope(news:read/article:read, ราย Department) - Versioning: prefix
/api/v1,/api/v2 - Audit: ทุก call →
WEB.ApiAccessLog(Endpoint/StatusCode/ResponseTimeMs)
4. API Surface (สรุป)¶
| Endpoint | Method | Auth | ตารางหลัก | REQ |
|---|---|---|---|---|
/api/departments |
GET | public (cache) | Department | 002 |
/cms/templates, /cms/department-theme |
GET/PUT | SUPER/DEPARTMENT_ADMIN | Template, DepartmentTheme | 002 |
/cms/menus |
CRUD | DEPARTMENT_ADMIN | Menu, MenuTranslation | 002 |
/cms/content, /cms/content/{id}/revisions |
CRUD | EDITOR/APPROVER | Content, ContentArticle, ContentRevision | 003 |
/cms/content/{id}/media |
POST | EDITOR | MediaAsset, MediaVariant, ContentImage | 003 |
/cms/banners |
CRUD | EDITOR | Banner, BannerTranslation, BannerSetting | 003 |
/cms/embed-codes |
CRUD | SUPER(Header)/DEPARTMENT | EmbedCode, EmbedCodeAudit | 004 |
/api/articles/search |
GET | public | Content, ContentArticle (+FTS), SearchLog | 005 |
/cms/search |
GET | admin | AdminMenu, MediaAsset, SearchLog | 006 |
/api/v1/news, /api/v1/articles |
GET | API Key/OAuth | Content, ContentArticle, ApiAccessLog | 007 |
/cms/accessibility-issues |
CRUD | SUPER_ADMIN | AccessibilityIssue | 001 |
5. Auth (SSO) + Authorization (รวมศูนย์)¶
Authentication = Keycloak SSO (realm rlpd, เดียวกับ portal) — ThaiD (ประชาชน/ผู้ดูแลเนื้อหา) / AD (เจ้าหน้าที่)
- login → redirect Keycloak (OIDC) → JWT → middleware verify → map sub (Keycloak UUID) เข้า WEB.[User].KeycloakUserId
- first login ที่ยังไม่มี profile → auto-provision WEB.[User] (ดึง name/email/citizenId จาก token claims)
- ไม่มี password/lockout ใน WEB — credential/lockout/password-aging = Keycloak realm policy
Authorization = กำหนดสิทธิ์ที่เดียวทุกระบบ (REQ-WPT-005) — S8 ไม่เก็บ role/grant เอง:
- Role catalog: 6 roles ของ S8 ลงทะเบียนที่ PORTAL.ModuleRoles (SystemCode='S8') — เห็นรวมกับทุกระบบที่ portal
- การมอบสิทธิ์: ผ่านฟอร์มที่ portal → PORTAL.PermissionRequest/RequestedPermissions → อนุมัติ → เขียน PORTAL.UserRole และ sync เป็น Keycloak client role ของ client s8-cms
- Runtime ใน S8: อ่าน role จาก JWT (resource_access['s8-cms'].roles) — ไม่ query DB หาสิทธิ์ user
- map role→หน้าจอ (config เฉพาะแอป): WEB.RoleMenu (RoleCode = central role code) × WEB.AdminMenu, PermissionLevel 0/1/2 — ใช้กับ Internal Search filter (REQ-WEB-006) ด้วย
- Department scope ของ DEPARTMENT_ADMIN: ส่งมากับ grant (role + department attribute ใน Keycloak group / claim) → S8 filter DepartmentId ใน repository ; WEB.[User].DepartmentId = หน่วยงานต้นสังกัด (default scope)
- API_CONSUMER/PUBLIC_USER ไม่ผ่าน Keycloak admin login (API = key/OAuth ; public = ไม่ต้อง login)
6. NFR Implementation¶
| NFR | วิธีทำ |
|---|---|
| S06 lockout / password | Keycloak realm policy (login ผิด, lockout, password-aging) — ไม่อยู่ใน WEB |
| Auth | Keycloak SSO (ThaiD/AD, OIDC) — WEB.[User] เก็บแค่ profile + CMS roles |
| M02 logging | AuditLog middleware (admin actions) + ApiAccessLog (API) → centralized log (JSON) |
| S07 CSP / XSS | CSP header + sanitize EmbedCode/HtmlContent |
| P04 search | Full-Text Catalog + index ใน schema.sql |
7. ลำดับงาน (Build Order)¶
- Foundation:
Language,Department, Keycloak SSO middleware +Userprofile +AdminMenu/RoleMenu(ลงทะเบียน 6 roles ที่PORTAL.ModuleRoles) - Template/Theme/Menu → Public render + Department Selection (cache)
- Media pipeline (
MediaAsset/MediaVariant) → Banner → Content/Article + workflow - Embed Code + audit
- Search (article FTS + internal) + AuditLog
- Web API (
/api/v1) + OpenAPI doc - Accessibility register + WCAG hardening (frontend)
8. Migration: legacy RLPD_WEB → WEB¶
ETL ครั้งเดียว (script/SSIS) ก่อน cutover:
| legacy | → target | logic |
|---|---|---|
Department |
Department |
map field, set Code จาก subdomain |
Theme_Choice/Theme/Theme_Color |
Template/DepartmentTheme/ThemeColor |
จับคู่ template เดิม → 5 template ใหม่ |
Content (+++,copy1,bk) |
Content |
เลือกเฉพาะตัวจริง ทิ้งตารางซ้ำ; set Status='PUBLISHED' |
Content_Article (+copy1/2/3) |
ContentArticle |
ตัวล่าสุด → article; (ไม่ต้องสร้าง revision ย้อนหลัง) |
AttachFile/Content_IMG |
MediaAsset (+gen MediaVariant) |
re-process รูปเพื่อสร้าง variant |
Menu(+copy1)/Menu_Detail |
Menu/MenuTranslation |
|
Users(+_bk) |
[User] |
provision เข้า Keycloak (ThaiD/AD) → map KeycloakUserId; keep Identity_id→CitizenId; ไม่ migrate password |
Role/UserRoles |
→ PORTAL.ModuleRoles + PORTAL.UserRole + Keycloak | ย้ายไปบริหารกลาง (ลงทะเบียน 6 roles ของ S8 ที่ ModuleRoles) |
Role_Menu/Tm_Menu |
RoleMenu/AdminMenu |
RoleMenu keyed by central role code |
inline Embed_Code |
EmbedCode |
แตกเป็นรายตำแหน่ง |
Survey* |
— | ตัดสินใจ archive/migrate ข้อมูลเดิม (ย้ายแบบประเมินไป S9 REQ-WPT-009) |
Verify หลัง ETL: นับ Content/Banner/Menu/User ต้นทาง vs ปลายทาง, ตรวจ FK ครบ, สุ่มเทียบเนื้อหา
9. SSO Integration Status¶
| ส่วน | สถานะ | หมายเหตุ |
|---|---|---|
Keycloak (realm rlpd, sso.rlpd.go.th) |
✅ พร้อม (Phase 1) | OIDC/OAuth2 — S8 admin เชื่อมเป็น OIDC client เพิ่ม |
| ThaiD / Digital ID | ✅ พร้อม | login ประชาชน/ผู้ดูแลเนื้อหาผ่าน ThaiD |
| AD (เจ้าหน้าที่) | ✅ พร้อม | Keycloak ↔ LDAP federation |
| S8 CMS admin | ⬜ ต้องทำใน Phase 2 | สร้าง Keycloak client ของ S8 + map KeycloakUserId; เลิก local username/password ของ legacy Users |
เปลี่ยนจาก SRS S8 (ที่ระบุ Username/Password local) → ใช้ SSO กลาง ตามทิศทางกรมฯ (ทุกระบบ SSO) · CMS roles ยังบริหารใน
WEB(Keycloak ทำแค่ authentication ไม่ใช่ authorization ของ CMS)
10. Dependencies / Open items¶
- ต้องสรุปกับลูกค้า: redesign สะอาด vs คง schema เดิม (ดู parent decisions)
- ขนาด section ของ 5 template (ใช้ตั้งค่า auto-resize) — ต้องได้จากทีม UX
- เลือก search engine: SQL FTS (ในตัว) หรือ Elasticsearch (แยก service)
- OAuth provider สำหรับ Web API (ถ้าไม่ใช้ API Key) — ตกลงกับหน่วยงานภายนอก
- การ provision ผู้ดูแลเนื้อหาเดิมเข้า Keycloak (สร้าง user ใน realm
rlpd+ ผูก role S8)