docs: Add Forgejo and development setup guides, and refactor API documentation structure.
This commit is contained in:
parent
7b1ed6f6cc
commit
a6427b2083
9 changed files with 2446 additions and 7 deletions
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -66,6 +66,9 @@ Table lessons {
|
||||||
content jsonb [note: 'multi-language lesson content']
|
content jsonb [note: 'multi-language lesson content']
|
||||||
type varchar [not null, note: 'video | pdf | text | quiz']
|
type varchar [not null, note: 'video | pdf | text | quiz']
|
||||||
sort_order int [not null, default: 0, note: 'must be >= 0']
|
sort_order int [not null, default: 0, note: 'must be >= 0']
|
||||||
|
is_sequential boolean [not null, default: true, note: 'require previous lessons to be completed']
|
||||||
|
prerequisite_lesson_ids jsonb [note: 'array of lesson IDs that must be completed first, e.g. [1, 2, 3]']
|
||||||
|
require_pass_quiz boolean [default: false, note: 'require passing quiz to unlock next lessons']
|
||||||
created_at datetime [not null, default: `now()`]
|
created_at datetime [not null, default: `now()`]
|
||||||
updated_at datetime [not null, default: `now()`]
|
updated_at datetime [not null, default: `now()`]
|
||||||
|
|
||||||
|
|
@ -214,6 +217,23 @@ Table announcement_attachments {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Table lesson_attachments {
|
||||||
|
id int [pk, increment]
|
||||||
|
lesson_id int [not null, ref: > lessons.id]
|
||||||
|
file_name varchar [not null]
|
||||||
|
file_path varchar [not null, note: 'S3 key or file path']
|
||||||
|
file_size int [not null, note: 'in bytes']
|
||||||
|
mime_type varchar [not null]
|
||||||
|
description jsonb [note: 'multi-language description']
|
||||||
|
sort_order int [not null, default: 0]
|
||||||
|
created_at datetime [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
lesson_id
|
||||||
|
(lesson_id, sort_order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Table orders {
|
Table orders {
|
||||||
id int [pk, increment]
|
id int [pk, increment]
|
||||||
user_id int [not null, ref: > users.id]
|
user_id int [not null, ref: > users.id]
|
||||||
|
|
|
||||||
520
docs/api-docs/api_attachments_examples.md
Normal file
520
docs/api-docs/api_attachments_examples.md
Normal file
|
|
@ -0,0 +1,520 @@
|
||||||
|
# API Usage Examples - Lesson Attachments
|
||||||
|
|
||||||
|
## 📎 Lesson Attachments Endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Upload Attachment (Instructor)
|
||||||
|
|
||||||
|
### POST `/instructor/courses/:courseId/lessons/:lessonId/attachments`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/lessons/5/attachments
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
file: <slides.pdf>
|
||||||
|
description_th: "สไลด์ประกอบการสอน"
|
||||||
|
description_en: "Lecture slides"
|
||||||
|
sort_order: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 201:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"lesson_id": 5,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"file_path": "lessons/5/slides.pdf",
|
||||||
|
"file_size": 2048576,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"description": {
|
||||||
|
"th": "สไลด์ประกอบการสอน",
|
||||||
|
"en": "Lecture slides"
|
||||||
|
},
|
||||||
|
"sort_order": 1,
|
||||||
|
"download_url": "/api/lessons/5/attachments/10/download",
|
||||||
|
"created_at": "2024-12-23T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 422 (File Too Large):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "FILE_TOO_LARGE",
|
||||||
|
"message": "File size exceeds maximum limit of 100 MB",
|
||||||
|
"file_size": 104857600,
|
||||||
|
"max_size": 104857600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 422 (Too Many Attachments):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "MAX_ATTACHMENTS_EXCEEDED",
|
||||||
|
"message": "Maximum 10 attachments per lesson",
|
||||||
|
"current_count": 10,
|
||||||
|
"max_count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 422 (Invalid File Type):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "INVALID_FILE_TYPE",
|
||||||
|
"message": "File type not allowed",
|
||||||
|
"mime_type": "application/x-executable",
|
||||||
|
"allowed_types": ["application/pdf", "application/zip", "image/jpeg", "..."]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. List Attachments (Instructor)
|
||||||
|
|
||||||
|
### GET `/instructor/courses/:courseId/lessons/:lessonId/attachments`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/instructor/courses/1/lessons/5/attachments
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lesson_id": 5,
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"file_size": 2048576,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"description": {
|
||||||
|
"th": "สไลด์ประกอบการสอน",
|
||||||
|
"en": "Lecture slides"
|
||||||
|
},
|
||||||
|
"sort_order": 1,
|
||||||
|
"download_url": "/api/lessons/5/attachments/10/download",
|
||||||
|
"created_at": "2024-12-23T15:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"file_name": "code.zip",
|
||||||
|
"file_size": 512000,
|
||||||
|
"mime_type": "application/zip",
|
||||||
|
"description": {
|
||||||
|
"th": "โค้ดตัวอย่าง",
|
||||||
|
"en": "Sample code"
|
||||||
|
},
|
||||||
|
"sort_order": 2,
|
||||||
|
"download_url": "/api/lessons/5/attachments/11/download",
|
||||||
|
"created_at": "2024-12-23T15:05:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"file_name": "exercises.docx",
|
||||||
|
"file_size": 256000,
|
||||||
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"description": {
|
||||||
|
"th": "แบบฝึกหัด",
|
||||||
|
"en": "Exercises"
|
||||||
|
},
|
||||||
|
"sort_order": 3,
|
||||||
|
"download_url": "/api/lessons/5/attachments/12/download",
|
||||||
|
"created_at": "2024-12-23T15:10:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Update Attachment Info (Instructor)
|
||||||
|
|
||||||
|
### PUT `/instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
PUT /api/instructor/courses/1/lessons/5/attachments/10
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"th": "สไลด์ประกอบการสอน (ฉบับแก้ไข)",
|
||||||
|
"en": "Lecture slides (revised)"
|
||||||
|
},
|
||||||
|
"sort_order": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"lesson_id": 5,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"file_size": 2048576,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"description": {
|
||||||
|
"th": "สไลด์ประกอบการสอน (ฉบับแก้ไข)",
|
||||||
|
"en": "Lecture slides (revised)"
|
||||||
|
},
|
||||||
|
"sort_order": 2,
|
||||||
|
"updated_at": "2024-12-23T15:20:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Delete Attachment (Instructor)
|
||||||
|
|
||||||
|
### DELETE `/instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
DELETE /api/instructor/courses/1/lessons/5/attachments/10
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 204:
|
||||||
|
```
|
||||||
|
No Content
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Reorder Attachments (Instructor)
|
||||||
|
|
||||||
|
### PUT `/instructor/courses/:courseId/lessons/:lessonId/attachments/reorder`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
PUT /api/instructor/courses/1/lessons/5/attachments/reorder
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"attachments": [
|
||||||
|
{ "id": 11, "sort_order": 1 },
|
||||||
|
{ "id": 12, "sort_order": 2 },
|
||||||
|
{ "id": 10, "sort_order": 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Attachments reordered successfully",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"file_name": "code.zip",
|
||||||
|
"sort_order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"file_name": "exercises.docx",
|
||||||
|
"sort_order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"sort_order": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Get Lesson with Attachments (Student)
|
||||||
|
|
||||||
|
### GET `/students/courses/:courseId/lessons/:lessonId`
|
||||||
|
|
||||||
|
**Changes**: เพิ่ม `attachments` array
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/students/courses/1/lessons/5
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "บทที่ 3: ทฤษฎี",
|
||||||
|
"content": "<p>เนื้อหาบทเรียน...</p>",
|
||||||
|
"type": "video",
|
||||||
|
"video_url": "https://cdn.example.com/videos/lesson-5.m3u8",
|
||||||
|
"duration": "00:25:00",
|
||||||
|
"is_completed": false,
|
||||||
|
"can_access": true,
|
||||||
|
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"file_name": "code.zip",
|
||||||
|
"file_size": 512000,
|
||||||
|
"mime_type": "application/zip",
|
||||||
|
"description": "โค้ดตัวอย่าง",
|
||||||
|
"download_url": "/api/lessons/5/attachments/11/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"file_name": "exercises.docx",
|
||||||
|
"file_size": 256000,
|
||||||
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"description": "แบบฝึกหัด",
|
||||||
|
"download_url": "/api/lessons/5/attachments/12/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"file_size": 2048576,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"description": "สไลด์ประกอบการสอน",
|
||||||
|
"download_url": "/api/lessons/5/attachments/10/download"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Download Attachment
|
||||||
|
|
||||||
|
### GET `/lessons/:lessonId/attachments/:attachmentId/download`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/lessons/5/attachments/10/download
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```http
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/pdf
|
||||||
|
Content-Disposition: attachment; filename="slides.pdf"
|
||||||
|
Content-Length: 2048576
|
||||||
|
|
||||||
|
<binary file data>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 403 (Not Enrolled):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "NOT_ENROLLED",
|
||||||
|
"message": "You must enroll in this course to download attachments",
|
||||||
|
"course_id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Integration Examples
|
||||||
|
|
||||||
|
### 1. Upload Attachment Component
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function uploadAttachment(courseId, lessonId, file, description) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('description_th', description.th);
|
||||||
|
formData.append('description_en', description.en);
|
||||||
|
formData.append('sort_order', 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/instructor/courses/${courseId}/lessons/${lessonId}/attachments`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
|
||||||
|
if (error.error.code === 'FILE_TOO_LARGE') {
|
||||||
|
showAlert('ไฟล์ใหญ่เกินไป (สูงสุด 100 MB)');
|
||||||
|
} else if (error.error.code === 'MAX_ATTACHMENTS_EXCEEDED') {
|
||||||
|
showAlert('แนบไฟล์ได้สูงสุด 10 ไฟล์');
|
||||||
|
} else if (error.error.code === 'INVALID_FILE_TYPE') {
|
||||||
|
showAlert('ประเภทไฟล์ไม่รองรับ');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showSuccess('อัปโหลดไฟล์สำเร็จ');
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
showAlert('เกิดข้อผิดพลาดในการอัปโหลด');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Display Attachments List
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function AttachmentsList({ attachments }) {
|
||||||
|
return (
|
||||||
|
<div className="attachments-list">
|
||||||
|
<h3>📎 ไฟล์แนบ ({attachments.length})</h3>
|
||||||
|
|
||||||
|
{attachments.map(attachment => (
|
||||||
|
<div key={attachment.id} className="attachment-item">
|
||||||
|
<div className="file-icon">
|
||||||
|
{getFileIcon(attachment.mime_type)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-info">
|
||||||
|
<div className="file-name">{attachment.file_name}</div>
|
||||||
|
<div className="file-description">{attachment.description}</div>
|
||||||
|
<div className="file-size">{formatFileSize(attachment.file_size)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={attachment.download_url}
|
||||||
|
download
|
||||||
|
className="download-button"
|
||||||
|
>
|
||||||
|
⬇ ดาวน์โหลด
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon(mimeType) {
|
||||||
|
if (mimeType.includes('pdf')) return '📄';
|
||||||
|
if (mimeType.includes('zip')) return '📦';
|
||||||
|
if (mimeType.includes('word')) return '📝';
|
||||||
|
if (mimeType.includes('excel')) return '📊';
|
||||||
|
if (mimeType.includes('powerpoint')) return '📊';
|
||||||
|
if (mimeType.includes('image')) return '🖼️';
|
||||||
|
return '📎';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Drag & Drop Reorder
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function ReorderAttachments({ attachments, onReorder }) {
|
||||||
|
const [items, setItems] = useState(attachments);
|
||||||
|
|
||||||
|
const handleDragEnd = (result) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
|
||||||
|
const reordered = Array.from(items);
|
||||||
|
const [removed] = reordered.splice(result.source.index, 1);
|
||||||
|
reordered.splice(result.destination.index, 0, removed);
|
||||||
|
|
||||||
|
// Update sort_order
|
||||||
|
const updated = reordered.map((item, index) => ({
|
||||||
|
id: item.id,
|
||||||
|
sort_order: index + 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
setItems(reordered);
|
||||||
|
onReorder(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
|
<Droppable droppableId="attachments">
|
||||||
|
{(provided) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Draggable key={item.id} draggableId={String(item.id)} index={index}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
{item.file_name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Allowed File Types
|
||||||
|
|
||||||
|
| Category | MIME Type | Extension |
|
||||||
|
|----------|-----------|-----------|
|
||||||
|
| **Documents** | `application/pdf` | .pdf |
|
||||||
|
| | `application/msword` | .doc |
|
||||||
|
| | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | .docx |
|
||||||
|
| | `application/vnd.ms-excel` | .xls |
|
||||||
|
| | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | .xlsx |
|
||||||
|
| | `application/vnd.ms-powerpoint` | .ppt |
|
||||||
|
| | `application/vnd.openxmlformats-officedocument.presentationml.presentation` | .pptx |
|
||||||
|
| **Archives** | `application/zip` | .zip |
|
||||||
|
| | `application/x-rar-compressed` | .rar |
|
||||||
|
| | `application/x-7z-compressed` | .7z |
|
||||||
|
| **Images** | `image/jpeg` | .jpg |
|
||||||
|
| | `image/png` | .png |
|
||||||
|
| | `image/gif` | .gif |
|
||||||
|
| **Text** | `text/plain` | .txt |
|
||||||
|
| | `text/csv` | .csv |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Limits
|
||||||
|
|
||||||
|
- **Max file size**: 100 MB per file
|
||||||
|
- **Max attachments**: 10 files per lesson
|
||||||
|
- **Total size**: 500 MB per lesson
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**New Endpoints**: 6
|
||||||
|
- `POST /instructor/courses/:courseId/lessons/:lessonId/attachments`
|
||||||
|
- `GET /instructor/courses/:courseId/lessons/:lessonId/attachments`
|
||||||
|
- `PUT /instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId`
|
||||||
|
- `DELETE /instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId`
|
||||||
|
- `PUT /instructor/courses/:courseId/lessons/:lessonId/attachments/reorder`
|
||||||
|
- `GET /lessons/:lessonId/attachments/:attachmentId/download`
|
||||||
|
|
||||||
|
**Modified Endpoints**: 2
|
||||||
|
- `GET /students/courses/:courseId/lessons/:lessonId` - เพิ่ม attachments array
|
||||||
|
- `GET /students/courses/:courseId/learn` - เพิ่ม attachments_count
|
||||||
|
|
@ -67,8 +67,9 @@ Authorization: Bearer <token>
|
||||||
|--------|----------|------|-------------|
|
|--------|----------|------|-------------|
|
||||||
| POST | `/courses/:courseId/enroll` | 👨🎓 Student | Enroll in course |
|
| POST | `/courses/:courseId/enroll` | 👨🎓 Student | Enroll in course |
|
||||||
| GET | `/students/courses` | 👨🎓 Student | Get my enrolled courses |
|
| GET | `/students/courses` | 👨🎓 Student | Get my enrolled courses |
|
||||||
| GET | `/students/courses/:courseId/learn` | 👨🎓 Student | Get course learning page |
|
| GET | `/students/courses/:courseId/learn` | 👨🎓 Student | Get course learning page (with lock status) |
|
||||||
| GET | `/students/courses/:courseId/lessons/:lessonId` | 👨🎓 Student | Get lesson content |
|
| GET | `/students/courses/:courseId/lessons/:lessonId` | 👨🎓 Student | Get lesson content (checks prerequisites) |
|
||||||
|
| GET | `/students/courses/:courseId/lessons/:lessonId/access-check` | 👨🎓 Student | Check lesson access without loading content |
|
||||||
| POST | `/students/courses/:courseId/lessons/:lessonId/complete` | 👨🎓 Student | Mark lesson as complete |
|
| POST | `/students/courses/:courseId/lessons/:lessonId/complete` | 👨🎓 Student | Mark lesson as complete |
|
||||||
|
|
||||||
### Instructor Endpoints
|
### Instructor Endpoints
|
||||||
|
|
@ -96,11 +97,22 @@ Authorization: Bearer <token>
|
||||||
### Lessons
|
### Lessons
|
||||||
| Method | Endpoint | Auth | Description |
|
| Method | Endpoint | Auth | Description |
|
||||||
|--------|----------|------|-------------|
|
|--------|----------|------|-------------|
|
||||||
| POST | `/instructor/courses/:courseId/chapters/:chapterId/lessons` | 👨🏫 Instructor | Create lesson |
|
| POST | `/instructor/courses/:courseId/chapters/:chapterId/lessons` | 👨🏫 Instructor | Create lesson (with video & attachments) |
|
||||||
| PUT | `/instructor/courses/:courseId/lessons/:lessonId` | 👨🏫 Instructor | Update lesson |
|
| PUT | `/instructor/courses/:courseId/lessons/:lessonId` | 👨🏫 Instructor | Update lesson |
|
||||||
| DELETE | `/instructor/courses/:courseId/lessons/:lessonId` | 👨🏫 Instructor | Delete lesson |
|
| DELETE | `/instructor/courses/:courseId/lessons/:lessonId` | 👨🏫 Instructor | Delete lesson |
|
||||||
| POST | `/instructor/courses/:courseId/lessons/:lessonId/video` | 👨🏫 Instructor | Upload video |
|
|
||||||
| PUT | `/instructor/courses/:courseId/lessons/reorder` | 👨🏫 Instructor | Reorder lessons |
|
| PUT | `/instructor/courses/:courseId/lessons/reorder` | 👨🏫 Instructor | Reorder lessons |
|
||||||
|
| GET | `/instructor/courses/:courseId/lessons/:lessonId/prerequisites` | 👨🏫 Instructor | View lesson prerequisites |
|
||||||
|
| POST | `/instructor/courses/:courseId/lessons/:lessonId/prerequisites` | 👨🏫 Instructor | Update lesson prerequisites |
|
||||||
|
|
||||||
|
### Lesson Attachments
|
||||||
|
| Method | Endpoint | Auth | Description |
|
||||||
|
|--------|----------|------|-------------|
|
||||||
|
| POST | `/instructor/courses/:courseId/lessons/:lessonId/attachments` | 👨🏫 Instructor | Add more attachments |
|
||||||
|
| GET | `/instructor/courses/:courseId/lessons/:lessonId/attachments` | 👨🏫 Instructor | List attachments |
|
||||||
|
| PUT | `/instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId` | 👨🏫 Instructor | Update attachment info |
|
||||||
|
| DELETE | `/instructor/courses/:courseId/lessons/:lessonId/attachments/:attachmentId` | 👨🏫 Instructor | Delete attachment |
|
||||||
|
| PUT | `/instructor/courses/:courseId/lessons/:lessonId/attachments/reorder` | 👨🏫 Instructor | Reorder attachments |
|
||||||
|
| GET | `/lessons/:lessonId/attachments/:attachmentId/download` | ✅ | Download attachment |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -291,13 +303,13 @@ Authorization: Bearer <token>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Total Endpoints: **85+**
|
## Total Endpoints: **93+**
|
||||||
|
|
||||||
- Authentication: 6
|
- Authentication: 6
|
||||||
- User Management: 4
|
- User Management: 4
|
||||||
- Categories: 5
|
- Categories: 5
|
||||||
- Courses: 11
|
- Courses: 12
|
||||||
- Chapters & Lessons: 9
|
- Chapters & Lessons: 16 (-1)
|
||||||
- Quizzes: 10
|
- Quizzes: 10
|
||||||
- Announcements: 6
|
- Announcements: 6
|
||||||
- Reports: 11
|
- Reports: 11
|
||||||
306
docs/api-docs/api_lesson_creation_simplified.md
Normal file
306
docs/api-docs/api_lesson_creation_simplified.md
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
# Create Lesson with Files - Simplified API
|
||||||
|
|
||||||
|
## 📝 Create Lesson (One Request)
|
||||||
|
|
||||||
|
### POST `/instructor/courses/:courseId/chapters/:chapterId/lessons`
|
||||||
|
|
||||||
|
สร้าง lesson พร้อม video และ attachments ในครั้งเดียว
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request Examples
|
||||||
|
|
||||||
|
### 1. Text Lesson (No Files)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/chapters/1/lessons
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"th": "บทที่ 1: แนะนำ",
|
||||||
|
"en": "Lesson 1: Introduction"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"th": "<p>เนื้อหาบทเรียน...</p>",
|
||||||
|
"en": "<p>Lesson content...</p>"
|
||||||
|
},
|
||||||
|
"type": "text",
|
||||||
|
"sort_order": 1,
|
||||||
|
"is_sequential": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Video Lesson (Video Only)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/chapters/1/lessons
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
title_th: "บทที่ 1: แนะนำ Python"
|
||||||
|
title_en: "Lesson 1: Introduction to Python"
|
||||||
|
content_th: "<p>Python เป็นภาษา...</p>"
|
||||||
|
content_en: "<p>Python is a...</p>"
|
||||||
|
type: "video"
|
||||||
|
sort_order: 1
|
||||||
|
video: <lesson-video.mp4>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 201**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"chapter_id": 1,
|
||||||
|
"title": {
|
||||||
|
"th": "บทที่ 1: แนะนำ Python",
|
||||||
|
"en": "Lesson 1: Introduction to Python"
|
||||||
|
},
|
||||||
|
"type": "video",
|
||||||
|
"video": {
|
||||||
|
"url": "https://cdn.example.com/videos/lesson-1.m3u8",
|
||||||
|
"duration": "00:15:30",
|
||||||
|
"file_size": 52428800
|
||||||
|
},
|
||||||
|
"attachments": [],
|
||||||
|
"created_at": "2024-12-23T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Video + Attachments (Complete)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/chapters/1/lessons
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
# Lesson Info
|
||||||
|
title_th: "บทที่ 1: แนะนำ Python"
|
||||||
|
title_en: "Lesson 1: Introduction to Python"
|
||||||
|
content_th: "<p>Python เป็นภาษา...</p>"
|
||||||
|
content_en: "<p>Python is a...</p>"
|
||||||
|
type: "video"
|
||||||
|
sort_order: 1
|
||||||
|
is_sequential: true
|
||||||
|
|
||||||
|
# Video
|
||||||
|
video: <lesson-video.mp4>
|
||||||
|
|
||||||
|
# Attachments
|
||||||
|
attachments[]: <slides.pdf>
|
||||||
|
attachments[]: <code.zip>
|
||||||
|
attachments[]: <exercises.docx>
|
||||||
|
|
||||||
|
# Attachment Descriptions
|
||||||
|
descriptions[0][th]: "สไลด์ประกอบการสอน"
|
||||||
|
descriptions[0][en]: "Lecture slides"
|
||||||
|
descriptions[1][th]: "โค้ดตัวอย่าง"
|
||||||
|
descriptions[1][en]: "Sample code"
|
||||||
|
descriptions[2][th]: "แบบฝึกหัด"
|
||||||
|
descriptions[2][en]: "Exercises"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 201**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"chapter_id": 1,
|
||||||
|
"title": {
|
||||||
|
"th": "บทที่ 1: แนะนำ Python",
|
||||||
|
"en": "Lesson 1: Introduction to Python"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"th": "<p>Python เป็นภาษา...</p>",
|
||||||
|
"en": "<p>Python is a...</p>"
|
||||||
|
},
|
||||||
|
"type": "video",
|
||||||
|
"sort_order": 1,
|
||||||
|
"is_sequential": true,
|
||||||
|
|
||||||
|
"video": {
|
||||||
|
"url": "https://cdn.example.com/videos/lesson-1.m3u8",
|
||||||
|
"duration": "00:15:30",
|
||||||
|
"file_size": 52428800,
|
||||||
|
"uploaded_at": "2024-12-23T15:00:00Z"
|
||||||
|
},
|
||||||
|
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"file_name": "slides.pdf",
|
||||||
|
"file_size": 2048576,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"description": {
|
||||||
|
"th": "สไลด์ประกอบการสอน",
|
||||||
|
"en": "Lecture slides"
|
||||||
|
},
|
||||||
|
"sort_order": 1,
|
||||||
|
"download_url": "/api/lessons/1/attachments/1/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"file_name": "code.zip",
|
||||||
|
"file_size": 512000,
|
||||||
|
"mime_type": "application/zip",
|
||||||
|
"description": {
|
||||||
|
"th": "โค้ดตัวอย่าง",
|
||||||
|
"en": "Sample code"
|
||||||
|
},
|
||||||
|
"sort_order": 2,
|
||||||
|
"download_url": "/api/lessons/1/attachments/2/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"file_name": "exercises.docx",
|
||||||
|
"file_size": 256000,
|
||||||
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"description": {
|
||||||
|
"th": "แบบฝึกหัด",
|
||||||
|
"en": "Exercises"
|
||||||
|
},
|
||||||
|
"sort_order": 3,
|
||||||
|
"download_url": "/api/lessons/1/attachments/3/download"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"created_at": "2024-12-23T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. PDF Lesson (Attachments Only)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/chapters/1/lessons
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
title_th: "บทที่ 2: เอกสารประกอบ"
|
||||||
|
title_en: "Lesson 2: Reading Materials"
|
||||||
|
type: "pdf"
|
||||||
|
sort_order: 2
|
||||||
|
|
||||||
|
attachments[]: <main-document.pdf>
|
||||||
|
attachments[]: <supplementary.pdf>
|
||||||
|
|
||||||
|
descriptions[0][th]: "เอกสารหลัก"
|
||||||
|
descriptions[0][en]: "Main document"
|
||||||
|
descriptions[1][th]: "เอกสารเพิ่มเติม"
|
||||||
|
descriptions[1][en]: "Supplementary material"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Add More Files Later
|
||||||
|
|
||||||
|
ถ้าต้องการเพิ่มไฟล์ทีหลัง:
|
||||||
|
|
||||||
|
### POST `/instructor/courses/:courseId/lessons/:lessonId/attachments`
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/lessons/1/attachments
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
attachments[]: <additional-file.pdf>
|
||||||
|
descriptions[0][th]: "ไฟล์เพิ่มเติม"
|
||||||
|
descriptions[0][en]: "Additional file"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createLesson(courseId, chapterId, lessonData) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Basic info
|
||||||
|
formData.append('title_th', lessonData.title.th);
|
||||||
|
formData.append('title_en', lessonData.title.en);
|
||||||
|
formData.append('content_th', lessonData.content.th);
|
||||||
|
formData.append('content_en', lessonData.content.en);
|
||||||
|
formData.append('type', lessonData.type);
|
||||||
|
formData.append('sort_order', lessonData.sortOrder);
|
||||||
|
|
||||||
|
// Video (if exists)
|
||||||
|
if (lessonData.video) {
|
||||||
|
formData.append('video', lessonData.video);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachments (if exists)
|
||||||
|
if (lessonData.attachments && lessonData.attachments.length > 0) {
|
||||||
|
lessonData.attachments.forEach((file, index) => {
|
||||||
|
formData.append('attachments[]', file.file);
|
||||||
|
formData.append(`descriptions[${index}][th]`, file.description.th);
|
||||||
|
formData.append(`descriptions[${index}][en]`, file.description.en);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/instructor/courses/${courseId}/chapters/${chapterId}/lessons`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const lesson = await createLesson(1, 1, {
|
||||||
|
title: {
|
||||||
|
th: 'บทที่ 1: แนะนำ',
|
||||||
|
en: 'Lesson 1: Introduction'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
th: '<p>เนื้อหา...</p>',
|
||||||
|
en: '<p>Content...</p>'
|
||||||
|
},
|
||||||
|
type: 'video',
|
||||||
|
sortOrder: 1,
|
||||||
|
video: videoFile,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
file: pdfFile,
|
||||||
|
description: { th: 'สไลด์', en: 'Slides' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: zipFile,
|
||||||
|
description: { th: 'โค้ด', en: 'Code' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **One Request** - ส่งครั้งเดียว ไม่ต้องส่งหลายรอบ
|
||||||
|
✅ **Atomic** - สร้างสำเร็จหรือ fail ทั้งหมด
|
||||||
|
✅ **Faster** - ลด HTTP requests
|
||||||
|
✅ **Better UX** - Instructor ไม่ต้องรอหลายขั้นตอน
|
||||||
|
✅ **Flexible** - เพิ่มไฟล์ทีหลังได้
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Before**: 3 requests
|
||||||
|
1. Create lesson
|
||||||
|
2. Upload video
|
||||||
|
3. Upload attachments
|
||||||
|
|
||||||
|
**After**: 1 request
|
||||||
|
1. Create lesson with everything ✨
|
||||||
438
docs/api-docs/api_prerequisites_examples.md
Normal file
438
docs/api-docs/api_prerequisites_examples.md
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
# API Usage Examples - Lesson Prerequisites
|
||||||
|
|
||||||
|
## New Endpoints Usage Examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Check Lesson Access (Student)
|
||||||
|
|
||||||
|
### GET `/students/courses/:courseId/lessons/:lessonId/access-check`
|
||||||
|
|
||||||
|
**Purpose**: ตรวจสอบว่าสามารถเข้าถึง lesson ได้หรือไม่ โดยไม่ต้อง load content
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/students/courses/1/lessons/3/access-check
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200 (Can Access):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"can_access": true,
|
||||||
|
"lesson": {
|
||||||
|
"id": 3,
|
||||||
|
"title": "แบบทดสอบท้ายบท",
|
||||||
|
"type": "quiz",
|
||||||
|
"sort_order": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200 (Locked - Incomplete Prerequisites):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"can_access": false,
|
||||||
|
"reason": "incomplete_prerequisites",
|
||||||
|
"message": "กรุณาเรียนบทเรียนก่อนหน้าให้เสร็จก่อน",
|
||||||
|
"required_lessons": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "บทที่ 1: แนะนำ",
|
||||||
|
"type": "video",
|
||||||
|
"is_completed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "บทที่ 2: ตัวแปร",
|
||||||
|
"type": "video",
|
||||||
|
"is_completed": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missing_lessons": [2],
|
||||||
|
"next_available_lesson": {
|
||||||
|
"id": 2,
|
||||||
|
"title": "บทที่ 2: ตัวแปร"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200 (Locked - Quiz Not Passed):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"can_access": false,
|
||||||
|
"reason": "quiz_not_passed",
|
||||||
|
"message": "กรุณาทำแบบทดสอบให้ผ่านก่อน",
|
||||||
|
"required_quiz": {
|
||||||
|
"lesson_id": 3,
|
||||||
|
"title": "แบบทดสอบท้ายบท",
|
||||||
|
"passing_score": 70,
|
||||||
|
"your_best_score": 55,
|
||||||
|
"attempts_used": 2,
|
||||||
|
"max_attempts": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Get Learning Page with Lock Status (Student)
|
||||||
|
|
||||||
|
### GET `/students/courses/:courseId/learn`
|
||||||
|
|
||||||
|
**Changes**: เพิ่ม `is_locked` และ `lock_reason` ในแต่ละ lesson
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/students/courses/1/learn
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"course": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "Python สำหรับผู้เริ่มต้น"
|
||||||
|
},
|
||||||
|
"enrollment": {
|
||||||
|
"status": "enrolled",
|
||||||
|
"progress_percentage": 35
|
||||||
|
},
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "บทที่ 1: พื้นฐาน",
|
||||||
|
"sort_order": 1,
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "1.1 แนะนำ Python",
|
||||||
|
"type": "video",
|
||||||
|
"duration": "00:15:30",
|
||||||
|
"is_completed": true,
|
||||||
|
"is_locked": false,
|
||||||
|
"completed_at": "2024-12-23T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "1.2 ตัวแปร",
|
||||||
|
"type": "video",
|
||||||
|
"duration": "00:20:00",
|
||||||
|
"is_completed": false,
|
||||||
|
"is_locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "1.3 แบบทดสอบ",
|
||||||
|
"type": "quiz",
|
||||||
|
"is_completed": false,
|
||||||
|
"is_locked": true,
|
||||||
|
"lock_reason": "incomplete_prerequisites",
|
||||||
|
"required_lessons": [1, 2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "1.4 ขั้นสูง",
|
||||||
|
"type": "video",
|
||||||
|
"duration": "00:25:00",
|
||||||
|
"is_completed": false,
|
||||||
|
"is_locked": true,
|
||||||
|
"lock_reason": "quiz_not_passed",
|
||||||
|
"required_quiz": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Get Lesson Content with Prerequisites Check (Student)
|
||||||
|
|
||||||
|
### GET `/students/courses/:courseId/lessons/:lessonId`
|
||||||
|
|
||||||
|
**Changes**: ตรวจสอบ prerequisites ก่อน return content
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/students/courses/1/lessons/3
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 403 (Locked):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "LESSON_LOCKED",
|
||||||
|
"message": "Cannot access this lesson",
|
||||||
|
"reason": "incomplete_prerequisites",
|
||||||
|
"required_lessons": [1, 2],
|
||||||
|
"completed_lessons": [1],
|
||||||
|
"missing_lessons": [2],
|
||||||
|
"next_available_lesson": {
|
||||||
|
"id": 2,
|
||||||
|
"title": "บทที่ 2: ตัวแปร",
|
||||||
|
"type": "video"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Create Lesson with Prerequisites (Instructor)
|
||||||
|
|
||||||
|
### POST `/instructor/courses/:courseId/chapters/:chapterId/lessons`
|
||||||
|
|
||||||
|
**Changes**: เพิ่ม fields สำหรับ prerequisites
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/chapters/1/lessons
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"th": "แบบทดสอบท้ายบท",
|
||||||
|
"en": "Chapter Quiz"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"th": "<p>ทดสอบความเข้าใจ</p>",
|
||||||
|
"en": "<p>Test your understanding</p>"
|
||||||
|
},
|
||||||
|
"type": "quiz",
|
||||||
|
"sort_order": 3,
|
||||||
|
"is_sequential": true,
|
||||||
|
"prerequisite_lesson_ids": [1, 2],
|
||||||
|
"require_pass_quiz": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 201:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"chapter_id": 1,
|
||||||
|
"title": {
|
||||||
|
"th": "แบบทดสอบท้ายบท",
|
||||||
|
"en": "Chapter Quiz"
|
||||||
|
},
|
||||||
|
"type": "quiz",
|
||||||
|
"sort_order": 3,
|
||||||
|
"is_sequential": true,
|
||||||
|
"prerequisite_lesson_ids": [1, 2],
|
||||||
|
"require_pass_quiz": true,
|
||||||
|
"created_at": "2024-12-23T14:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 422 (Validation Error):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Invalid prerequisites",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"field": "prerequisite_lesson_ids",
|
||||||
|
"message": "Lesson ID 99 does not exist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. View Lesson Prerequisites (Instructor)
|
||||||
|
|
||||||
|
### GET `/instructor/courses/:courseId/lessons/:lessonId/prerequisites`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
GET /api/instructor/courses/1/lessons/4/prerequisites
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lesson": {
|
||||||
|
"id": 4,
|
||||||
|
"title": "บทที่ 3: ขั้นสูง",
|
||||||
|
"type": "video",
|
||||||
|
"sort_order": 4
|
||||||
|
},
|
||||||
|
"is_sequential": true,
|
||||||
|
"prerequisites": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "แบบทดสอบท้ายบท",
|
||||||
|
"type": "quiz",
|
||||||
|
"require_pass": true,
|
||||||
|
"passing_score": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"affected_students": {
|
||||||
|
"total_enrolled": 150,
|
||||||
|
"can_access": 85,
|
||||||
|
"locked": 65,
|
||||||
|
"breakdown": {
|
||||||
|
"incomplete_prerequisites": 45,
|
||||||
|
"quiz_not_passed": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Update Lesson Prerequisites (Instructor)
|
||||||
|
|
||||||
|
### POST `/instructor/courses/:courseId/lessons/:lessonId/prerequisites`
|
||||||
|
|
||||||
|
#### Request:
|
||||||
|
```http
|
||||||
|
POST /api/instructor/courses/1/lessons/4/prerequisites
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"is_sequential": true,
|
||||||
|
"prerequisite_lesson_ids": [3],
|
||||||
|
"require_pass_quiz": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response 200:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lesson_id": 4,
|
||||||
|
"is_sequential": true,
|
||||||
|
"prerequisites": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "แบบทดสอบท้ายบท",
|
||||||
|
"type": "quiz",
|
||||||
|
"require_pass": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": "Prerequisites updated successfully",
|
||||||
|
"affected_students": {
|
||||||
|
"newly_unlocked": 20,
|
||||||
|
"still_locked": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
### New Error Codes for Prerequisites
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `LESSON_LOCKED` | Cannot access lesson due to prerequisites |
|
||||||
|
| `INCOMPLETE_PREREQUISITES` | Required lessons not completed |
|
||||||
|
| `QUIZ_NOT_PASSED` | Required quiz not passed |
|
||||||
|
| `INVALID_PREREQUISITES` | Invalid prerequisite configuration |
|
||||||
|
| `CIRCULAR_DEPENDENCY` | Circular dependency detected |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Integration Examples
|
||||||
|
|
||||||
|
### 1. Display Locked Lesson
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function LessonItem({ lesson }) {
|
||||||
|
if (lesson.is_locked) {
|
||||||
|
return (
|
||||||
|
<div className="lesson-item locked">
|
||||||
|
<LockIcon />
|
||||||
|
<span className="lesson-title">{lesson.title}</span>
|
||||||
|
|
||||||
|
{lesson.lock_reason === 'incomplete_prerequisites' && (
|
||||||
|
<Tooltip>
|
||||||
|
กรุณาเรียนบทเรียนก่อนหน้าให้เสร็จก่อน
|
||||||
|
<ul>
|
||||||
|
{lesson.required_lessons.map(id => (
|
||||||
|
<li key={id}>Lesson {id}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lesson.lock_reason === 'quiz_not_passed' && (
|
||||||
|
<Tooltip>
|
||||||
|
กรุณาทำแบบทดสอบให้ผ่านก่อน (ต้องได้ 70% ขึ้นไป)
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/lessons/${lesson.id}`} className="lesson-item">
|
||||||
|
{lesson.is_completed ? <CheckIcon /> : <PlayIcon />}
|
||||||
|
<span>{lesson.title}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Access Before Navigation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function navigateToLesson(courseId, lessonId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/students/courses/${courseId}/lessons/${lessonId}/access-check`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.can_access) {
|
||||||
|
// Navigate to lesson
|
||||||
|
router.push(`/courses/${courseId}/lessons/${lessonId}`);
|
||||||
|
} else {
|
||||||
|
// Show lock message
|
||||||
|
if (data.reason === 'incomplete_prerequisites') {
|
||||||
|
showAlert(`กรุณาเรียนบทเรียนก่อนหน้าให้เสร็จก่อน`);
|
||||||
|
// Highlight next available lesson
|
||||||
|
highlightLesson(data.next_available_lesson.id);
|
||||||
|
} else if (data.reason === 'quiz_not_passed') {
|
||||||
|
showAlert(`กรุณาทำแบบทดสอบให้ผ่านก่อน`);
|
||||||
|
// Navigate to quiz
|
||||||
|
router.push(`/courses/${courseId}/lessons/${data.required_quiz.lesson_id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking lesson access:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**New Endpoints**: 3
|
||||||
|
- `GET /students/courses/:courseId/lessons/:lessonId/access-check`
|
||||||
|
- `GET /instructor/courses/:courseId/lessons/:lessonId/prerequisites`
|
||||||
|
- `POST /instructor/courses/:courseId/lessons/:lessonId/prerequisites`
|
||||||
|
|
||||||
|
**Modified Endpoints**: 4
|
||||||
|
- `GET /students/courses/:courseId/learn` - เพิ่ม lock status
|
||||||
|
- `GET /students/courses/:courseId/lessons/:lessonId` - เช็ค prerequisites
|
||||||
|
- `POST /instructor/courses/:courseId/chapters/:chapterId/lessons` - รองรับ prerequisites
|
||||||
|
- `PUT /instructor/courses/:courseId/lessons/:lessonId` - รองรับ prerequisites
|
||||||
650
docs/development_setup.md
Normal file
650
docs/development_setup.md
Normal file
|
|
@ -0,0 +1,650 @@
|
||||||
|
# Development Environment Setup Guide
|
||||||
|
|
||||||
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Developer Machines │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Frontend │ │ Frontend │ │ Backend │ │
|
||||||
|
│ │ (Student) │ │ (Admin) │ │ (API) │ │
|
||||||
|
│ │ Port: 3000 │ │ Port: 3001 │ │ Port: 4000 │ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────────┴──────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└────────────────────────────┼─────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Proxmox Server (Shared Services) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Docker Compose Stack │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||||
|
│ │ │ PostgreSQL │ │ MinIO │ │ Redis │ │ │
|
||||||
|
│ │ │ Port: 5432 │ │ Port: 9000 │ │ Port: 6379 │ │ │
|
||||||
|
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||||
|
│ │ │ Mailhog │ │ Adminer │ │ Forgejo │ │ │
|
||||||
|
│ │ │ Port: 1025 │ │ Port: 8080 │ │ Port: 3030 │ │ │
|
||||||
|
│ │ │ Port: 8025 │ │ │ │ Port: 2222 │ │ │
|
||||||
|
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ IP: 192.168.1.100 (example) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Services on Proxmox Server
|
||||||
|
|
||||||
|
### 1. PostgreSQL - Database
|
||||||
|
- **Port**: 5432
|
||||||
|
- **Purpose**: Main application database
|
||||||
|
- **Access**: All developers
|
||||||
|
|
||||||
|
### 2. MinIO - S3-compatible Storage
|
||||||
|
- **Port**: 9000 (API), 9001 (Console)
|
||||||
|
- **Purpose**: File storage (videos, documents, images)
|
||||||
|
- **Access**: Backend + Developers
|
||||||
|
|
||||||
|
### 3. Redis - Cache & Session
|
||||||
|
- **Port**: 6379
|
||||||
|
- **Purpose**: Caching, session storage, queue
|
||||||
|
- **Access**: Backend only
|
||||||
|
|
||||||
|
### 4. Mailhog - Email Testing
|
||||||
|
- **Port**: 1025 (SMTP), 8025 (Web UI)
|
||||||
|
- **Purpose**: Catch all emails in development
|
||||||
|
- **Access**: All developers
|
||||||
|
|
||||||
|
### 5. Adminer - Database Management
|
||||||
|
- **Port**: 8080
|
||||||
|
- **Purpose**: Web-based database management
|
||||||
|
- **Access**: All developers
|
||||||
|
|
||||||
|
### 6. Forgejo - Git Server
|
||||||
|
- **Port**: 3030 (HTTP), 2222 (SSH)
|
||||||
|
- **Purpose**: Self-hosted Git repository (like GitHub/GitLab)
|
||||||
|
- **Access**: All developers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker Compose Configuration
|
||||||
|
|
||||||
|
Create this file on your Proxmox server:
|
||||||
|
|
||||||
|
### `/opt/elearning-dev/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: elearning-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: elearning_dev
|
||||||
|
POSTGRES_USER: elearning
|
||||||
|
POSTGRES_PASSWORD: dev_password_change_in_prod
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init-scripts:/docker-entrypoint-initdb.d
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U elearning"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# MinIO - S3 Compatible Storage
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: elearning-minio
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API
|
||||||
|
- "9001:9001" # Console
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin123
|
||||||
|
MINIO_BROWSER_REDIRECT_URL: http://192.168.1.100:9001
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# MinIO Client - Create buckets on startup
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:latest
|
||||||
|
container_name: elearning-minio-init
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
sleep 5;
|
||||||
|
/usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin123;
|
||||||
|
/usr/bin/mc mb myminio/courses --ignore-existing;
|
||||||
|
/usr/bin/mc mb myminio/videos --ignore-existing;
|
||||||
|
/usr/bin/mc mb myminio/documents --ignore-existing;
|
||||||
|
/usr/bin/mc mb myminio/images --ignore-existing;
|
||||||
|
/usr/bin/mc mb myminio/attachments --ignore-existing;
|
||||||
|
/usr/bin/mc anonymous set download myminio/images;
|
||||||
|
echo 'MinIO buckets created successfully';
|
||||||
|
exit 0;
|
||||||
|
"
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
|
||||||
|
# Redis - Cache & Session Store
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: elearning-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: redis-server --appendonly yes --requirepass dev_redis_password
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Mailhog - Email Testing
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog:latest
|
||||||
|
container_name: elearning-mailhog
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "1025:1025" # SMTP
|
||||||
|
- "8025:8025" # Web UI
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
|
||||||
|
# Adminer - Database Management UI
|
||||||
|
adminer:
|
||||||
|
image: adminer:latest
|
||||||
|
container_name: elearning-adminer
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: postgres
|
||||||
|
ADMINER_DESIGN: dracula
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
# Forgejo - Self-hosted Git Server
|
||||||
|
forgejo:
|
||||||
|
image: codeberg.org/forgejo/forgejo:1.21
|
||||||
|
container_name: elearning-forgejo
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3030:3000" # HTTP
|
||||||
|
- "2222:22" # SSH
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- FORGEJO__database__DB_TYPE=postgres
|
||||||
|
- FORGEJO__database__HOST=postgres:5432
|
||||||
|
- FORGEJO__database__NAME=forgejo
|
||||||
|
- FORGEJO__database__USER=elearning
|
||||||
|
- FORGEJO__database__PASSWD=dev_password_change_in_prod
|
||||||
|
volumes:
|
||||||
|
- forgejo_data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
networks:
|
||||||
|
- elearning-network
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
minio_data:
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
forgejo_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
elearning-network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Setup Instructions
|
||||||
|
|
||||||
|
### Step 1: Prepare Proxmox Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH to Proxmox server
|
||||||
|
ssh root@192.168.1.100
|
||||||
|
|
||||||
|
# Create directory
|
||||||
|
mkdir -p /opt/elearning-dev
|
||||||
|
cd /opt/elearning-dev
|
||||||
|
|
||||||
|
# Create docker-compose.yml (paste content above)
|
||||||
|
nano docker-compose.yml
|
||||||
|
|
||||||
|
# Create init scripts directory
|
||||||
|
mkdir init-scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Database Init Script (Optional)
|
||||||
|
|
||||||
|
Create `/opt/elearning-dev/init-scripts/01-create-extensions.sql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable required PostgreSQL extensions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||||
|
|
||||||
|
-- Create Forgejo database
|
||||||
|
CREATE DATABASE forgejo;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE forgejo TO elearning;
|
||||||
|
|
||||||
|
-- Create additional schemas if needed
|
||||||
|
-- CREATE SCHEMA IF NOT EXISTS analytics;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Start Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start all services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Check specific service
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Verify Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test PostgreSQL
|
||||||
|
docker exec -it elearning-postgres psql -U elearning -d elearning_dev -c "SELECT version();"
|
||||||
|
|
||||||
|
# Test Redis
|
||||||
|
docker exec -it elearning-redis redis-cli -a dev_redis_password ping
|
||||||
|
|
||||||
|
# Test MinIO
|
||||||
|
curl http://192.168.1.100:9000/minio/health/live
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Developer Machine Setup
|
||||||
|
|
||||||
|
### Frontend (Student) - Port 3000
|
||||||
|
|
||||||
|
**Directory**: `/path/to/frontend-student`
|
||||||
|
|
||||||
|
**`.env.local`**:
|
||||||
|
```env
|
||||||
|
# API Configuration
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000/api
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:4000
|
||||||
|
|
||||||
|
# MinIO/S3 Configuration
|
||||||
|
NEXT_PUBLIC_S3_ENDPOINT=http://192.168.1.100:9000
|
||||||
|
NEXT_PUBLIC_S3_BUCKET=courses
|
||||||
|
|
||||||
|
# App Configuration
|
||||||
|
NEXT_PUBLIC_APP_NAME=E-Learning Platform
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Start Command**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend (Admin) - Port 3001
|
||||||
|
|
||||||
|
**Directory**: `/path/to/frontend-admin`
|
||||||
|
|
||||||
|
**`.env.local`**:
|
||||||
|
```env
|
||||||
|
# API Configuration
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000/api
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:4000
|
||||||
|
|
||||||
|
# MinIO/S3 Configuration
|
||||||
|
NEXT_PUBLIC_S3_ENDPOINT=http://192.168.1.100:9000
|
||||||
|
NEXT_PUBLIC_S3_BUCKET=courses
|
||||||
|
|
||||||
|
# App Configuration
|
||||||
|
NEXT_PUBLIC_APP_NAME=E-Learning Admin
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
**package.json** (modify dev script):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Start Command**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Backend (API) - Port 4000
|
||||||
|
|
||||||
|
**Directory**: `/path/to/backend`
|
||||||
|
|
||||||
|
**`.env`**:
|
||||||
|
```env
|
||||||
|
# Application
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=4000
|
||||||
|
APP_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://elearning:dev_password_change_in_prod@192.168.1.100:5432/elearning_dev
|
||||||
|
DB_HOST=192.168.1.100
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=elearning_dev
|
||||||
|
DB_USER=elearning
|
||||||
|
DB_PASSWORD=dev_password_change_in_prod
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://:dev_redis_password@192.168.1.100:6379
|
||||||
|
REDIS_HOST=192.168.1.100
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=dev_redis_password
|
||||||
|
|
||||||
|
# MinIO/S3
|
||||||
|
S3_ENDPOINT=http://192.168.1.100:9000
|
||||||
|
S3_ACCESS_KEY=minioadmin
|
||||||
|
S3_SECRET_KEY=minioadmin123
|
||||||
|
S3_BUCKET_COURSES=courses
|
||||||
|
S3_BUCKET_VIDEOS=videos
|
||||||
|
S3_BUCKET_DOCUMENTS=documents
|
||||||
|
S3_BUCKET_IMAGES=images
|
||||||
|
S3_BUCKET_ATTACHMENTS=attachments
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_USE_SSL=false
|
||||||
|
|
||||||
|
# Email (Mailhog)
|
||||||
|
SMTP_HOST=192.168.1.100
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_FROM=noreply@elearning.local
|
||||||
|
SMTP_FROM_NAME=E-Learning Platform
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN=http://localhost:3000,http://localhost:3001
|
||||||
|
|
||||||
|
# File Upload
|
||||||
|
MAX_FILE_SIZE=104857600 # 100MB
|
||||||
|
ALLOWED_VIDEO_TYPES=video/mp4,video/webm
|
||||||
|
ALLOWED_DOCUMENT_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
ALLOWED_IMAGE_TYPES=image/jpeg,image/png,image/gif
|
||||||
|
```
|
||||||
|
|
||||||
|
**Start Command**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Useful Commands
|
||||||
|
|
||||||
|
### Docker Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop all services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Stop and remove volumes (⚠️ deletes data)
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker-compose restart postgres
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
|
||||||
|
# Execute command in container
|
||||||
|
docker exec -it elearning-postgres psql -U elearning -d elearning_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup database
|
||||||
|
docker exec elearning-postgres pg_dump -U elearning elearning_dev > backup.sql
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
cat backup.sql | docker exec -i elearning-postgres psql -U elearning -d elearning_dev
|
||||||
|
|
||||||
|
# Access database shell
|
||||||
|
docker exec -it elearning-postgres psql -U elearning -d elearning_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access MinIO Console
|
||||||
|
# Open browser: http://192.168.1.100:9001
|
||||||
|
# Username: minioadmin
|
||||||
|
# Password: minioadmin123
|
||||||
|
|
||||||
|
# Using MinIO Client (mc)
|
||||||
|
docker exec -it elearning-minio mc alias set local http://localhost:9000 minioadmin minioadmin123
|
||||||
|
docker exec -it elearning-minio mc ls local/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Access URLs
|
||||||
|
|
||||||
|
| Service | URL | Credentials |
|
||||||
|
|---------|-----|-------------|
|
||||||
|
| **Frontend (Student)** | http://localhost:3000 | - |
|
||||||
|
| **Frontend (Admin)** | http://localhost:3001 | - |
|
||||||
|
| **Backend API** | http://localhost:4000 | - |
|
||||||
|
| **PostgreSQL** | 192.168.1.100:5432 | elearning / dev_password_change_in_prod |
|
||||||
|
| **MinIO Console** | http://192.168.1.100:9001 | minioadmin / minioadmin123 |
|
||||||
|
| **MinIO API** | http://192.168.1.100:9000 | - |
|
||||||
|
| **Redis** | 192.168.1.100:6379 | Password: dev_redis_password |
|
||||||
|
| **Mailhog UI** | http://192.168.1.100:8025 | - |
|
||||||
|
| **Adminer** | http://192.168.1.100:8080 | - |
|
||||||
|
| **Forgejo** | http://192.168.1.100:3030 | Setup on first visit |
|
||||||
|
| **Forgejo SSH** | ssh://git@192.168.1.100:2222 | SSH key required |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Resource Requirements
|
||||||
|
|
||||||
|
### Proxmox Server (Recommended)
|
||||||
|
- **CPU**: 4 cores
|
||||||
|
- **RAM**: 8 GB
|
||||||
|
- **Storage**: 50 GB (SSD recommended)
|
||||||
|
- **Network**: 1 Gbps
|
||||||
|
|
||||||
|
### Developer Machines
|
||||||
|
- **CPU**: 2+ cores
|
||||||
|
- **RAM**: 8 GB
|
||||||
|
- **Storage**: 20 GB free space
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Notes
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **Development Environment Only!**
|
||||||
|
> - These credentials are for development only
|
||||||
|
> - Change all passwords in production
|
||||||
|
> - Never commit `.env` files to git
|
||||||
|
> - Use `.env.example` for templates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Cannot connect to PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if container is running
|
||||||
|
docker ps | grep postgres
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker logs elearning-postgres
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
docker exec -it elearning-postgres pg_isready -U elearning
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cannot connect to MinIO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if container is running
|
||||||
|
docker ps | grep minio
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker logs elearning-minio
|
||||||
|
|
||||||
|
# Test health
|
||||||
|
curl http://192.168.1.100:9000/minio/health/live
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find process using port
|
||||||
|
lsof -i :5432
|
||||||
|
# or
|
||||||
|
netstat -tulpn | grep 5432
|
||||||
|
|
||||||
|
# Kill process
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker network issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recreate network
|
||||||
|
docker-compose down
|
||||||
|
docker network prune
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Development Workflow
|
||||||
|
|
||||||
|
### Daily Workflow
|
||||||
|
|
||||||
|
1. **Start Proxmox Services** (if not running)
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.1.100
|
||||||
|
cd /opt/elearning-dev
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start Backend**
|
||||||
|
```bash
|
||||||
|
cd /path/to/backend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start Frontend (Student)**
|
||||||
|
```bash
|
||||||
|
cd /path/to/frontend-student
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start Frontend (Admin)**
|
||||||
|
```bash
|
||||||
|
cd /path/to/frontend-admin
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Develop & Test**
|
||||||
|
- Student app: http://localhost:3000
|
||||||
|
- Admin app: http://localhost:3001
|
||||||
|
- API: http://localhost:4000
|
||||||
|
- Check emails: http://192.168.1.100:8025
|
||||||
|
|
||||||
|
### End of Day
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Optional: Stop services to save resources
|
||||||
|
ssh root@192.168.1.100
|
||||||
|
cd /opt/elearning-dev
|
||||||
|
docker-compose stop
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
1. ✅ Setup Proxmox server with Docker Compose
|
||||||
|
2. ✅ Configure developer machines with `.env` files
|
||||||
|
3. ✅ Test connectivity to all services
|
||||||
|
4. ✅ Run database migrations
|
||||||
|
5. ✅ Seed initial data
|
||||||
|
6. ✅ Start development!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||||
|
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
||||||
|
- [MinIO Documentation](https://min.io/docs/)
|
||||||
|
- [Redis Documentation](https://redis.io/documentation)
|
||||||
493
docs/forgejo_setup.md
Normal file
493
docs/forgejo_setup.md
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
# Forgejo Setup Guide
|
||||||
|
|
||||||
|
## 🎯 What is Forgejo?
|
||||||
|
|
||||||
|
**Forgejo** is a self-hosted Git server (like GitHub or GitLab) that you can run on your own server. It's perfect for:
|
||||||
|
- 📦 Hosting your code repositories privately
|
||||||
|
- 👥 Team collaboration with pull requests
|
||||||
|
- 🔍 Code review
|
||||||
|
- 📋 Issue tracking
|
||||||
|
- 🔄 CI/CD integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Initial Setup
|
||||||
|
|
||||||
|
### Step 1: Access Forgejo
|
||||||
|
|
||||||
|
After starting Docker Compose, open your browser:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.1.100:3030
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll see the **Initial Configuration** page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Configure Forgejo
|
||||||
|
|
||||||
|
Fill in the following settings:
|
||||||
|
|
||||||
|
#### **Database Settings** (Already configured via Docker)
|
||||||
|
- ✅ Database Type: `PostgreSQL`
|
||||||
|
- ✅ Host: `postgres:5432`
|
||||||
|
- ✅ Username: `elearning`
|
||||||
|
- ✅ Password: `dev_password_change_in_prod`
|
||||||
|
- ✅ Database Name: `forgejo`
|
||||||
|
|
||||||
|
#### **General Settings**
|
||||||
|
- **Site Title**: `E-Learning Dev`
|
||||||
|
- **Repository Root Path**: `/data/git/repositories` (default)
|
||||||
|
- **Git LFS Root Path**: `/data/git/lfs` (default)
|
||||||
|
- **Run As Username**: `git` (default)
|
||||||
|
|
||||||
|
#### **Server and Third-Party Service Settings**
|
||||||
|
- **SSH Server Domain**: `192.168.1.100`
|
||||||
|
- **SSH Port**: `2222`
|
||||||
|
- **Forgejo HTTP Listen Port**: `3000` (internal)
|
||||||
|
- **Forgejo Base URL**: `http://192.168.1.100:3030`
|
||||||
|
- **Log Path**: `/data/forgejo/log` (default)
|
||||||
|
|
||||||
|
#### **Email Settings** (Optional - use Mailhog)
|
||||||
|
- **SMTP Host**: `mailhog:1025`
|
||||||
|
- **Send Email As**: `forgejo@elearning.local`
|
||||||
|
- **Require Sign-In to View Pages**: ✅ (recommended)
|
||||||
|
|
||||||
|
#### **Administrator Account Settings**
|
||||||
|
Create your admin account:
|
||||||
|
- **Username**: `admin`
|
||||||
|
- **Password**: `admin123` (change in production!)
|
||||||
|
- **Email**: `admin@elearning.local`
|
||||||
|
|
||||||
|
Click **Install Forgejo**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Creating Developer Accounts
|
||||||
|
|
||||||
|
### Option 1: Self-Registration (if enabled)
|
||||||
|
|
||||||
|
Users can register at: `http://192.168.1.100:3030/user/sign_up`
|
||||||
|
|
||||||
|
### Option 2: Admin Creates Users
|
||||||
|
|
||||||
|
1. Login as admin
|
||||||
|
2. Go to **Site Administration** (top right)
|
||||||
|
3. Click **User Accounts**
|
||||||
|
4. Click **Create User Account**
|
||||||
|
5. Fill in:
|
||||||
|
- Username
|
||||||
|
- Email
|
||||||
|
- Password
|
||||||
|
6. Click **Create User Account**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Creating Your First Repository
|
||||||
|
|
||||||
|
### Via Web Interface
|
||||||
|
|
||||||
|
1. Login to Forgejo
|
||||||
|
2. Click **+** (top right) → **New Repository**
|
||||||
|
3. Fill in:
|
||||||
|
- **Owner**: Your username
|
||||||
|
- **Repository Name**: `elearning-backend`
|
||||||
|
- **Description**: Backend API for E-Learning Platform
|
||||||
|
- **Visibility**: Private
|
||||||
|
- **Initialize Repository**: ✅ Add README
|
||||||
|
- **Add .gitignore**: Node
|
||||||
|
- **Add License**: MIT (optional)
|
||||||
|
4. Click **Create Repository**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Setting Up SSH Access
|
||||||
|
|
||||||
|
### Step 1: Generate SSH Key (if you don't have one)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On your developer machine
|
||||||
|
ssh-keygen -t ed25519 -C "your_email@example.com"
|
||||||
|
|
||||||
|
# Press Enter to accept default location (~/.ssh/id_ed25519)
|
||||||
|
# Enter a passphrase (optional but recommended)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Add SSH Key to Forgejo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy your public key
|
||||||
|
cat ~/.ssh/id_ed25519.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Login to Forgejo
|
||||||
|
2. Click your **avatar** (top right) → **Settings**
|
||||||
|
3. Click **SSH / GPG Keys**
|
||||||
|
4. Click **Add Key**
|
||||||
|
5. Paste your public key
|
||||||
|
6. Give it a name (e.g., "MacBook Pro")
|
||||||
|
7. Click **Add Key**
|
||||||
|
|
||||||
|
### Step 3: Test SSH Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -T git@192.168.1.100 -p 2222
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```
|
||||||
|
Hi there, <username>! You've successfully authenticated, but Forgejo does not provide shell access.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Using Git with Forgejo
|
||||||
|
|
||||||
|
### Clone Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via SSH (recommended)
|
||||||
|
git clone ssh://git@192.168.1.100:2222/<username>/elearning-backend.git
|
||||||
|
|
||||||
|
# Via HTTP
|
||||||
|
git clone http://192.168.1.100:3030/<username>/elearning-backend.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push Existing Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/your/project
|
||||||
|
|
||||||
|
# Initialize git (if not already)
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Add Forgejo as remote
|
||||||
|
git remote add origin ssh://git@192.168.1.100:2222/<username>/elearning-backend.git
|
||||||
|
|
||||||
|
# Add files
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
|
||||||
|
# Push to Forgejo
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 Organizing Repositories
|
||||||
|
|
||||||
|
### Create Organization
|
||||||
|
|
||||||
|
1. Click **+** (top right) → **New Organization**
|
||||||
|
2. Fill in:
|
||||||
|
- **Organization Name**: `elearning-team`
|
||||||
|
- **Visibility**: Private
|
||||||
|
3. Click **Create Organization**
|
||||||
|
|
||||||
|
### Add Team Members
|
||||||
|
|
||||||
|
1. Go to Organization → **Teams**
|
||||||
|
2. Click **Create New Team**
|
||||||
|
3. Fill in:
|
||||||
|
- **Team Name**: `Developers`
|
||||||
|
- **Permission**: Write
|
||||||
|
4. Click **Create Team**
|
||||||
|
5. Click **Add Team Member** and select users
|
||||||
|
|
||||||
|
### Create Repository in Organization
|
||||||
|
|
||||||
|
1. Click **+** → **New Repository**
|
||||||
|
2. **Owner**: Select `elearning-team`
|
||||||
|
3. Fill in repository details
|
||||||
|
4. Click **Create Repository**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Recommended Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
elearning-team/
|
||||||
|
├── elearning-backend # Backend API
|
||||||
|
├── elearning-frontend-student # Student Frontend
|
||||||
|
├── elearning-frontend-admin # Admin Frontend
|
||||||
|
├── elearning-docs # Documentation
|
||||||
|
└── elearning-infrastructure # Docker, CI/CD configs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Workflow Example
|
||||||
|
|
||||||
|
### 1. Clone Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@192.168.1.100:2222/elearning-team/elearning-backend.git
|
||||||
|
cd elearning-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Feature Branch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/add-quiz-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Make Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit files
|
||||||
|
nano src/controllers/quizController.js
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: add quiz submission endpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Push Branch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin feature/add-quiz-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Pull Request
|
||||||
|
|
||||||
|
1. Go to repository in Forgejo
|
||||||
|
2. Click **Pull Requests**
|
||||||
|
3. Click **New Pull Request**
|
||||||
|
4. Select:
|
||||||
|
- **Base**: `main`
|
||||||
|
- **Compare**: `feature/add-quiz-api`
|
||||||
|
5. Fill in title and description
|
||||||
|
6. Click **Create Pull Request**
|
||||||
|
|
||||||
|
### 6. Code Review
|
||||||
|
|
||||||
|
Team members can:
|
||||||
|
- Review code changes
|
||||||
|
- Add comments
|
||||||
|
- Approve or request changes
|
||||||
|
|
||||||
|
### 7. Merge
|
||||||
|
|
||||||
|
Once approved:
|
||||||
|
1. Click **Merge Pull Request**
|
||||||
|
2. Select merge method (Merge / Squash / Rebase)
|
||||||
|
3. Click **Confirm Merge**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Git Configuration
|
||||||
|
|
||||||
|
### Set Git Config for Forgejo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set your identity
|
||||||
|
git config --global user.name "Your Name"
|
||||||
|
git config --global user.email "your.email@example.com"
|
||||||
|
|
||||||
|
# Use SSH by default
|
||||||
|
git config --global url."ssh://git@192.168.1.100:2222/".insteadOf "http://192.168.1.100:3030/"
|
||||||
|
|
||||||
|
# Set default branch name
|
||||||
|
git config --global init.defaultBranch main
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Config (Optional)
|
||||||
|
|
||||||
|
Create `~/.ssh/config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Host forgejo
|
||||||
|
HostName 192.168.1.100
|
||||||
|
Port 2222
|
||||||
|
User git
|
||||||
|
IdentityFile ~/.ssh/id_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can clone with:
|
||||||
|
```bash
|
||||||
|
git clone forgejo:<username>/elearning-backend.git
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Common Tasks
|
||||||
|
|
||||||
|
### Create .gitignore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create .gitignore for Node.js
|
||||||
|
cat > .gitignore << 'EOF'
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.next/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git add .gitignore
|
||||||
|
git commit -m "chore: add .gitignore"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect Main Branch
|
||||||
|
|
||||||
|
1. Go to Repository → **Settings**
|
||||||
|
2. Click **Branches**
|
||||||
|
3. Click **Add Rule**
|
||||||
|
4. Fill in:
|
||||||
|
- **Branch Name Pattern**: `main`
|
||||||
|
- **Enable Branch Protection**: ✅
|
||||||
|
- **Require Pull Request**: ✅
|
||||||
|
- **Require Approvals**: 1
|
||||||
|
- **Dismiss Stale Approvals**: ✅
|
||||||
|
5. Click **Save**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Cannot Connect via SSH
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Forgejo is running
|
||||||
|
docker ps | grep forgejo
|
||||||
|
|
||||||
|
# Test SSH connection
|
||||||
|
ssh -T git@192.168.1.100 -p 2222 -v
|
||||||
|
|
||||||
|
# Check if port 2222 is open
|
||||||
|
telnet 192.168.1.100 2222
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied (publickey)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure your SSH key is added to Forgejo
|
||||||
|
# Check SSH agent
|
||||||
|
ssh-add -l
|
||||||
|
|
||||||
|
# Add your key to agent
|
||||||
|
ssh-add ~/.ssh/id_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cannot Push to Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check remote URL
|
||||||
|
git remote -v
|
||||||
|
|
||||||
|
# Update remote URL
|
||||||
|
git remote set-url origin ssh://git@192.168.1.100:2222/<username>/repo.git
|
||||||
|
|
||||||
|
# Check your permissions in Forgejo
|
||||||
|
# Make sure you have write access to the repository
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
### 1. **Use SSH for Authentication**
|
||||||
|
- More secure than HTTP
|
||||||
|
- No need to enter password every time
|
||||||
|
|
||||||
|
### 2. **Protect Main Branch**
|
||||||
|
- Require pull requests
|
||||||
|
- Require code reviews
|
||||||
|
- Prevent force pushes
|
||||||
|
|
||||||
|
### 3. **Use Meaningful Commit Messages**
|
||||||
|
```bash
|
||||||
|
# Good
|
||||||
|
git commit -m "feat: add user authentication endpoint"
|
||||||
|
git commit -m "fix: resolve quiz submission bug"
|
||||||
|
git commit -m "docs: update API documentation"
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
git commit -m "update"
|
||||||
|
git commit -m "fix bug"
|
||||||
|
git commit -m "changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Create Feature Branches**
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
git checkout -b feature/user-profile
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
git checkout -b fix/login-error
|
||||||
|
|
||||||
|
# Hotfix
|
||||||
|
git checkout -b hotfix/security-patch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Regular Commits**
|
||||||
|
- Commit often
|
||||||
|
- Keep commits focused
|
||||||
|
- One logical change per commit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- [Forgejo Documentation](https://forgejo.org/docs/)
|
||||||
|
- [Git Documentation](https://git-scm.com/doc)
|
||||||
|
- [GitHub Flow Guide](https://guides.github.com/introduction/flow/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Quick Reference
|
||||||
|
|
||||||
|
| Action | Command |
|
||||||
|
|--------|---------|
|
||||||
|
| Clone repo | `git clone ssh://git@192.168.1.100:2222/<user>/<repo>.git` |
|
||||||
|
| Create branch | `git checkout -b feature/name` |
|
||||||
|
| Stage changes | `git add .` |
|
||||||
|
| Commit | `git commit -m "message"` |
|
||||||
|
| Push | `git push origin branch-name` |
|
||||||
|
| Pull latest | `git pull origin main` |
|
||||||
|
| Check status | `git status` |
|
||||||
|
| View history | `git log --oneline` |
|
||||||
|
| Switch branch | `git checkout branch-name` |
|
||||||
|
| Delete branch | `git branch -d branch-name` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎊 You're Ready!
|
||||||
|
|
||||||
|
Now you have a fully functional Git server for your E-Learning project!
|
||||||
|
|
||||||
|
**Next Steps**:
|
||||||
|
1. Create repositories for each component (backend, frontend-student, frontend-admin)
|
||||||
|
2. Invite team members
|
||||||
|
3. Set up branch protection
|
||||||
|
4. Start collaborating! 🚀
|
||||||
Loading…
Add table
Add a link
Reference in a new issue