diff --git a/docs/edge_cases_quick_reference.md b/docs/edge_cases_quick_reference.md new file mode 100644 index 00000000..fd03ba27 --- /dev/null +++ b/docs/edge_cases_quick_reference.md @@ -0,0 +1,267 @@ +# Edge Cases Quick Reference + +> สรุปกรณีพิเศษที่ต้องระวังในระบบ E-Learning - สำหรับอ้างอิงด่วน + +## 📋 สารบัญ Edge Cases + +| # | Edge Case | ความเสี่ยง | แนวทางแก้ไข | +|---|-----------|-----------|-------------| +| 1 | [การเปลี่ยนบทบาทโดย Admin](#1-การเปลี่ยนบทบาทผู้ใช้) | 🟡 ปานกลาง | Invalidate sessions + audit log | +| 2 | [หลักสูตรถูกซ่อน/ลบ](#2-หลักสูตรถูกซ่อนลบ) | 🟡 ปานกลาง | Read-only mode สำหรับผู้ลงทะเบียนแล้ว | +| 3 | [ลงทะเบียนซ้ำ](#3-ลงทะเบียนซ้ำ) | 🟢 ต่ำ | ตรวจสอบและ redirect | +| 4 | [ข้ามลำดับบทเรียน](#4-ข้ามลำดับบทเรียน) | 🟡 ปานกลาง | ตรวจสอบ prerequisite | +| 5 | [ทำแบบทดสอบหลายครั้ง](#5-ทำแบบทดสอบหลายครั้ง) | 🟡 ปานกลาง | จำกัดครั้ง + cooldown | +| 6 | [แก้ไขขณะมีคนใช้งาน](#6-แก้ไขขณะมีคนใช้งาน) | 🔴 สูง | Soft delete + แจ้งเตือน | +| 7 | [ลบ Instructor ที่มีหลักสูตร](#7-ลบ-instructor-ที่มีหลักสูตร) | 🔴 สูง | บังคับโอนหลักสูตรก่อน | +| 8 | [เข้าถึงข้อมูลข้ามหลักสูตร](#8-เข้าถึงข้อมูลข้ามหลักสูตร) | 🔴 สูง | Middleware ตรวจสอบ ownership | +| 9 | [เพิ่มเนื้อหาหลังจบหลักสูตร](#9-เพิ่มเนื้อหาหลังจบหลักสูตร) | 🟡 ปานกลาง | เนื้อหาใหม่ = เสริม (ไม่บังคับ) | +| 10 | [Maintenance Mode](#10-maintenance-mode) | 🟡 ปานกลาง | Auto-save + แจ้งเตือนล่วงหน้า | + +--- + +## 1. การเปลี่ยนบทบาทผู้ใช้ + +> **หมายเหตุ**: ไม่มีการเปลี่ยนบทบาทระหว่าง Student-Instructor ในการใช้งานปกติ +> เกิดขึ้นเฉพาะกรณีพิเศษที่ Admin จัดการ (เช่น แก้ไขข้อมูลผิดพลาด) + +**ปัญหา**: Token/Session เก่ายังมีสิทธิ์เดิม + +**วิธีแก้**: +```javascript +// Admin เปลี่ยนบทบาท (กรณีพิเศษ) +// 1. บันทึก audit log (ระบุเหตุผล) +// 2. อัปเดตบทบาท +// 3. Invalidate sessions ทั้งหมด +// 4. แจ้งเตือนผู้ใช้ทางอีเมล +// 5. บังคับ login ใหม่ +``` + +**Key Points**: +- ✅ เฉพาะ Admin เท่านั้น +- ✅ ต้องระบุเหตุผลและบันทึก audit log +- ✅ Invalidate sessions ทันที +- ✅ แจ้งเตือนผู้ใช้ทางอีเมล + +--- + +## 2. หลักสูตรถูกซ่อน/ลบ + +**ปัญหา**: Student ที่ลงทะเบียนแล้วควรเข้าถึงได้หรือไม่? + +**วิธีแก้**: +- **ซ่อน**: Student ที่ลงทะเบียนแล้ว → เข้าถึงได้ปกติ +- **ลบ**: Student ที่ลงทะเบียนแล้ว → Read-only mode + +**Key Points**: +- ✅ ใช้ Soft Delete +- ✅ ใบประกาศนียบัตรที่ออกแล้วยังใช้ได้ +- ❌ ห้ามทำแบบทดสอบใหม่ในหลักสูตรที่ถูกลบ + +--- + +## 3. ลงทะเบียนซ้ำ + +**ปัญหา**: ข้อมูลความคืบหน้าจะเป็นอย่างไร? + +**วิธีแก้**: +```javascript +if (existingEnrollment) { + return redirect(`/courses/${courseId}`); +} +``` + +**Key Points**: +- ✅ ตรวจสอบก่อนสร้าง enrollment +- ✅ Redirect ไปที่หลักสูตร +- ❌ ห้ามรีเซ็ตความคืบหน้า + +--- + +## 4. ข้ามลำดับบทเรียน + +**ปัญหา**: Student ใช้ Direct URL ข้ามลำดับ + +**วิธีแก้**: +```javascript +// ตรวจสอบว่าเรียน Lesson ก่อนหน้าครบหรือไม่ +if (!allPreviousCompleted) { + return { access: false, nextAvailableLesson }; +} +``` + +**Key Points**: +- ✅ ตรวจสอบที่ Backend (ห้ามพึ่ง Frontend) +- ✅ แสดง Lesson ถัดไปที่เข้าถึงได้ +- ✅ รองรับหลักสูตรที่ไม่บังคับลำดับ + +--- + +## 5. ทำแบบทดสอบหลายครั้ง + +**ปัญหา**: จำกัดครั้ง? คะแนนไหนที่นับ? รอกี่นาที? + +**วิธีแก้**: +```javascript +// ตรวจสอบ: +// 1. จำนวนครั้งที่ทำได้ (maxAttempts) +// 2. ระยะเวลารอ (cooldownMinutes) +// 3. นโยบายคะแนน (HIGHEST/LATEST/FIRST/AVERAGE) +``` + +**Key Points**: +- ✅ ใช้คะแนนสูงสุด (HIGHEST) เป็นค่าเริ่มต้น +- ✅ กำหนด cooldown ได้ต่อแบบทดสอบ +- ✅ เก็บประวัติการทำทุกครั้ง + +--- + +## 6. แก้ไขขณะมีคนใช้งาน + +**ปัญหา**: Student กำลังดู Lesson อยู่ แต่ Instructor ลบ + +**วิธีแก้**: +```javascript +// ใช้ Soft Delete +if (activeViewers.length > 0) { + markAsDeleted(); // ลบถาวรใน 24 ชม. + notifyInstructor(); +} +``` + +**Key Points**: +- ✅ ใช้ Soft Delete แทน Hard Delete +- ✅ แจ้งเตือน Instructor ว่ามีคนดูอยู่ +- ✅ Student ที่เข้าถึงก่อนหน้า → ดูต่อได้ + +--- + +## 7. ลบ Instructor ที่มีหลักสูตร + +**ปัญหา**: หลักสูตรจะกลายเป็น "ไม่มีเจ้าของ" + +**วิธีแก้**: +```javascript +if (courses.length > 0) { + return error('กรุณาโอนหลักสูตรก่อน'); +} +// ใช้ Soft Delete (deactivate) +``` + +**Key Points**: +- ✅ บังคับโอนหลักสูตรก่อนลบ +- ✅ ใช้ Soft Delete (deactivate) +- ✅ เก็บ Audit Log + +--- + +## 8. เข้าถึงข้อมูลข้ามหลักสูตร + +**ปัญหา**: Instructor A ดูข้อมูลหลักสูตรของ Instructor B + +**วิธีแก้**: +```javascript +// Middleware ตรวจสอบ ownership +if (role === 'INSTRUCTOR' && course.instructorId !== userId) { + return 403; +} +``` + +**Key Points**: +- ✅ ตรวจสอบ ownership ที่ Backend +- ✅ Admin → read-only access +- ✅ Instructor → เฉพาะหลักสูตรของตนเอง + +--- + +## 9. เพิ่มเนื้อหาหลังจบหลักสูตร + +**ปัญหา**: ความคืบหน้ากลับไม่ครบ 100% + +**วิธีแก้**: +```javascript +// คำนวณจากเนื้อหาตอนลงทะเบียน +if (originalProgress === 100 && certificateIssued) { + return { progress: 100, newContentAvailable: true }; +} +``` + +**Key Points**: +- ✅ ใบประกาศนียบัตรยังใช้ได้ +- ✅ เนื้อหาใหม่ = เสริม (ไม่บังคับ) +- ✅ แสดงป้าย "มีเนื้อหาใหม่" + +--- + +## 10. Maintenance Mode + +**ปัญหา**: Student กำลังทำแบบทดสอบ + +**วิธีแก้**: +```javascript +// 1. แจ้งเตือนล่วงหน้า 24 ชม. +// 2. Auto-save ทุก 30 วินาที +// 3. เก็บ draft 24 ชม. +// 4. Admin เข้าถึงได้ +``` + +**Key Points**: +- ✅ Auto-save แบบทดสอบทุก 30 วินาที +- ✅ แจ้งเตือนล่วงหน้า +- ✅ กู้คืนข้อมูลได้ภายใน 24 ชม. + +--- + +## 🔒 หลักการสำคัญ (Best Practices) + +### 1. Soft Delete > Hard Delete +- ใช้ `isDeleted` flag แทนการลบจริง +- เก็บข้อมูลไว้สำหรับ audit trail +- ป้องกันการสูญหายของข้อมูล + +### 2. Invalidate Sessions เมื่อเปลี่ยนสิทธิ์ +- เปลี่ยนบทบาท → logout ทันที +- ลบบัญชี → invalidate sessions +- เปลี่ยนรหัสผ่าน → logout devices อื่น + +### 3. ตรวจสอบสิทธิ์ที่ Backend เสมอ +- Frontend = UX (ซ่อน/แสดง UI) +- Backend = Security (ป้องกันการเข้าถึง) +- ห้ามพึ่งพา Frontend เพียงอย่างเดียว + +### 4. Auto-save สำหรับข้อมูลสำคัญ +- แบบทดสอบ → auto-save ทุก 30 วินาที +- เนื้อหาที่กำลังเขียน → auto-save +- เก็บ draft ไว้อย่างน้อย 24 ชม. + +### 5. แจ้งเตือนผู้ใช้ +- Maintenance → แจ้งล่วงหน้า 24 ชม. +- ลบเนื้อหา → แจ้งว่ามีคนดูอยู่ +- เปลี่ยนแปลงสำคัญ → ส่งอีเมลแจ้ง + +--- + +## 📊 สรุปความเสี่ยงและลำดับความสำคัญ + +### 🔴 ความเสี่ยงสูง (ต้องจัดการก่อน) +1. แก้ไขขณะมีคนใช้งาน +2. ลบ Instructor ที่มีหลักสูตร +3. เข้าถึงข้อมูลข้ามหลักสูตร + +### 🟡 ความเสี่ยงปานกลาง +4. การเปลี่ยนบทบาทโดย Admin (กรณีพิเศษ) +5. หลักสูตรถูกซ่อน/ลบ +6. ข้ามลำดับบทเรียน +7. ทำแบบทดสอบหลายครั้ง +8. เพิ่มเนื้อหาหลังจบหลักสูตร +9. Maintenance Mode + +### 🟢 ความเสี่ยงต่ำ +10. ลงทะเบียนซ้ำ + +--- + +## 🔗 เอกสารที่เกี่ยวข้อง + +- [ตารางสิทธิ์แบบเต็ม](./permissions_matrix.md) +- [User Roles and Capabilities](./user_roles_and_capabilities.md) +- [System Architecture](./elearning_architecture.md) diff --git a/docs/permissions_matrix.md b/docs/permissions_matrix.md new file mode 100644 index 00000000..67466f09 --- /dev/null +++ b/docs/permissions_matrix.md @@ -0,0 +1,792 @@ +# ตารางสิทธิ์การใช้งานระบบ E-Learning (Permissions Matrix) + +## รายการสิทธิ์ทั้งหมด (Permission List) + +### 1. การจัดการผู้ใช้งาน (User Management) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `user.register` | ลงทะเบียนผู้ใช้ใหม่ | สามารถสร้างบัญชีผู้ใช้ใหม่ในระบบ | +| `user.login` | เข้าสู่ระบบ | สามารถเข้าสู่ระบบด้วยอีเมล/รหัสผ่าน | +| `user.profile.view` | ดูโปรไฟล์ตนเอง | สามารถดูข้อมูลโปรไฟล์ของตนเอง | +| `user.profile.edit` | แก้ไขโปรไฟล์ตนเอง | สามารถแก้ไขข้อมูลส่วนตัวของตนเอง | +| `user.password.reset` | รีเซ็ตรหัสผ่าน | สามารถรีเซ็ตรหัสผ่านผ่านอีเมล | +| `user.list.view` | ดูรายการผู้ใช้ทั้งหมด | สามารถดูรายชื่อผู้ใช้ทั้งหมดในระบบ | +| `user.manage` | จัดการผู้ใช้งาน | สามารถสร้าง แก้ไข ลบบัญชีผู้ใช้ | +| `user.role.assign` | กำหนดบทบาทผู้ใช้ | สามารถกำหนดและเปลี่ยนบทบาทของผู้ใช้ | + +--- + +### 2. การจัดการหลักสูตร (Course Management) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `course.list.view` | ดูรายการหลักสูตร | สามารถดูรายการหลักสูตรที่เผยแพร่ | +| `course.detail.view` | ดูรายละเอียดหลักสูตร | สามารถดูรายละเอียดของหลักสูตร | +| `course.search` | ค้นหาหลักสูตร | สามารถค้นหาหลักสูตรในระบบ | +| `course.enroll` | ลงทะเบียนเรียน | สามารถลงทะเบียนเรียนหลักสูตร | +| `course.create` | สร้างหลักสูตร | สามารถสร้างหลักสูตรใหม่ | +| `course.edit` | แก้ไขหลักสูตร | สามารถแก้ไขรายละเอียดหลักสูตร | +| `course.delete` | ลบหลักสูตร | สามารถลบหลักสูตร | +| `course.status.manage` | จัดการสถานะหลักสูตร | สามารถเปลี่ยนสถานะเผยแพร่/ซ่อนหลักสูตร | +| `course.category.manage` | จัดการหมวดหมู่หลักสูตร | สามารถกำหนดหมวดหมู่หลักสูตร | + +--- + +### 3. การจัดการเนื้อหาบทเรียน (Content Management) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `content.view` | ดูเนื้อหาบทเรียน | สามารถเข้าถึงและดูเนื้อหาบทเรียน | +| `content.video.play` | เล่นวิดีโอ | สามารถเล่นวิดีโอการเรียนรู้ | +| `content.document.view` | ดูเอกสาร | สามารถดูเอกสาร PDF | +| `content.document.download` | ดาวน์โหลดเอกสาร | สามารถดาวน์โหลดเอกสารประกอบ | +| `content.progress.track` | ติดตามความคืบหน้า | ระบบบันทึกความคืบหน้าการเรียน | +| `content.chapter.create` | สร้าง Chapter | สามารถสร้าง Chapter ในหลักสูตร | +| `content.lesson.create` | สร้าง Lesson | สามารถสร้าง Lesson ในหลักสูตร | +| `content.chapter.edit` | แก้ไข Chapter | สามารถแก้ไข Chapter | +| `content.lesson.edit` | แก้ไข Lesson | สามารถแก้ไข Lesson | +| `content.upload` | อัปโหลดเนื้อหา | สามารถอัปโหลดวิดีโอ, PDF, ไฟล์ประกอบ | +| `content.order.manage` | จัดเรียงลำดับบทเรียน | สามารถจัดเรียงลำดับ Chapter และ Lesson | +| `content.access.manage` | กำหนดเงื่อนไขการเข้าถึง | สามารถกำหนดเงื่อนไขการเข้าถึงบทเรียน | + +--- + +### 4. การจัดการแบบทดสอบ (Quiz & Assessment) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `quiz.take` | ทำแบบทดสอบ | สามารถทำแบบทดสอบออนไลน์ | +| `quiz.result.view` | ดูผลคะแนนตนเอง | สามารถดูผลคะแนนและประวัติการทำแบบทดสอบของตนเอง | +| `quiz.create` | สร้างแบบทดสอบ | สามารถสร้างแบบทดสอบใหม่ | +| `quiz.edit` | แก้ไขแบบทดสอบ | สามารถแก้ไขแบบทดสอบ | +| `quiz.delete` | ลบแบบทดสอบ | สามารถลบแบบทดสอบ | +| `quiz.settings.manage` | กำหนดการตั้งค่าแบบทดสอบ | สามารถกำหนดเวลา คะแนน เกณฑ์ผ่าน | +| `quiz.results.view.all` | ดูผลคะแนนผู้เรียนทั้งหมด | สามารถดูผลคะแนนและสถิติของผู้เรียนทั้งหมด | + +--- + +### 5. การรายงานและประเมินผล (Reporting) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `report.progress.view.own` | ดูรายงานความคืบหน้าตนเอง | สามารถดูรายงานความคืบหน้าการเรียนของตนเอง | +| `report.score.view.own` | ดูรายงานคะแนนตนเอง | สามารถดูรายงานคะแนนแบบทดสอบของตนเอง | +| `report.certificate.view` | ดูใบประกาศนียบัตร | สามารถดูและดาวน์โหลดใบประกาศนียบัตร | +| `report.students.view` | ดูรายงานผู้เรียน | สามารถดูรายงานผู้เรียนในหลักสูตร | +| `report.progress.view.all` | ดูรายงานความคืบหน้าผู้เรียนทั้งหมด | สามารถดูความคืบหน้าการเรียนของผู้เรียนทั้งหมด | +| `report.export` | ส่งออกรายงาน | สามารถส่งออกรายงานเป็น Excel หรือ PDF | +| `report.dashboard.view` | ดู Dashboard | สามารถดู Dashboard ภาพรวมของระบบ | +| `report.statistics.view` | ดูสถิติระบบ | สามารถดูสถิติการใช้งานระบบโดยรวม | + +--- + +### 6. การจัดการประกาศ (Announcements) + +| Permission Code | ชื่อสิทธิ์ | คำอธิบาย | +|----------------|-----------|---------| +| `announcement.view` | ดูประกาศ | สามารถดูประกาศภายในหลักสูตร | +| `announcement.create` | สร้างประกาศ | สามารถโพสต์ประกาศใหม่ | +| `announcement.edit` | แก้ไขประกาศ | สามารถแก้ไขประกาศ | +| `announcement.delete` | ลบประกาศ | สามารถลบประกาศ | + +--- + +## ตารางสิทธิ์ตามบทบาท (Role-Based Permissions Matrix) + +### สัญลักษณ์ +- ✅ = มีสิทธิ์ +- ❌ = ไม่มีสิทธิ์ +- 🔒 = มีสิทธิ์เฉพาะข้อมูลของตนเอง + +--- + +### 1. การจัดการผู้ใช้งาน (User Management) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `user.register` | ✅ | ✅ | ✅ | +| `user.login` | ✅ | ✅ | ✅ | +| `user.profile.view` | 🔒 | 🔒 | 🔒 | +| `user.profile.edit` | 🔒 | 🔒 | 🔒 | +| `user.password.reset` | ✅ | ✅ | ✅ | +| `user.list.view` | ❌ | ❌ | ✅ | +| `user.manage` | ❌ | ❌ | ✅ | +| `user.role.assign` | ❌ | ❌ | ✅ | + +--- + +### 2. การจัดการหลักสูตร (Course Management) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `course.list.view` | ✅ | ✅ | ✅ | +| `course.detail.view` | ✅ | ✅ | ✅ | +| `course.search` | ✅ | ✅ | ✅ | +| `course.enroll` | ✅ | ❌ | ❌ | +| `course.create` | ❌ | ✅ | ❌ | +| `course.edit` | ❌ | ✅ | ❌ | +| `course.delete` | ❌ | ✅ | ❌ | +| `course.status.manage` | ❌ | ✅ | ❌ | +| `course.category.manage` | ❌ | ✅ | ❌ | + +--- + +### 3. การจัดการเนื้อหาบทเรียน (Content Management) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `content.view` | ✅ | ✅ | ❌ | +| `content.video.play` | ✅ | ✅ | ❌ | +| `content.document.view` | ✅ | ✅ | ❌ | +| `content.document.download` | ✅ | ✅ | ❌ | +| `content.progress.track` | ✅ | ❌ | ❌ | +| `content.chapter.create` | ❌ | ✅ | ❌ | +| `content.lesson.create` | ❌ | ✅ | ❌ | +| `content.chapter.edit` | ❌ | ✅ | ❌ | +| `content.lesson.edit` | ❌ | ✅ | ❌ | +| `content.upload` | ❌ | ✅ | ❌ | +| `content.order.manage` | ❌ | ✅ | ❌ | +| `content.access.manage` | ❌ | ✅ | ❌ | + +--- + +### 4. การจัดการแบบทดสอบ (Quiz & Assessment) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `quiz.take` | ✅ | ❌ | ❌ | +| `quiz.result.view` | 🔒 | ❌ | ❌ | +| `quiz.create` | ❌ | ✅ | ❌ | +| `quiz.edit` | ❌ | ✅ | ❌ | +| `quiz.delete` | ❌ | ✅ | ❌ | +| `quiz.settings.manage` | ❌ | ✅ | ❌ | +| `quiz.results.view.all` | ❌ | ✅ | ❌ | + +--- + +### 5. การรายงานและประเมินผล (Reporting) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `report.progress.view.own` | ✅ | ❌ | ❌ | +| `report.score.view.own` | ✅ | ❌ | ❌ | +| `report.certificate.view` | ✅ | ❌ | ❌ | +| `report.students.view` | ❌ | ✅ | ❌ | +| `report.progress.view.all` | ❌ | ✅ | ❌ | +| `report.export` | ❌ | ✅ | ❌ | +| `report.dashboard.view` | ❌ | ❌ | ✅ | +| `report.statistics.view` | ❌ | ❌ | ✅ | + +--- + +### 6. การจัดการประกาศ (Announcements) + +| สิทธิ์ | Student | Instructor | Admin | +|-------|---------|------------|-------| +| `announcement.view` | ✅ | ✅ | ❌ | +| `announcement.create` | ❌ | ✅ | ❌ | +| `announcement.edit` | ❌ | ✅ | ❌ | +| `announcement.delete` | ❌ | ✅ | ❌ | + +--- + +## สรุปจำนวนสิทธิ์ตามบทบาท + +| บทบาท | จำนวนสิทธิ์ทั้งหมด | สิทธิ์เต็ม | สิทธิ์เฉพาะตนเอง | ไม่มีสิทธิ์ | +|--------|-------------------|-----------|------------------|------------| +| **Student** | 52 | 21 | 5 | 26 | +| **Instructor** | 52 | 30 | 3 | 19 | +| **Admin** | 52 | 9 | 3 | 40 | + +--- + +## หมายเหตุสำคัญ + +### 1. การควบคุมสิทธิ์ระดับข้อมูล (Data-Level Access Control) + +#### Student +- สามารถดูและแก้ไขเฉพาะโปรไฟล์ของตนเอง +- สามารถดูผลคะแนนและความคืบหน้าเฉพาะของตนเอง +- ไม่สามารถเข้าถึงข้อมูลของผู้เรียนคนอื่น + +#### Instructor +- สามารถจัดการเฉพาะหลักสูตรที่ตนเองสร้างหรือได้รับมอบหมาย +- สามารถดูข้อมูลผู้เรียนเฉพาะในหลักสูตรของตนเอง +- ไม่สามารถเข้าถึงข้อมูลระดับระบบโดยรวม + +#### Admin +- มีสิทธิ์เข้าถึงข้อมูลผู้ใช้ทั้งหมด +- มีสิทธิ์ดูสถิติและรายงานระดับระบบ +- ไม่มีสิทธิ์จัดการเนื้อหาการสอนโดยตรง (เป็นหน้าที่ของ Instructor) + +--- + +### 2. การนำไปใช้งาน (Implementation Guidelines) + +#### Backend API +```javascript +// ตัวอย่างการตรวจสอบสิทธิ์ +const checkPermission = (user, permission) => { + const rolePermissions = { + STUDENT: ['user.login', 'course.enroll', 'content.view', ...], + INSTRUCTOR: ['user.login', 'course.create', 'content.upload', ...], + ADMIN: ['user.manage', 'user.role.assign', 'report.dashboard.view', ...] + }; + + return rolePermissions[user.role]?.includes(permission); +}; +``` + +#### Frontend UI +```javascript +// ตัวอย่างการซ่อน/แสดง UI ตามสิทธิ์ +const canCreateCourse = hasPermission('course.create'); // true สำหรับ Instructor +const canManageUsers = hasPermission('user.manage'); // true สำหรับ Admin +``` + +--- + +### 3. ความปลอดภัย (Security Considerations) + +> [!IMPORTANT] +> - การตรวจสอบสิทธิ์ต้องทำทั้งที่ Frontend และ Backend +> - Frontend ใช้สำหรับ UX (ซ่อน/แสดง UI) +> - Backend ใช้สำหรับ Security (ป้องกันการเข้าถึงที่ไม่ได้รับอนุญาต) + +> [!WARNING] +> - ห้ามพึ่งพาการตรวจสอบสิทธิ์ที่ Frontend เพียงอย่างเดียว +> - ต้องมีการตรวจสอบสิทธิ์ที่ API Layer ทุกครั้ง + +--- + +### 4. กรณีพิเศษและ Edge Cases ที่ต้องจัดการ + +> [!CAUTION] +> กรณีพิเศษเหล่านี้ต้องได้รับการจัดการอย่างถูกต้องเพื่อป้องกันช่องโหว่ด้านความปลอดภัยและปัญหาการใช้งาน + +--- + +#### 4.1 การเปลี่ยนแปลงบทบาทผู้ใช้โดย Admin (Admin Role Management) + +**สถานการณ์**: Admin เปลี่ยนบทบาทผู้ใช้ (เช่น ปรับสถานะบัญชี, แก้ไขข้อมูลผิดพลาด) + +> [!NOTE] +> ในระบบนี้ **ไม่มีการเปลี่ยนบทบาทระหว่าง Student และ Instructor** ในการใช้งานปกติ +> - Student ลงทะเบียนเป็น Student ตั้งแต่แรก +> - Instructor สมัครหรือได้รับเชิญเป็น Instructor ตั้งแต่แรก +> - การเปลี่ยนบทบาทเกิดขึ้นเฉพาะกรณีพิเศษที่ Admin จัดการ (เช่น แก้ไขข้อมูลผิดพลาด) + +**ปัญหาที่อาจเกิด** (กรณีพิเศษที่ Admin ต้องเปลี่ยนบทบาท): +- Session/Token เก่าอาจยังมีสิทธิ์เดิมอยู่ +- ข้อมูลที่เกี่ยวข้องกับบทบาทเดิม (เช่น หลักสูตรที่สร้าง, การลงทะเบียน) + +**วิธีจัดการ**: +```javascript +// Admin เปลี่ยนบทบาทผู้ใช้ (กรณีพิเศษเท่านั้น) +async function adminChangeUserRole(userId, newRole, adminId, reason) { + const user = await getUser(userId); + const oldRole = user.role; + + // บันทึก audit log + await logRoleChange(userId, oldRole, newRole, adminId, reason); + + // 1. อัปเดตบทบาทในฐานข้อมูล + await updateUserRole(userId, newRole); + + // 2. Invalidate all active sessions/tokens + await invalidateUserSessions(userId); + + // 3. แจ้งเตือนผู้ใช้ทางอีเมล + await notifyUserRoleChange(userId, oldRole, newRole, reason); + + // 4. บังคับให้ Login ใหม่ + return { + success: true, + requireRelogin: true, + message: 'เปลี่ยนบทบาทเรียบร้อย ผู้ใช้จะต้อง Login ใหม่' + }; +} +``` + +--- + +#### 4.2 การเข้าถึงหลักสูตรที่ถูกซ่อน/ลบ (Hidden/Deleted Course Access) + +**สถานการณ์**: Student ลงทะเบียนเรียนหลักสูตรแล้ว แต่ Instructor เปลี่ยนสถานะเป็น "ซ่อน" หรือลบหลักสูตร + +**ปัญหาที่อาจเกิด**: +- Student ที่ลงทะเบียนไว้แล้วควรเข้าถึงได้หรือไม่? +- ความคืบหน้าการเรียนจะเกิดอะไร? +- ใบประกาศนียบัตรที่ได้รับไปแล้วยังใช้ได้หรือไม่? + +**วิธีจัดการ**: +```javascript +// ตรวจสอบการเข้าถึงหลักสูตร +async function canAccessCourse(userId, courseId) { + const course = await getCourse(courseId); + const enrollment = await getEnrollment(userId, courseId); + + // กรณีหลักสูตรถูกลบ + if (course.isDeleted) { + // อนุญาตให้ดูเฉพาะ read-only mode (ดูเนื้อหาเก่าได้ แต่ไม่สามารถทำแบบทดสอบ) + return enrollment ? { access: 'read-only', reason: 'course_deleted' } : false; + } + + // กรณีหลักสูตรถูกซ่อน + if (course.status === 'HIDDEN') { + // Student ที่ลงทะเบียนไว้แล้วยังเข้าถึงได้ปกติ + if (enrollment) return { access: 'full' }; + + // Student ใหม่ไม่สามารถลงทะเบียนได้ + return false; + } + + return { access: 'full' }; +} +``` + +**นโยบายที่แนะนำ**: +- หลักสูตรที่ถูก **ซ่อน**: Student ที่ลงทะเบียนแล้วยังเข้าถึงได้ปกติ +- หลักสูตรที่ถูก **ลบ**: เปลี่ยนเป็น Read-Only Mode (ดูเนื้อหาได้ แต่ไม่สามารถทำแบบทดสอบหรือรับใบประกาศนียบัตรใหม่) + +--- + +#### 4.3 การลงทะเบียนซ้ำ (Duplicate Enrollment) + +**สถานการณ์**: Student พยายามลงทะเบียนหลักสูตรที่ลงทะเบียนไว้แล้ว + +**ปัญหาที่อาจเกิด**: +- ข้อมูลความคืบหน้าเดิมจะเป็นอย่างไร? +- ควรรีเซ็ตความคืบหน้าหรือไม่? + +**วิธีจัดการ**: +```javascript +async function enrollCourse(userId, courseId) { + const existingEnrollment = await getEnrollment(userId, courseId); + + if (existingEnrollment) { + // ถ้าลงทะเบียนไว้แล้ว ให้ redirect ไปที่หลักสูตร + return { + success: false, + message: 'คุณได้ลงทะเบียนหลักสูตรนี้ไว้แล้ว', + redirectTo: `/courses/${courseId}` + }; + } + + // สร้างการลงทะเบียนใหม่ + return await createEnrollment(userId, courseId); +} +``` + +--- + +#### 4.4 การเข้าถึงเนื้อหาที่ยังไม่ถึงลำดับ (Sequential Content Access) + +**สถานการณ์**: Student พยายามเข้าถึง Lesson 5 โดยที่ยังไม่ได้เรียน Lesson 1-4 + +**ปัญหาที่อาจเกิด**: +- Student อาจใช้ Direct URL เพื่อข้ามลำดับ +- ระบบต้องบังคับให้เรียนตามลำดับ + +**วิธีจัดการ**: +```javascript +async function canAccessLesson(userId, lessonId) { + const lesson = await getLesson(lessonId); + const course = await getCourse(lesson.courseId); + + // ถ้าหลักสูตรไม่บังคับลำดับ + if (!course.requireSequentialAccess) { + return true; + } + + // ตรวจสอบว่าเรียน Lesson ก่อนหน้าครบหรือไม่ + const previousLessons = await getPreviousLessons(lessonId); + const completedLessons = await getCompletedLessons(userId, lesson.courseId); + + const allPreviousCompleted = previousLessons.every(prev => + completedLessons.includes(prev.id) + ); + + if (!allPreviousCompleted) { + return { + access: false, + reason: 'กรุณาเรียนบทเรียนก่อนหน้าให้เสร็จก่อน', + nextAvailableLesson: findNextAvailableLesson(userId, lesson.courseId) + }; + } + + return { access: true }; +} +``` + +--- + +#### 4.5 การทำแบบทดสอบหลายครั้ง (Multiple Quiz Attempts) + +**สถานการณ์**: Student ทำแบบทดสอบไม่ผ่านและต้องการทำใหม่ + +**ปัญหาที่อาจเกิด**: +- จำกัดจำนวนครั้งในการทำหรือไม่? +- คะแนนที่บันทึกควรเป็นครั้งแรก ครั้งสุดท้าย หรือคะแนนสูงสุด? +- ต้องรอระยะเวลาก่อนทำใหม่หรือไม่? + +**วิธีจัดการ**: +```javascript +async function canTakeQuiz(userId, quizId) { + const quiz = await getQuiz(quizId); + const attempts = await getQuizAttempts(userId, quizId); + + // ตรวจสอบจำนวนครั้งที่ทำได้ + if (quiz.maxAttempts && attempts.length >= quiz.maxAttempts) { + return { + canTake: false, + reason: `คุณทำแบบทดสอบครบ ${quiz.maxAttempts} ครั้งแล้ว` + }; + } + + // ตรวจสอบระยะเวลารอระหว่างการทำ (cooldown) + if (quiz.cooldownMinutes && attempts.length > 0) { + const lastAttempt = attempts[attempts.length - 1]; + const minutesSinceLastAttempt = + (Date.now() - lastAttempt.completedAt) / 1000 / 60; + + if (minutesSinceLastAttempt < quiz.cooldownMinutes) { + const remainingMinutes = Math.ceil( + quiz.cooldownMinutes - minutesSinceLastAttempt + ); + return { + canTake: false, + reason: `กรุณารออีก ${remainingMinutes} นาทีก่อนทำใหม่` + }; + } + } + + return { canTake: true }; +} + +// การบันทึกคะแนน +async function recordQuizScore(userId, quizId, score) { + const quiz = await getQuiz(quizId); + + // กำหนดว่าจะเก็บคะแนนแบบไหน + const scorePolicy = quiz.scorePolicy || 'HIGHEST'; // HIGHEST, LATEST, FIRST, AVERAGE + + await saveQuizAttempt(userId, quizId, score, scorePolicy); +} +``` + +**นโยบายที่แนะนำ**: +- **จำนวนครั้ง**: กำหนดได้ต่อแบบทดสอบ (เช่น 3 ครั้ง, ไม่จำกัด) +- **คะแนนที่บันทึก**: ใช้คะแนนสูงสุด (HIGHEST) เป็นค่าเริ่มต้น +- **Cooldown**: กำหนดได้ต่อแบบทดสอบ (เช่น รอ 30 นาที) + +--- + +#### 4.6 Instructor แก้ไขหลักสูตรขณะที่ Student กำลังเรียน + +**สถานการณ์**: Instructor แก้ไขเนื้อหา/ลบ Lesson ขณะที่ Student กำลังดูอยู่ + +**ปัญหาที่อาจเกิด**: +- Student อาจเห็นข้อผิดพลาด 404 Not Found +- ความคืบหน้าอาจไม่ถูกต้อง +- Video ที่กำลังเล่นอาจหยุดทำงาน + +**วิธีจัดการ**: +```javascript +// Soft Delete แทน Hard Delete +async function deleteLesson(lessonId, instructorId) { + const activeViewers = await getActiveLessonViewers(lessonId); + + if (activeViewers.length > 0) { + // ไม่ลบทันที แต่ทำ Soft Delete + await markLessonAsDeleted(lessonId); + + // แจ้งเตือน Instructor + return { + success: true, + warning: `มีผู้เรียน ${activeViewers.length} คนกำลังดูบทเรียนนี้อยู่ ระบบจะซ่อนบทเรียนนี้และลบถาวรในอีก 24 ชั่วโมง` + }; + } + + // ถ้าไม่มีคนดู ลบได้เลย + await hardDeleteLesson(lessonId); + return { success: true }; +} + +// ตรวจสอบการเข้าถึง Lesson +async function accessLesson(userId, lessonId) { + const lesson = await getLesson(lessonId); + + if (lesson.isDeleted) { + // ถ้า Student เข้าถึงก่อนที่จะถูกลบ ให้ดูต่อได้ + const wasAccessedBefore = await hasAccessedBefore(userId, lessonId); + + if (wasAccessedBefore) { + return { + access: true, + warning: 'บทเรียนนี้จะถูกลบในเร็วๆ นี้' + }; + } + + return { access: false, reason: 'บทเรียนนี้ถูกลบแล้ว' }; + } + + return { access: true }; +} +``` + +--- + +#### 4.7 Admin ลบบัญชี Instructor ที่มีหลักสูตรอยู่ + +**สถานการณ์**: Admin ลบบัญชี Instructor ที่เป็นเจ้าของหลักสูตรหลายรายการ + +**ปัญหาที่อาจเกิด**: +- หลักสูตรจะกลายเป็น "ไม่มีเจ้าของ" +- Student ที่ลงทะเบียนไว้จะเข้าถึงไม่ได้ +- ข้อมูลรายงานจะสูญหาย + +**วิธีจัดการ**: +```javascript +async function deleteInstructor(instructorId, adminId) { + const courses = await getCoursesByInstructor(instructorId); + + if (courses.length > 0) { + // ห้ามลบถ้ายังมีหลักสูตร + return { + success: false, + error: 'ไม่สามารถลบ Instructor ที่มีหลักสูตรอยู่', + suggestion: 'กรุณาโอนหลักสูตรให้ Instructor คนอื่นก่อน', + courses: courses.map(c => ({ id: c.id, title: c.title })) + }; + } + + // Soft Delete แทน Hard Delete + await deactivateUser(instructorId); + + return { + success: true, + message: 'ระงับการใช้งานบัญชีเรียบร้อย' + }; +} + +// ฟังก์ชันโอนหลักสูตร +async function transferCourses(fromInstructorId, toInstructorId, adminId) { + const courses = await getCoursesByInstructor(fromInstructorId); + + for (const course of courses) { + await updateCourseOwner(course.id, toInstructorId); + await logOwnershipTransfer(course.id, fromInstructorId, toInstructorId, adminId); + } + + return { success: true, transferredCount: courses.length }; +} +``` + +**นโยบายที่แนะนำ**: +- ใช้ **Soft Delete** (ระงับการใช้งาน) แทน Hard Delete +- บังคับให้โอนหลักสูตรก่อนลบบัญชี +- เก็บ Audit Log ของการโอนหลักสูตร + +--- + +#### 4.8 การเข้าถึงข้อมูลข้ามหลักสูตร (Cross-Course Data Access) + +**สถานการณ์**: Instructor A พยายามดูรายงานผู้เรียนในหลักสูตรของ Instructor B + +**ปัญหาที่อาจเกิด**: +- Instructor อาจใช้ API โดยตรงเพื่อเข้าถึงข้อมูลหลักสูตรอื่น +- การ Bypass ผ่าน Direct URL + +**วิธีจัดการ**: +```javascript +// Middleware ตรวจสอบความเป็นเจ้าของ +async function checkCourseOwnership(req, res, next) { + const { courseId } = req.params; + const { userId, role } = req.user; + + // Admin สามารถเข้าถึงได้ทุกหลักสูตร (เฉพาะดู ไม่ใช่แก้ไข) + if (role === 'ADMIN') { + req.accessLevel = 'read-only'; + return next(); + } + + // Instructor ต้องเป็นเจ้าของหลักสูตร + if (role === 'INSTRUCTOR') { + const course = await getCourse(courseId); + + if (course.instructorId !== userId) { + return res.status(403).json({ + error: 'คุณไม่มีสิทธิ์เข้าถึงหลักสูตรนี้' + }); + } + + req.accessLevel = 'full'; + return next(); + } + + // Student ต้องลงทะเบียนหลักสูตรนี้ + if (role === 'STUDENT') { + const enrollment = await getEnrollment(userId, courseId); + + if (!enrollment) { + return res.status(403).json({ + error: 'คุณต้องลงทะเบียนหลักสูตรนี้ก่อน' + }); + } + + req.accessLevel = 'student'; + return next(); + } + + return res.status(403).json({ error: 'ไม่มีสิทธิ์เข้าถึง' }); +} + +// ใช้งาน +app.get('/api/courses/:courseId/students', + authenticate, + checkCourseOwnership, + getStudentList +); +``` + +--- + +#### 4.9 Student จบหลักสูตรแล้วแต่ Instructor เพิ่มเนื้อหาใหม่ + +**สถานการณ์**: Student ทำครบทุก Lesson และได้ใบประกาศนียบัตรแล้ว แต่ Instructor เพิ่ม Lesson ใหม่ + +**ปัญหาที่อาจเกิด**: +- ความคืบหน้าจะกลับไปไม่ครบ 100% +- ใบประกาศนียบัตรยังใช้ได้หรือไม่? +- ต้องเรียนเนื้อหาใหม่หรือไม่? + +**วิธีจัดการ**: +```javascript +async function calculateProgress(userId, courseId) { + const enrollment = await getEnrollment(userId, courseId); + const allLessons = await getCourseLessons(courseId); + const completedLessons = await getCompletedLessons(userId, courseId); + + // แยกเนื้อหาเก่าและใหม่ + const lessonsAtEnrollment = allLessons.filter( + lesson => lesson.createdAt <= enrollment.enrolledAt + ); + + const newLessons = allLessons.filter( + lesson => lesson.createdAt > enrollment.enrolledAt + ); + + // คำนวณความคืบหน้าจากเนื้อหาตอนลงทะเบียน + const originalProgress = + (completedLessons.length / lessonsAtEnrollment.length) * 100; + + // ถ้าจบหลักสูตรแล้ว (100% ของเนื้อหาเดิม) + if (originalProgress === 100 && enrollment.certificateIssued) { + return { + progress: 100, + status: 'COMPLETED', + certificate: enrollment.certificateId, + newContentAvailable: newLessons.length > 0, + message: newLessons.length > 0 + ? `มีเนื้อหาใหม่ ${newLessons.length} บทเรียน (ไม่บังคับ)` + : null + }; + } + + // ถ้ายังไม่จบ คำนวณจากเนื้อหาทั้งหมด + const totalProgress = (completedLessons.length / allLessons.length) * 100; + + return { + progress: totalProgress, + status: 'IN_PROGRESS', + remainingLessons: allLessons.length - completedLessons.length + }; +} +``` + +**นโยบายที่แนะนำ**: +- ใบประกาศนียบัตรที่ออกแล้ว **ยังคงใช้ได้** +- เนื้อหาใหม่ถือเป็น **เนื้อหาเสริม** (ไม่บังคับ) +- แสดงป้าย "มีเนื้อหาใหม่" ให้ Student ที่จบแล้วทราบ + +--- + +#### 4.10 ระบบอยู่ในช่วง Maintenance + +**สถานการณ์**: ระบบต้องปิดปรับปรุงชั่วคราว + +**ปัญหาที่อาจเกิด**: +- Student ที่กำลังทำแบบทดสอบจะเกิดอะไร? +- ความคืบหน้าจะสูญหายหรือไม่? + +**วิธีจัดการ**: +```javascript +// Maintenance Mode Middleware +async function maintenanceMode(req, res, next) { + const isMaintenanceMode = await getSystemSetting('maintenance_mode'); + + if (!isMaintenanceMode) { + return next(); + } + + const { role } = req.user || {}; + + // อนุญาตให้ Admin เข้าถึงได้ + if (role === 'ADMIN') { + return next(); + } + + // บันทึกข้อมูลที่กำลังทำอยู่ก่อน + if (req.method === 'POST' || req.method === 'PUT') { + await savePendingRequest(req); + } + + return res.status(503).json({ + error: 'ระบบอยู่ในช่วงปรับปรุง', + message: 'ขออภัยในความไม่สะดวก ระบบจะกลับมาให้บริการเร็วๆ นี้', + estimatedTime: await getMaintenanceEndTime(), + savedData: req.method !== 'GET' // แจ้งว่าบันทึกข้อมูลไว้แล้ว + }); +} + +// Auto-save สำหรับแบบทดสอบ +async function autoSaveQuizProgress(userId, quizId, answers) { + // บันทึกทุก 30 วินาที + await saveQuizDraft(userId, quizId, answers, Date.now()); +} + +// กู้คืนข้อมูลหลัง Maintenance +async function restoreQuizProgress(userId, quizId) { + const draft = await getQuizDraft(userId, quizId); + + if (draft && Date.now() - draft.savedAt < 24 * 60 * 60 * 1000) { + return { + canRestore: true, + answers: draft.answers, + savedAt: draft.savedAt + }; + } + + return { canRestore: false }; +} +``` + +**นโยบายที่แนะนำ**: +- แจ้งเตือนล่วงหน้า 24 ชั่วโมงก่อน Maintenance +- Auto-save ข้อมูลแบบทดสอบทุก 30 วินาที +- เก็บ Draft ไว้ 24 ชั่วโมงหลัง Maintenance +- อนุญาตให้ Admin เข้าถึงระบบได้ระหว่าง Maintenance + +--- + +### 5. การขยายสิทธิ์ในอนาคต (Future Extensions) + +ระบบสามารถขยายเพิ่มสิทธิ์ได้ เช่น: +- `course.moderate` - สำหรับ Moderator ที่ช่วยตรวจสอบหลักสูตร +- `content.review` - สำหรับ Reviewer ที่ตรวจสอบเนื้อหา +- `quiz.manual.grade` - สำหรับการให้คะแนนแบบอัตนัย +- `discussion.moderate` - สำหรับการดูแลกระดานสนทนา (ถ้ามีในอนาคต) +- `course.co-teach` - สำหรับการสอนร่วมกันหลาย Instructor +- `content.version.manage` - สำหรับการจัดการเวอร์ชันของเนื้อหา