github-actions[bot] commited on
Commit
450b0f4
·
1 Parent(s): 345d455

Deploy Backend from GitHub Actions Commit: 1c0ff318f0e1e2dcc5a8efca696bd39b29d61aaa

Browse files
Files changed (47) hide show
  1. package-lock.json +13 -0
  2. package.json +1 -0
  3. prisma/schema.prisma +7 -6
  4. prisma/seed.ts +275 -7
  5. src/app.module.ts +12 -0
  6. src/courses/chapters.controller.ts +50 -0
  7. src/courses/courses.controller.ts +7 -488
  8. src/courses/courses.module.ts +4 -1
  9. src/courses/courses.service.ts +113 -10
  10. src/courses/dto/create-chapter.dto.ts +19 -0
  11. src/courses/dto/create-comment.dto.ts +15 -0
  12. src/courses/dto/create-course.dto.ts +15 -0
  13. src/courses/dto/create-document-ref.dto.ts +27 -0
  14. src/courses/dto/create-node.dto.ts +45 -0
  15. src/courses/dto/create-podcast.dto.ts +14 -0
  16. src/courses/dto/create-warmup.dto.ts +41 -0
  17. src/courses/dto/synthesize-podcast.dto.ts +11 -0
  18. src/courses/dto/update-chapter.dto.ts +15 -0
  19. src/courses/dto/update-component-progress.dto.ts +18 -0
  20. src/courses/dto/update-course.dto.ts +11 -0
  21. src/courses/dto/update-node.dto.ts +42 -0
  22. src/courses/dto/update-podcast.dto.ts +10 -0
  23. src/courses/dto/update-progress.dto.ts +27 -0
  24. src/courses/dto/upload-doc.dto.ts +11 -0
  25. src/courses/nodes.controller.ts +178 -0
  26. src/courses/podcasts.controller.ts +61 -0
  27. src/courses/validators/node-schema.validator.ts +132 -0
  28. src/debate/debates.controller.ts +5 -25
  29. src/debate/dto/create-debate-topic.dto.ts +15 -0
  30. src/debate/dto/debate-message.dto.ts +11 -0
  31. src/flashcards/dto/create-flashcard.dto.ts +19 -0
  32. src/flashcards/dto/review-card.dto.ts +16 -0
  33. src/flashcards/dto/update-flashcard.dto.ts +15 -0
  34. src/flashcards/flashcards.controller.ts +3 -48
  35. src/philosofun/dto/create-philosofun.dto.ts +15 -0
  36. src/philosofun/dto/update-philosofun.dto.ts +15 -0
  37. src/philosofun/philosofun.controller.ts +2 -29
  38. src/quizzes/dto/create-quiz.dto.ts +22 -0
  39. src/quizzes/dto/update-quiz.dto.ts +22 -0
  40. src/quizzes/quizzes.controller.ts +2 -43
  41. src/users/dto/create-feedback.dto.ts +11 -0
  42. src/users/dto/google-login.dto.ts +7 -0
  43. src/users/dto/login.dto.ts +11 -0
  44. src/users/dto/register.dto.ts +15 -0
  45. src/users/dto/supabase-login.dto.ts +7 -0
  46. src/users/dto/update-user.dto.ts +15 -0
  47. src/users/users.controller.ts +6 -61
package-lock.json CHANGED
@@ -15,6 +15,7 @@
15
  "@nestjs/passport": "^11.0.5",
16
  "@nestjs/platform-express": "^11.1.27",
17
  "@nestjs/swagger": "^11.4.4",
 
18
  "@prisma/client": "^5.7.0",
19
  "@supabase/supabase-js": "^2.39.0",
20
  "bcryptjs": "^3.0.3",
@@ -1891,6 +1892,17 @@
1891
  }
1892
  }
1893
  },
 
 
 
 
 
 
 
 
 
 
 
1894
  "node_modules/@noble/hashes": {
1895
  "version": "1.8.0",
1896
  "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -4612,6 +4624,7 @@
4612
  "version": "2.3.3",
4613
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
4614
  "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 
4615
  "hasInstallScript": true,
4616
  "license": "MIT",
4617
  "optional": true,
 
15
  "@nestjs/passport": "^11.0.5",
16
  "@nestjs/platform-express": "^11.1.27",
17
  "@nestjs/swagger": "^11.4.4",
18
+ "@nestjs/throttler": "^6.5.0",
19
  "@prisma/client": "^5.7.0",
20
  "@supabase/supabase-js": "^2.39.0",
21
  "bcryptjs": "^3.0.3",
 
1892
  }
1893
  }
1894
  },
1895
+ "node_modules/@nestjs/throttler": {
1896
+ "version": "6.5.0",
1897
+ "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
1898
+ "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
1899
+ "license": "MIT",
1900
+ "peerDependencies": {
1901
+ "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
1902
+ "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
1903
+ "reflect-metadata": "^0.1.13 || ^0.2.0"
1904
+ }
1905
+ },
1906
  "node_modules/@noble/hashes": {
1907
  "version": "1.8.0",
1908
  "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
 
4624
  "version": "2.3.3",
4625
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
4626
  "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
4627
+ "dev": true,
4628
  "hasInstallScript": true,
4629
  "license": "MIT",
4630
  "optional": true,
package.json CHANGED
@@ -27,6 +27,7 @@
27
  "@nestjs/passport": "^11.0.5",
28
  "@nestjs/platform-express": "^11.1.27",
29
  "@nestjs/swagger": "^11.4.4",
 
30
  "@prisma/client": "^5.7.0",
31
  "@supabase/supabase-js": "^2.39.0",
32
  "bcryptjs": "^3.0.3",
 
27
  "@nestjs/passport": "^11.0.5",
28
  "@nestjs/platform-express": "^11.1.27",
29
  "@nestjs/swagger": "^11.4.4",
30
+ "@nestjs/throttler": "^6.5.0",
31
  "@prisma/client": "^5.7.0",
32
  "@supabase/supabase-js": "^2.39.0",
33
  "bcryptjs": "^3.0.3",
prisma/schema.prisma CHANGED
@@ -84,12 +84,9 @@ model ConceptNode {
84
  comments Comment[]
85
  quizzes Quiz[]
86
 
87
- // Synchronized Lesson Framework Fields
88
- storyIntro Json?
89
- lessonContents Json?
90
- minigame Json?
91
- finalSummary Json?
92
- lessonType String? // "classic" | "adventure"
93
 
94
  @@index([chapterId, orderIndex])
95
  }
@@ -179,6 +176,10 @@ model Progress {
179
  flashcardCompleted Boolean @default(false)
180
  podcastCompleted Boolean @default(false)
181
  quizCompleted Boolean @default(false)
 
 
 
 
182
  updatedAt DateTime @updatedAt
183
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
184
  node ConceptNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
 
84
  comments Comment[]
85
  quizzes Quiz[]
86
 
87
+ // Component-based Lesson Flow
88
+ lessonFlow Json?
89
+ lessonType String @default("flow")
 
 
 
90
 
91
  @@index([chapterId, orderIndex])
92
  }
 
176
  flashcardCompleted Boolean @default(false)
177
  podcastCompleted Boolean @default(false)
178
  quizCompleted Boolean @default(false)
179
+ activeComponentId String?
180
+ currentComponentIndex Int @default(0)
181
+ completedComponentIds Json?
182
+ componentResults Json?
183
  updatedAt DateTime @updatedAt
184
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
185
  node ConceptNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
prisma/seed.ts CHANGED
@@ -6,6 +6,270 @@ import { createClient } from '@supabase/supabase-js';
6
 
7
  const prisma = new PrismaClient();
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  async function main() {
10
  console.log('Seeding PhiloMind philosophy sanctuary database with Vietnamese Marxist-Leninist Philosophy...');
11
 
@@ -380,6 +644,11 @@ async function main() {
380
  const createdCh1Nodes = [];
381
  for (const n of ch1Nodes) {
382
  const isNode2 = n.title === 'Khái niệm triết học';
 
 
 
 
 
383
  const node = await prisma.conceptNode.create({
384
  data: {
385
  title: n.title,
@@ -391,11 +660,8 @@ async function main() {
391
  videoUrl: n.videoUrl,
392
  orderIndex: n.orderIndex,
393
  chapterId: chapter1.id,
394
- lessonType: isNode2 ? 'adventure' : (n.lessonType || 'classic'),
395
- storyIntro: isNode2 ? seedingData.lesson_1b.storyIntro : (n.storyIntro ? (n.storyIntro as any) : undefined),
396
- lessonContents: isNode2 ? seedingData.lesson_1b.lessonContents : (n.lessonContents ? (n.lessonContents as any) : undefined),
397
- minigame: isNode2 ? seedingData.lesson_1b.minigame : (n.minigame ? (n.minigame as any) : undefined),
398
- finalSummary: isNode2 ? seedingData.lesson_1b.finalSummary : (n.finalSummary ? (n.finalSummary as any) : undefined),
399
  }
400
  });
401
  createdCh1Nodes.push(node);
@@ -439,7 +705,8 @@ async function main() {
439
  videoUrl: n.videoUrl,
440
  orderIndex: n.orderIndex,
441
  chapterId: chapter2.id,
442
- lessonType: 'classic',
 
443
  }
444
  });
445
  createdCh2Nodes.push(node);
@@ -481,7 +748,8 @@ async function main() {
481
  videoUrl: n.videoUrl,
482
  orderIndex: n.orderIndex,
483
  chapterId: chapter3.id,
484
- lessonType: 'classic',
 
485
  }
486
  });
487
  createdCh3Nodes.push(node);
 
6
 
7
  const prisma = new PrismaClient();
8
 
9
+ function buildDefaultLessonFlow(node: any) {
10
+ const flow: any[] = [];
11
+
12
+ if (node.videoUrl) {
13
+ flow.push({
14
+ id: 'lesson-video',
15
+ type: 'media',
16
+ title: 'Video nhập môn',
17
+ config: {
18
+ mediaType: 'video',
19
+ url: node.videoUrl,
20
+ title: node.title,
21
+ subtitle: node.quickTake,
22
+ },
23
+ completionRule: { type: 'viewed' },
24
+ });
25
+ }
26
+
27
+ flow.push({
28
+ id: 'main-reading',
29
+ type: 'markdown',
30
+ title: node.title,
31
+ config: {
32
+ content: node.originalText,
33
+ },
34
+ completionRule: { type: 'viewed' },
35
+ });
36
+
37
+ flow.push({
38
+ id: 'quick-check',
39
+ type: 'mcq',
40
+ title: 'Kiểm tra nhanh',
41
+ config: {
42
+ question: `Ý chính của bài "${node.title}" là gì?`,
43
+ options: [
44
+ { id: 'a', text: node.quickTake, isCorrect: true, explanation: node.summary },
45
+ { id: 'b', text: 'Đây chỉ là một nội dung phụ, chưa phản ánh trọng tâm bài học.', isCorrect: false },
46
+ { id: 'c', text: 'Bài học này không có mối liên hệ với thế giới quan và phương pháp luận.', isCorrect: false },
47
+ ],
48
+ },
49
+ completionRule: { type: 'correct' },
50
+ });
51
+
52
+ flow.push({
53
+ id: 'final-summary',
54
+ type: 'final_summary',
55
+ title: 'Đúc kết bài học',
56
+ config: {
57
+ message: node.quickTake,
58
+ keyTakeaways: [node.summary],
59
+ rewards: { xp: 80, badge: 'Hoàn thành bài học' },
60
+ },
61
+ completionRule: { type: 'viewed' },
62
+ });
63
+
64
+ return flow;
65
+ }
66
+
67
+ function buildOriginLessonFlow(node: any) {
68
+ return [
69
+ {
70
+ id: 'origin-intro',
71
+ type: 'dialogue',
72
+ title: 'Nhiệm vụ khai sáng',
73
+ config: {
74
+ lines: [
75
+ {
76
+ who: 'guide',
77
+ text: 'Nhiệm vụ của bạn: đi tìm nguồn gốc khai sinh của triết học. Hành trình này có hai mảnh ghép: nguồn gốc nhận thức và nguồn gốc xã hội.',
78
+ },
79
+ ],
80
+ },
81
+ completionRule: { type: 'viewed' },
82
+ },
83
+ {
84
+ id: 'origin-video',
85
+ type: 'media',
86
+ title: 'Bối cảnh cổ đại',
87
+ config: {
88
+ mediaType: 'video',
89
+ url: node.videoUrl,
90
+ title: 'Triết học ra đời trong bước ngoặt tư duy của nhân loại',
91
+ subtitle: 'Từ huyền thoại đến lý luận',
92
+ },
93
+ completionRule: { type: 'viewed' },
94
+ },
95
+ {
96
+ id: 'cognitive-origin',
97
+ type: 'mcq',
98
+ title: 'Nguồn gốc nhận thức',
99
+ config: {
100
+ question: 'Điều gì làm cho tư duy triết học bắt đầu thay thế tư duy huyền thoại?',
101
+ options: [
102
+ { id: 'a', text: 'Con người bắt đầu tìm quy luật và lý lẽ để giải thích thế giới.', isCorrect: true, explanation: 'Đây là bước chuyển từ niềm tin huyền thoại sang tư duy lý luận.' },
103
+ { id: 'b', text: 'Con người từ bỏ hoàn toàn nhu cầu hiểu biết thế giới.', isCorrect: false },
104
+ { id: 'c', text: 'Con người chỉ tăng cường nghi lễ cúng bái.', isCorrect: false },
105
+ ],
106
+ },
107
+ completionRule: { type: 'correct' },
108
+ },
109
+ {
110
+ id: 'social-origin-chain',
111
+ type: 'sequence_sorting',
112
+ title: 'Chuỗi nhân quả xã hội',
113
+ config: {
114
+ instruction: 'Chọn các mắt xích theo đúng thứ tự hình thành điều kiện xã hội của triết học.',
115
+ items: [
116
+ { id: 'c1', order: 0, text: 'Sản xuất phát triển, của cải dư thừa và chế độ tư hữu hình thành.' },
117
+ { id: 'c2', order: 1, text: 'Xã hội phân chia giai cấp.' },
118
+ { id: 'c3', order: 2, text: 'Lao động trí óc tách khỏi lao động chân tay.' },
119
+ { id: 'c4', order: 3, text: 'Tầng lớp trí thức hệ thống hóa tri thức thành triết học.' },
120
+ ],
121
+ successFeedback: 'Chuỗi nhân quả đã hoàn chỉnh: đây là nguồn gốc xã hội của triết học.',
122
+ },
123
+ completionRule: { type: 'correct' },
124
+ },
125
+ {
126
+ id: 'origin-reading',
127
+ type: 'markdown',
128
+ title: 'Đúc kết hai nguồn gốc',
129
+ config: {
130
+ content: node.originalText,
131
+ },
132
+ completionRule: { type: 'viewed' },
133
+ },
134
+ {
135
+ id: 'origin-final',
136
+ type: 'final_summary',
137
+ title: 'Hoàn thành bài học',
138
+ config: {
139
+ message: node.quickTake,
140
+ keyTakeaways: [
141
+ 'Nguồn gốc nhận thức: nhu cầu hiểu biết thế giới và năng lực tư duy trừu tượng.',
142
+ 'Nguồn gốc xã hội: phân công lao động, giai cấp và tầng lớp trí thức.',
143
+ ],
144
+ rewards: { xp: 120, badge: 'Nhà Khai Sáng' },
145
+ },
146
+ completionRule: { type: 'viewed' },
147
+ },
148
+ ];
149
+ }
150
+
151
+ function buildLesson1bFlow(source: any) {
152
+ return [
153
+ {
154
+ id: 'ancient-origin-map',
155
+ type: 'target_matching',
156
+ title: 'Nguồn gốc thuật ngữ triết học',
157
+ config: {
158
+ targets: [
159
+ { id: 'china', label: 'Trung Quốc', icon: 'temple_buddhist' },
160
+ { id: 'india', label: 'Ấn Độ', icon: 'water' },
161
+ { id: 'greece', label: 'Hy Lạp', icon: 'account_balance' },
162
+ ],
163
+ items: [
164
+ { id: 'zhe', text: '哲', targetId: 'china' },
165
+ { id: 'darsana', text: "Dar'sana", targetId: 'india' },
166
+ { id: 'philosophia', text: 'φιλοσοφία', targetId: 'greece' },
167
+ ],
168
+ summary: 'Triết học xuất hiện ở nhiều trung tâm văn minh lớn và biểu hiện nhu cầu nhận thức bậc cao của con người.',
169
+ },
170
+ completionRule: { type: 'correct' },
171
+ },
172
+ {
173
+ id: 'worldview-sorting',
174
+ type: 'category_sorting',
175
+ title: 'Phân loại thế giới quan',
176
+ config: {
177
+ categories: [
178
+ { id: 'myth-religion', label: 'Thần thoại - Tôn giáo' },
179
+ { id: 'philosophy', label: 'Triết học' },
180
+ ],
181
+ cards: [
182
+ { id: 'faith', text: 'Niềm tin', categoryId: 'myth-religion' },
183
+ { id: 'ritual', text: 'Nghi lễ', categoryId: 'myth-religion' },
184
+ { id: 'reason', text: 'Công cụ lý tính', categoryId: 'philosophy' },
185
+ { id: 'law', text: 'Quy luật', categoryId: 'philosophy' },
186
+ ],
187
+ summary: 'Triết học tìm cách lý giải thế giới bằng lý tính, logic và khái quát hóa.',
188
+ },
189
+ completionRule: { type: 'correct' },
190
+ },
191
+ {
192
+ id: 'philosophy-features',
193
+ type: 'mindmap_reveal',
194
+ title: 'Bốn đặc trưng cốt lõi',
195
+ config: {
196
+ center: 'Triết học',
197
+ nodes: [
198
+ { id: 'social-consciousness', label: 'Hình thái ý thức xã hội', detail: 'Triết học là một hình thái ý thức xã hội bậc cao.' },
199
+ { id: 'whole', label: 'Chỉnh thể toàn vẹn', detail: 'Triết học xem xét thế giới trong tính chỉnh thể.' },
200
+ { id: 'universal-laws', label: 'Quy luật phổ biến', detail: 'Triết học hướng tới các quy luật chung nhất của tự nhiên, xã hội và tư duy.' },
201
+ { id: 'worldview-core', label: 'Hạt nhân thế giới quan', detail: 'Triết học là hạt nhân lý luận của thế giới quan.' },
202
+ ],
203
+ },
204
+ completionRule: { type: 'viewed' },
205
+ },
206
+ {
207
+ id: 'marxist-definition',
208
+ type: 'mcq',
209
+ title: 'Định nghĩa triết học Mác - Lênin',
210
+ config: {
211
+ question: 'Định nghĩa nào phù hợp nhất với triết học Mác - Lênin?',
212
+ options: [
213
+ { id: 'a', text: 'Tập hợp các niềm tin tôn giáo.', isCorrect: false },
214
+ { id: 'b', text: 'Hệ thống tri thức lý luận chung nhất về thế giới, vị trí con người trong thế giới và phương pháp nhận thức, cải tạo thế giới.', isCorrect: true, explanation: 'Đây là cách hiểu khái quát về chức năng thế giới quan và phương pháp luận của triết học.' },
215
+ { id: 'c', text: 'Một khoa học thực nghiệm nghiên cứu riêng một lĩnh vực.', isCorrect: false },
216
+ ],
217
+ },
218
+ completionRule: { type: 'correct' },
219
+ },
220
+ {
221
+ id: 'philosophy-science-matching',
222
+ type: 'matching_columns',
223
+ title: 'Phân biệt tri thức triết học và khoa học cụ thể',
224
+ config: {
225
+ leftColumn: [
226
+ { id: 'philosophy', text: 'Tri thức triết học' },
227
+ { id: 'specific-science', text: 'Khoa học cụ thể' },
228
+ { id: 'worldview', text: 'Thế giới quan' },
229
+ ],
230
+ rightColumn: [
231
+ { id: 'general', text: 'Khái quát những vấn đề chung nhất' },
232
+ { id: 'domain', text: 'Nghiên cứu một lĩnh vực, đối tượng cụ thể' },
233
+ { id: 'orientation', text: 'Định hướng cách con người nhìn nhận thế giới' },
234
+ ],
235
+ correctPairs: [
236
+ { leftId: 'philosophy', rightId: 'general' },
237
+ { leftId: 'specific-science', rightId: 'domain' },
238
+ { leftId: 'worldview', rightId: 'orientation' },
239
+ ],
240
+ },
241
+ completionRule: { type: 'correct' },
242
+ },
243
+ {
244
+ id: 'all-philosophy-is-science',
245
+ type: 'true_false',
246
+ title: 'Mọi học thuyết triết học đều là khoa học?',
247
+ config: {
248
+ statement: 'Mọi học thuyết triết học đều là khoa học.',
249
+ correctAnswer: false,
250
+ explanation: 'Không phải mọi học thuyết triết học đều khoa học; tính khoa học phụ thuộc vào cơ sở lý luận, phương pháp và khả năng phản ánh đúng hiện thực.',
251
+ },
252
+ completionRule: { type: 'correct' },
253
+ },
254
+ {
255
+ id: 'lesson-1b-final',
256
+ type: 'final_summary',
257
+ title: 'Hoàn thành bài 1.b',
258
+ config: {
259
+ message: source.finalSummary?.summary?.finalStatement || 'Bạn đã nắm được khái niệm triết học.',
260
+ keyTakeaways: [
261
+ 'Triết học là hệ thống tri thức lý luận chung nhất về thế giới.',
262
+ 'Triết học là hạt nhân lý luận của thế giới quan.',
263
+ 'Cần phân biệt tri thức triết học với tri thức của các khoa học cụ thể.',
264
+ ],
265
+ rewards: source.finalSummary?.rewards || { xp: 120, badge: 'Người khai mở tư duy' },
266
+ quiz: source.finalSummary?.quiz || [],
267
+ },
268
+ completionRule: { type: 'viewed' },
269
+ },
270
+ ];
271
+ }
272
+
273
  async function main() {
274
  console.log('Seeding PhiloMind philosophy sanctuary database with Vietnamese Marxist-Leninist Philosophy...');
275
 
 
644
  const createdCh1Nodes = [];
645
  for (const n of ch1Nodes) {
646
  const isNode2 = n.title === 'Khái niệm triết học';
647
+ const lessonFlow = isNode2
648
+ ? buildLesson1bFlow(seedingData.lesson_1b)
649
+ : n.title === 'Nguồn gốc của triết học'
650
+ ? buildOriginLessonFlow(n)
651
+ : buildDefaultLessonFlow(n);
652
  const node = await prisma.conceptNode.create({
653
  data: {
654
  title: n.title,
 
660
  videoUrl: n.videoUrl,
661
  orderIndex: n.orderIndex,
662
  chapterId: chapter1.id,
663
+ lessonType: 'flow',
664
+ lessonFlow: lessonFlow as any,
 
 
 
665
  }
666
  });
667
  createdCh1Nodes.push(node);
 
705
  videoUrl: n.videoUrl,
706
  orderIndex: n.orderIndex,
707
  chapterId: chapter2.id,
708
+ lessonType: 'flow',
709
+ lessonFlow: buildDefaultLessonFlow(n) as any,
710
  }
711
  });
712
  createdCh2Nodes.push(node);
 
748
  videoUrl: n.videoUrl,
749
  orderIndex: n.orderIndex,
750
  chapterId: chapter3.id,
751
+ lessonType: 'flow',
752
+ lessonFlow: buildDefaultLessonFlow(n) as any,
753
  }
754
  });
755
  createdCh3Nodes.push(node);
src/app.module.ts CHANGED
@@ -12,12 +12,18 @@ import { UsersModule } from './users/users.module';
12
  import { PhilosofunModule } from './philosofun/philosofun.module';
13
  import { AuthModule } from './auth/auth.module';
14
  import { AppController } from './app.controller';
 
 
15
 
16
  @Module({
17
  imports: [
18
  ConfigModule.forRoot({
19
  isGlobal: true,
20
  }),
 
 
 
 
21
  DatabaseModule,
22
  AIModule,
23
  SupabaseModule,
@@ -31,5 +37,11 @@ import { AppController } from './app.controller';
31
  AuthModule,
32
  ],
33
  controllers: [AppController],
 
 
 
 
 
 
34
  })
35
  export class AppModule {}
 
12
  import { PhilosofunModule } from './philosofun/philosofun.module';
13
  import { AuthModule } from './auth/auth.module';
14
  import { AppController } from './app.controller';
15
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
16
+ import { APP_GUARD } from '@nestjs/core';
17
 
18
  @Module({
19
  imports: [
20
  ConfigModule.forRoot({
21
  isGlobal: true,
22
  }),
23
+ ThrottlerModule.forRoot([{
24
+ ttl: 60000,
25
+ limit: 100, // 100 requests per minute by default
26
+ }]),
27
  DatabaseModule,
28
  AIModule,
29
  SupabaseModule,
 
37
  AuthModule,
38
  ],
39
  controllers: [AppController],
40
+ providers: [
41
+ {
42
+ provide: APP_GUARD,
43
+ useClass: ThrottlerGuard,
44
+ },
45
+ ],
46
  })
47
  export class AppModule {}
src/courses/chapters.controller.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, Post, Body, Param, Query, Put, Delete, UseGuards } from '@nestjs/common';
2
+ import { CoursesService } from './courses.service';
3
+ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
+ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
+ import { RolesGuard } from '../auth/roles.guard';
6
+ import { Roles } from '../auth/roles.decorator';
7
+ import { CreateChapterDto } from './dto/create-chapter.dto';
8
+ import { UpdateChapterDto } from './dto/update-chapter.dto';
9
+
10
+ @ApiTags('Courses & Roadmaps')
11
+ @Controller('chapters')
12
+ @UseGuards(JwtAuthGuard, RolesGuard)
13
+ @ApiBearerAuth()
14
+ export class ChaptersController {
15
+ constructor(private coursesService: CoursesService) {}
16
+
17
+ @Post()
18
+ @Roles('admin')
19
+ @ApiOperation({ summary: 'Create a new chapter (Admin)' })
20
+ async createChapter(@Body() dto: CreateChapterDto) {
21
+ return this.coursesService.createChapter(dto.title, dto.orderIndex, dto.courseId, dto.parentChapterId);
22
+ }
23
+
24
+ @Get()
25
+ @ApiOperation({ summary: 'List all chapters' })
26
+ async getChapters(@Query('courseId') courseId?: string) {
27
+ return this.coursesService.getChapters(courseId);
28
+ }
29
+
30
+ @Get(':id')
31
+ @Roles('admin')
32
+ @ApiOperation({ summary: 'Get single chapter details (Admin)' })
33
+ async getChapterById(@Param('id') id: string) {
34
+ return this.coursesService.getChapterById(id);
35
+ }
36
+
37
+ @Put(':id')
38
+ @Roles('admin')
39
+ @ApiOperation({ summary: 'Update a chapter (Admin)' })
40
+ async updateChapter(@Param('id') id: string, @Body() dto: UpdateChapterDto) {
41
+ return this.coursesService.updateChapter(id, dto.title, dto.orderIndex, dto.parentChapterId);
42
+ }
43
+
44
+ @Delete(':id')
45
+ @Roles('admin')
46
+ @ApiOperation({ summary: 'Delete a chapter (Admin)' })
47
+ async deleteChapter(@Param('id') id: string) {
48
+ return this.coursesService.deleteChapter(id);
49
+ }
50
+ }
src/courses/courses.controller.ts CHANGED
@@ -1,275 +1,13 @@
1
- import { Controller, Get, Post, Body, Param, Query, Patch, Put, Delete, UseInterceptors, UploadedFile, BadRequestException, UseGuards, Req } from '@nestjs/common';
2
  import { CoursesService } from './courses.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsOptional, IsNumber } from 'class-validator';
5
  import { FileInterceptor } from '@nestjs/platform-express';
6
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
7
  import { RolesGuard } from '../auth/roles.guard';
8
  import { Roles } from '../auth/roles.decorator';
9
-
10
- class CreateCourseDto {
11
- @IsString()
12
- @IsNotEmpty()
13
- title: string;
14
-
15
- @IsString()
16
- @IsOptional()
17
- description?: string;
18
-
19
- @IsString()
20
- @IsNotEmpty()
21
- userId: string;
22
- }
23
-
24
- class UpdateCourseDto {
25
- @IsString()
26
- @IsOptional()
27
- title?: string;
28
-
29
- @IsString()
30
- @IsOptional()
31
- description?: string;
32
- }
33
-
34
- class UploadDocDto {
35
- @IsString()
36
- @IsNotEmpty()
37
- fileName: string;
38
-
39
- @IsString()
40
- @IsNotEmpty()
41
- content: string;
42
- }
43
-
44
- class UpdateProgressDto {
45
- @IsString()
46
- @IsOptional()
47
- userId: string;
48
-
49
- @IsString()
50
- @IsOptional()
51
- status?: string;
52
-
53
- @IsOptional()
54
- lessonCompleted?: boolean;
55
-
56
- @IsOptional()
57
- flashcardCompleted?: boolean;
58
-
59
- @IsOptional()
60
- podcastCompleted?: boolean;
61
-
62
- @IsOptional()
63
- quizCompleted?: boolean;
64
- }
65
-
66
- class CreateChapterDto {
67
- @IsString()
68
- @IsNotEmpty()
69
- title: string;
70
-
71
- @IsNumber()
72
- @IsNotEmpty()
73
- orderIndex: number;
74
-
75
- @IsString()
76
- @IsNotEmpty()
77
- courseId: string;
78
-
79
- @IsString()
80
- @IsOptional()
81
- parentChapterId?: string;
82
- }
83
-
84
- class UpdateChapterDto {
85
- @IsString()
86
- @IsOptional()
87
- title?: string;
88
-
89
- @IsNumber()
90
- @IsOptional()
91
- orderIndex?: number;
92
-
93
- @IsString()
94
- @IsOptional()
95
- parentChapterId?: string;
96
- }
97
-
98
- class CreateNodeDto {
99
- @IsString()
100
- @IsNotEmpty()
101
- title: string;
102
-
103
- @IsString()
104
- @IsNotEmpty()
105
- summary: string;
106
-
107
- @IsString()
108
- @IsNotEmpty()
109
- originalText: string;
110
-
111
- @IsString()
112
- @IsNotEmpty()
113
- quickTake: string;
114
-
115
- @IsString()
116
- @IsOptional()
117
- difficulty?: string;
118
-
119
- @IsString()
120
- @IsOptional()
121
- timeToRead?: string;
122
-
123
- @IsString()
124
- @IsOptional()
125
- videoUrl?: string; // YouTube Video URL
126
-
127
- @IsNumber()
128
- @IsNotEmpty()
129
- orderIndex: number;
130
-
131
- @IsString()
132
- @IsNotEmpty()
133
- chapterId: string;
134
-
135
- @IsString()
136
- @IsNotEmpty()
137
- lessonType: string;
138
-
139
- @IsOptional()
140
- storyIntro?: any;
141
-
142
- @IsOptional()
143
- lessonContents?: any;
144
-
145
- @IsOptional()
146
- minigame?: any;
147
-
148
- @IsOptional()
149
- finalSummary?: any;
150
- }
151
-
152
- class UpdateNodeDto {
153
- @IsString()
154
- @IsOptional()
155
- title?: string;
156
-
157
- @IsString()
158
- @IsOptional()
159
- summary?: string;
160
-
161
- @IsString()
162
- @IsOptional()
163
- originalText?: string;
164
-
165
- @IsString()
166
- @IsOptional()
167
- quickTake?: string;
168
-
169
- @IsString()
170
- @IsOptional()
171
- difficulty?: string;
172
-
173
- @IsString()
174
- @IsOptional()
175
- timeToRead?: string;
176
-
177
- @IsString()
178
- @IsOptional()
179
- videoUrl?: string; // YouTube Video URL
180
-
181
- @IsNumber()
182
- @IsOptional()
183
- orderIndex?: number;
184
-
185
- @IsString()
186
- @IsOptional()
187
- lessonType?: string;
188
-
189
- @IsOptional()
190
- storyIntro?: any;
191
-
192
- @IsOptional()
193
- lessonContents?: any;
194
-
195
- @IsOptional()
196
- minigame?: any;
197
-
198
- @IsOptional()
199
- finalSummary?: any;
200
- }
201
-
202
- class SynthesizePodcastDto {
203
- @IsString()
204
- @IsNotEmpty()
205
- nodeId: string;
206
-
207
- @IsString()
208
- @IsNotEmpty()
209
- scriptText: string;
210
- }
211
-
212
- class CreateWarmupDto {
213
- @IsString()
214
- @IsNotEmpty()
215
- type: string; // "image-guess" | "story"
216
-
217
- @IsString()
218
- @IsNotEmpty()
219
- title: string;
220
-
221
- @IsString()
222
- @IsOptional()
223
- image?: string;
224
-
225
- @IsString()
226
- @IsOptional()
227
- blanks?: string;
228
-
229
- @IsString()
230
- @IsOptional()
231
- answer?: string;
232
-
233
- @IsString()
234
- @IsOptional()
235
- story?: string;
236
-
237
- @IsString()
238
- @IsOptional()
239
- question?: string;
240
-
241
- @IsOptional()
242
- options?: string[];
243
-
244
- @IsOptional()
245
- correctIndex?: number;
246
-
247
- @IsString()
248
- @IsNotEmpty()
249
- reveal: string;
250
- }
251
-
252
- class CreatePodcastDto {
253
- @IsString()
254
- @IsNotEmpty()
255
- nodeId: string;
256
-
257
- @IsString()
258
- @IsNotEmpty()
259
- audioUrl: string;
260
-
261
- @IsNotEmpty()
262
- transcript: any;
263
- }
264
-
265
- class UpdatePodcastDto {
266
- @IsString()
267
- @IsOptional()
268
- audioUrl?: string;
269
-
270
- @IsOptional()
271
- transcript?: any;
272
- }
273
 
274
  @ApiTags('Courses & Roadmaps')
275
  @Controller()
@@ -289,8 +27,9 @@ export class CoursesController {
289
 
290
  @Get('courses')
291
  @ApiOperation({ summary: 'Retrieve courses list' })
292
- async getCourses(@Query('userId') userId?: string) {
293
- return this.coursesService.getCourses(userId);
 
294
  }
295
 
296
  @Get('courses/:id')
@@ -321,202 +60,6 @@ export class CoursesController {
321
  return this.coursesService.processDocumentUpload(courseId, dto.fileName, dto.content);
322
  }
323
 
324
- // ==================== CHAPTERS ====================
325
-
326
- @Post('chapters')
327
- @Roles('admin')
328
- @ApiOperation({ summary: 'Create a new chapter (Admin)' })
329
- async createChapter(@Body() dto: CreateChapterDto) {
330
- return this.coursesService.createChapter(dto.title, dto.orderIndex, dto.courseId, dto.parentChapterId);
331
- }
332
-
333
- @Get('chapters')
334
- @ApiOperation({ summary: 'List all chapters (Admin)' })
335
- async getChapters(@Query('courseId') courseId?: string) {
336
- return this.coursesService.getChapters(courseId);
337
- }
338
-
339
- @Get('chapters/:id')
340
- @Roles('admin')
341
- @ApiOperation({ summary: 'Get single chapter details (Admin)' })
342
- async getChapterById(@Param('id') id: string) {
343
- return this.coursesService.getChapterById(id);
344
- }
345
-
346
- @Put('chapters/:id')
347
- @Roles('admin')
348
- @ApiOperation({ summary: 'Update a chapter (Admin)' })
349
- async updateChapter(@Param('id') id: string, @Body() dto: UpdateChapterDto) {
350
- return this.coursesService.updateChapter(id, dto.title, dto.orderIndex, dto.parentChapterId);
351
- }
352
-
353
- @Delete('chapters/:id')
354
- @Roles('admin')
355
- @ApiOperation({ summary: 'Delete a chapter (Admin)' })
356
- async deleteChapter(@Param('id') id: string) {
357
- return this.coursesService.deleteChapter(id);
358
- }
359
-
360
- // ==================== CONCEPT NODES ====================
361
-
362
- @Get('courses/:id/journey')
363
- @ApiOperation({ summary: 'Get course journey roadmap nodes' })
364
- async getCourseJourney(@Param('id') courseId: string, @Req() req: any) {
365
- return this.coursesService.getCourseJourney(courseId, req.user.id);
366
- }
367
-
368
- @Get('courses/nodes/:nodeId')
369
- @ApiOperation({ summary: 'Retrieve comprehensive learn detail for a concept node' })
370
- async getNodeDetails(@Param('nodeId') nodeId: string, @Req() req: any) {
371
- return this.coursesService.getNodeDetails(nodeId, req.user.id);
372
- }
373
-
374
- @Get('courses/nodes/:nodeId/core')
375
- @ApiOperation({ summary: 'Retrieve core progress and type info for a concept node' })
376
- async getNodeCore(@Param('nodeId') nodeId: string, @Req() req: any) {
377
- return this.coursesService.getNodeCore(nodeId, req.user.id);
378
- }
379
-
380
- @Post('courses/nodes/:nodeId/complete')
381
- @ApiOperation({ summary: 'Mark node as completed and auto-unlock next node' })
382
- async completeNode(
383
- @Param('nodeId') nodeId: string,
384
- @Req() req: any,
385
- ) {
386
- return this.coursesService.completeNode(nodeId, req.user.id);
387
- }
388
-
389
- @Patch('courses/nodes/:nodeId/progress')
390
- @ApiOperation({ summary: 'Update node learn progress status' })
391
- async updateNodeProgress(
392
- @Param('nodeId') nodeId: string,
393
- @Body() dto: UpdateProgressDto,
394
- @Req() req: any,
395
- ) {
396
- return this.coursesService.updateNodeProgress(
397
- req.user.id,
398
- nodeId,
399
- dto.status,
400
- dto.lessonCompleted,
401
- dto.flashcardCompleted,
402
- dto.podcastCompleted,
403
- dto.quizCompleted,
404
- );
405
- }
406
-
407
- @Post('nodes')
408
- @Roles('admin')
409
- @ApiOperation({ summary: 'Create a new concept node (Admin)' })
410
- async createNode(@Body() dto: CreateNodeDto) {
411
- return this.coursesService.createNode(dto);
412
- }
413
-
414
- @Get('nodes')
415
- @Roles('admin')
416
- @ApiOperation({ summary: 'List all concept nodes (Admin)' })
417
- async getNodes(@Query('chapterId') chapterId?: string) {
418
- return this.coursesService.getNodes(chapterId);
419
- }
420
-
421
- @Put('nodes/:nodeId')
422
- @Roles('admin')
423
- @ApiOperation({ summary: 'Update a concept node (Admin)' })
424
- async updateNode(@Param('nodeId') nodeId: string, @Body() dto: UpdateNodeDto) {
425
- return this.coursesService.updateNode(nodeId, dto);
426
- }
427
-
428
- @Delete('nodes/:nodeId')
429
- @Roles('admin')
430
- @ApiOperation({ summary: 'Delete a concept node (Admin)' })
431
- async deleteNode(@Param('nodeId') nodeId: string) {
432
- return this.coursesService.deleteNode(nodeId);
433
- }
434
-
435
- // ==================== PODCASTS ====================
436
-
437
- @Get('podcasts')
438
- @Roles('admin')
439
- @ApiOperation({ summary: 'List all podcasts (Admin)' })
440
- async getPodcasts() {
441
- return this.coursesService.getPodcasts();
442
- }
443
-
444
- @Get('podcasts/:id')
445
- @Roles('admin')
446
- @ApiOperation({ summary: 'Get single podcast details (Admin)' })
447
- async getPodcastById(@Param('id') id: string) {
448
- return this.coursesService.getPodcastById(id);
449
- }
450
-
451
- @Post('podcasts')
452
- @Roles('admin')
453
- @ApiOperation({ summary: 'Create a new podcast manually (Admin)' })
454
- async createPodcast(@Body() dto: CreatePodcastDto) {
455
- return this.coursesService.createPodcast(dto);
456
- }
457
-
458
- @Put('podcasts/:id')
459
- @Roles('admin')
460
- @ApiOperation({ summary: 'Update a podcast (Admin)' })
461
- async updatePodcast(@Param('id') id: string, @Body() dto: UpdatePodcastDto) {
462
- return this.coursesService.updatePodcast(id, dto);
463
- }
464
-
465
- @Delete('podcasts/:id')
466
- @Roles('admin')
467
- @ApiOperation({ summary: 'Delete a podcast (Admin)' })
468
- async deletePodcast(@Param('id') id: string) {
469
- return this.coursesService.deletePodcast(id);
470
- }
471
-
472
- @Post('podcasts/synthesize')
473
- @Roles('admin')
474
- @ApiOperation({ summary: 'Synthesize script text via TTS and return public audio URL for preview' })
475
- async synthesizePodcast(@Body() dto: SynthesizePodcastDto) {
476
- return this.coursesService.synthesizePodcast(dto.nodeId, dto.scriptText);
477
- }
478
-
479
- // ==================== WARMUPS ====================
480
-
481
- @Post('nodes/:nodeId/warmups')
482
- @Roles('admin')
483
- @ApiOperation({ summary: 'Create a new warmup for a concept node (Admin)' })
484
- async createWarmup(@Param('nodeId') nodeId: string, @Body() dto: CreateWarmupDto) {
485
- return this.coursesService.createWarmup(nodeId, dto);
486
- }
487
-
488
- @Get('nodes/:nodeId/warmups')
489
- @ApiOperation({ summary: 'List all warmups for a concept node' })
490
- async getWarmups(@Param('nodeId') nodeId: string) {
491
- return this.coursesService.getWarmups(nodeId);
492
- }
493
-
494
- @Delete('warmups/:id')
495
- @Roles('admin')
496
- @ApiOperation({ summary: 'Delete a warmup by ID (Admin)' })
497
- async deleteWarmup(@Param('id') id: string) {
498
- return this.coursesService.deleteWarmup(id);
499
- }
500
-
501
- // ==================== DISCUSSIONS / COMMENTS ====================
502
-
503
- @Post('courses/nodes/:nodeId/comments')
504
- @ApiOperation({ summary: 'Post a comment on a concept node discussion' })
505
- async createComment(
506
- @Param('nodeId') nodeId: string,
507
- @Body() dto: { userId: string; content: string; role?: string },
508
- @Req() req: any,
509
- ) {
510
- const role = req.user.role === 'admin' ? 'admin' : 'student';
511
- return this.coursesService.createComment(nodeId, req.user.id, dto.content, role);
512
- }
513
-
514
- @Get('courses/nodes/:nodeId/comments')
515
- @ApiOperation({ summary: 'Get all comments for a concept node discussion' })
516
- async getComments(@Param('nodeId') nodeId: string) {
517
- return this.coursesService.getComments(nodeId);
518
- }
519
-
520
  // ==================== BUCKET FILE UPLOADER ====================
521
 
522
  @Post('files/upload')
@@ -529,28 +72,4 @@ export class CoursesController {
529
  }
530
  return this.coursesService.saveUploadedFile(file.originalname, file.buffer, file.mimetype);
531
  }
532
-
533
- // ==================== PDF REFERENCE DOCUMENTS CRUD ====================
534
-
535
- @Post('documents')
536
- @Roles('admin')
537
- @ApiOperation({ summary: 'Save a PDF document reference' })
538
- async createDocument(
539
- @Body() dto: { courseId: string; fileName: string; fileUrl: string; status?: string; title?: string; description?: string },
540
- ) {
541
- return this.coursesService.createDocument(dto.courseId, dto.fileName, dto.fileUrl, dto.status, dto.title, dto.description);
542
- }
543
-
544
- @Get('documents')
545
- @ApiOperation({ summary: 'List all reference PDF documents' })
546
- async listDocuments(@Query('courseId') courseId?: string) {
547
- return this.coursesService.listDocuments(courseId);
548
- }
549
-
550
- @Delete('documents/:id')
551
- @Roles('admin')
552
- @ApiOperation({ summary: 'Delete a PDF reference document by ID' })
553
- async deleteDocument(@Param('id') id: string) {
554
- return this.coursesService.deleteDocument(id);
555
- }
556
  }
 
1
+ import { Controller, Get, Post, Body, Param, Query, Put, Delete, UseInterceptors, UploadedFile, BadRequestException, UseGuards, Req } from '@nestjs/common';
2
  import { CoursesService } from './courses.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { FileInterceptor } from '@nestjs/platform-express';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
+ import { CreateCourseDto } from './dto/create-course.dto';
9
+ import { UpdateCourseDto } from './dto/update-course.dto';
10
+ import { UploadDocDto } from './dto/upload-doc.dto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  @ApiTags('Courses & Roadmaps')
13
  @Controller()
 
27
 
28
  @Get('courses')
29
  @ApiOperation({ summary: 'Retrieve courses list' })
30
+ async getCourses(@Req() req: any, @Query('userId') userId?: string) {
31
+ const resolvedUserId = req.user.role === 'admin' ? (userId || req.user.id) : req.user.id;
32
+ return this.coursesService.getCourses(resolvedUserId);
33
  }
34
 
35
  @Get('courses/:id')
 
60
  return this.coursesService.processDocumentUpload(courseId, dto.fileName, dto.content);
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  // ==================== BUCKET FILE UPLOADER ====================
64
 
65
  @Post('files/upload')
 
72
  }
73
  return this.coursesService.saveUploadedFile(file.originalname, file.buffer, file.mimetype);
74
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
src/courses/courses.module.ts CHANGED
@@ -1,9 +1,12 @@
1
  import { Module } from '@nestjs/common';
2
  import { CoursesService } from './courses.service';
3
  import { CoursesController } from './courses.controller';
 
 
 
4
 
5
  @Module({
6
- controllers: [CoursesController],
7
  providers: [CoursesService],
8
  })
9
  export class CoursesModule {}
 
1
  import { Module } from '@nestjs/common';
2
  import { CoursesService } from './courses.service';
3
  import { CoursesController } from './courses.controller';
4
+ import { ChaptersController } from './chapters.controller';
5
+ import { NodesController } from './nodes.controller';
6
+ import { PodcastsController } from './podcasts.controller';
7
 
8
  @Module({
9
+ controllers: [CoursesController, ChaptersController, NodesController, PodcastsController],
10
  providers: [CoursesService],
11
  })
12
  export class CoursesModule {}
src/courses/courses.service.ts CHANGED
@@ -6,6 +6,7 @@ import { SupabaseService } from '../supabase/supabase.service';
6
  import * as fs from 'fs';
7
  import * as path from 'path';
8
  import { randomUUID } from 'crypto';
 
9
 
10
  @Injectable()
11
  export class CoursesService {
@@ -196,6 +197,10 @@ export class CoursesService {
196
  flashcardCompleted: true,
197
  podcastCompleted: true,
198
  quizCompleted: true,
 
 
 
 
199
  }
200
  },
201
  _count: { select: { flashcards: true } },
@@ -245,6 +250,10 @@ export class CoursesService {
245
  flashcardCompleted: true,
246
  podcastCompleted: true,
247
  quizCompleted: true,
 
 
 
 
248
  }
249
  },
250
  },
@@ -519,6 +528,8 @@ export class CoursesService {
519
 
520
  async createNode(dto: any) {
521
  await this.getChapterById(dto.chapterId);
 
 
522
  return this.prisma.conceptNode.create({
523
  data: {
524
  title: dto.title,
@@ -530,11 +541,8 @@ export class CoursesService {
530
  videoUrl: dto.videoUrl || null,
531
  orderIndex: dto.orderIndex,
532
  chapterId: dto.chapterId,
533
- lessonType: dto.lessonType,
534
- storyIntro: dto.storyIntro !== undefined ? dto.storyIntro : null,
535
- lessonContents: dto.lessonContents !== undefined ? dto.lessonContents : null,
536
- minigame: dto.minigame !== undefined ? dto.minigame : null,
537
- finalSummary: dto.finalSummary !== undefined ? dto.finalSummary : null,
538
  },
539
  });
540
  }
@@ -553,6 +561,9 @@ export class CoursesService {
553
 
554
  async updateNode(nodeId: string, dto: any) {
555
  await this.getNodeDetails(nodeId, 'admin-user');
 
 
 
556
  return this.prisma.conceptNode.update({
557
  where: { id: nodeId },
558
  data: {
@@ -564,11 +575,103 @@ export class CoursesService {
564
  timeToRead: dto.timeToRead,
565
  videoUrl: dto.videoUrl !== undefined ? dto.videoUrl : undefined,
566
  orderIndex: dto.orderIndex,
567
- lessonType: dto.lessonType !== undefined ? dto.lessonType : undefined,
568
- storyIntro: dto.storyIntro !== undefined ? dto.storyIntro : undefined,
569
- lessonContents: dto.lessonContents !== undefined ? dto.lessonContents : undefined,
570
- minigame: dto.minigame !== undefined ? dto.minigame : undefined,
571
- finalSummary: dto.finalSummary !== undefined ? dto.finalSummary : undefined,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  },
573
  });
574
  }
 
6
  import * as fs from 'fs';
7
  import * as path from 'path';
8
  import { randomUUID } from 'crypto';
9
+ import { NodeSchemaValidator } from './validators/node-schema.validator';
10
 
11
  @Injectable()
12
  export class CoursesService {
 
197
  flashcardCompleted: true,
198
  podcastCompleted: true,
199
  quizCompleted: true,
200
+ activeComponentId: true,
201
+ currentComponentIndex: true,
202
+ completedComponentIds: true,
203
+ componentResults: true,
204
  }
205
  },
206
  _count: { select: { flashcards: true } },
 
250
  flashcardCompleted: true,
251
  podcastCompleted: true,
252
  quizCompleted: true,
253
+ activeComponentId: true,
254
+ currentComponentIndex: true,
255
+ completedComponentIds: true,
256
+ componentResults: true,
257
  }
258
  },
259
  },
 
528
 
529
  async createNode(dto: any) {
530
  await this.getChapterById(dto.chapterId);
531
+ const lessonFlow = dto.lessonFlow || this.buildDefaultLessonFlow(dto);
532
+ NodeSchemaValidator.validateNode(lessonFlow);
533
  return this.prisma.conceptNode.create({
534
  data: {
535
  title: dto.title,
 
541
  videoUrl: dto.videoUrl || null,
542
  orderIndex: dto.orderIndex,
543
  chapterId: dto.chapterId,
544
+ lessonType: 'flow',
545
+ lessonFlow: lessonFlow as any,
 
 
 
546
  },
547
  });
548
  }
 
561
 
562
  async updateNode(nodeId: string, dto: any) {
563
  await this.getNodeDetails(nodeId, 'admin-user');
564
+ if (dto.lessonFlow !== undefined) {
565
+ NodeSchemaValidator.validateNode(dto.lessonFlow);
566
+ }
567
  return this.prisma.conceptNode.update({
568
  where: { id: nodeId },
569
  data: {
 
575
  timeToRead: dto.timeToRead,
576
  videoUrl: dto.videoUrl !== undefined ? dto.videoUrl : undefined,
577
  orderIndex: dto.orderIndex,
578
+ lessonType: 'flow',
579
+ lessonFlow: dto.lessonFlow !== undefined ? (dto.lessonFlow as any) : undefined,
580
+ },
581
+ });
582
+ }
583
+
584
+ private buildDefaultLessonFlow(dto: any) {
585
+ const flow: any[] = [];
586
+
587
+ if (dto.videoUrl) {
588
+ flow.push({
589
+ id: 'lesson-video',
590
+ type: 'media',
591
+ title: 'Video nhập môn',
592
+ config: {
593
+ mediaType: 'video',
594
+ url: dto.videoUrl,
595
+ title: dto.title,
596
+ },
597
+ completionRule: { type: 'viewed' },
598
+ });
599
+ }
600
+
601
+ flow.push({
602
+ id: 'main-reading',
603
+ type: 'markdown',
604
+ title: dto.title || 'Nội dung bài học',
605
+ config: {
606
+ content: dto.originalText || dto.summary || 'Nội dung bài học đang được cập nhật.',
607
+ },
608
+ completionRule: { type: 'viewed' },
609
+ });
610
+
611
+ flow.push({
612
+ id: 'final-summary',
613
+ type: 'final_summary',
614
+ title: 'Đúc kết bài học',
615
+ config: {
616
+ message: dto.quickTake || dto.summary || 'Hoàn thành bài học.',
617
+ keyTakeaways: [dto.summary || dto.quickTake || 'Nắm được nội dung trọng tâm của bài học.'],
618
+ rewards: { xp: 80, badge: 'Hoàn thành bài học' },
619
+ },
620
+ completionRule: { type: 'viewed' },
621
+ });
622
+
623
+ return flow;
624
+ }
625
+
626
+ async updateComponentProgress(
627
+ userId: string,
628
+ nodeId: string,
629
+ activeComponentId?: string,
630
+ currentComponentIndex?: number,
631
+ completedComponentIds?: string[],
632
+ componentResult?: any,
633
+ ) {
634
+ const existing = await this.prisma.progress.findFirst({
635
+ where: { userId, nodeId },
636
+ });
637
+
638
+ const previousResults = Array.isArray(existing?.componentResults)
639
+ ? (existing?.componentResults as any[])
640
+ : [];
641
+ const nextResults = componentResult
642
+ ? [
643
+ ...previousResults.filter((result) => result.componentId !== componentResult.componentId),
644
+ {
645
+ ...componentResult,
646
+ completedAt: componentResult.completedAt || new Date().toISOString(),
647
+ },
648
+ ]
649
+ : previousResults;
650
+
651
+ const updateData: any = {
652
+ status: existing?.status === 'completed' ? 'completed' : 'in_progress',
653
+ };
654
+ if (activeComponentId !== undefined) updateData.activeComponentId = activeComponentId;
655
+ if (currentComponentIndex !== undefined) updateData.currentComponentIndex = currentComponentIndex;
656
+ if (completedComponentIds !== undefined) updateData.completedComponentIds = completedComponentIds as any;
657
+ if (componentResult !== undefined) updateData.componentResults = nextResults as any;
658
+
659
+ if (existing) {
660
+ return this.prisma.progress.update({
661
+ where: { id: existing.id },
662
+ data: updateData,
663
+ });
664
+ }
665
+
666
+ return this.prisma.progress.create({
667
+ data: {
668
+ userId,
669
+ nodeId,
670
+ status: 'in_progress',
671
+ activeComponentId: activeComponentId || null,
672
+ currentComponentIndex: currentComponentIndex || 0,
673
+ completedComponentIds: (completedComponentIds || []) as any,
674
+ componentResults: nextResults as any,
675
  },
676
  });
677
  }
src/courses/dto/create-chapter.dto.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional, IsNumber } from 'class-validator';
2
+
3
+ export class CreateChapterDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ title: string;
7
+
8
+ @IsNumber()
9
+ @IsNotEmpty()
10
+ orderIndex: number;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ courseId: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ parentChapterId?: string;
19
+ }
src/courses/dto/create-comment.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateCommentDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ userId?: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ content: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ role?: string;
15
+ }
src/courses/dto/create-course.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateCourseDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ title: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ description?: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ userId: string;
15
+ }
src/courses/dto/create-document-ref.dto.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateDocumentRefDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ courseId: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ fileName: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ fileUrl: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ status?: string;
19
+
20
+ @IsString()
21
+ @IsOptional()
22
+ title?: string;
23
+
24
+ @IsString()
25
+ @IsOptional()
26
+ description?: string;
27
+ }
src/courses/dto/create-node.dto.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional, IsNumber } from 'class-validator';
2
+
3
+ export class CreateNodeDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ title: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ summary: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ originalText: string;
15
+
16
+ @IsString()
17
+ @IsNotEmpty()
18
+ quickTake: string;
19
+
20
+ @IsString()
21
+ @IsOptional()
22
+ difficulty?: string;
23
+
24
+ @IsString()
25
+ @IsOptional()
26
+ timeToRead?: string;
27
+
28
+ @IsString()
29
+ @IsOptional()
30
+ videoUrl?: string;
31
+
32
+ @IsNumber()
33
+ @IsNotEmpty()
34
+ orderIndex: number;
35
+
36
+ @IsString()
37
+ @IsNotEmpty()
38
+ chapterId: string;
39
+
40
+ @IsOptional()
41
+ lessonType?: string;
42
+
43
+ @IsOptional()
44
+ lessonFlow?: any;
45
+ }
src/courses/dto/create-podcast.dto.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class CreatePodcastDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ nodeId: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ audioUrl: string;
11
+
12
+ @IsNotEmpty()
13
+ transcript: any;
14
+ }
src/courses/dto/create-warmup.dto.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateWarmupDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ type: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ title: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ image?: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ blanks?: string;
19
+
20
+ @IsString()
21
+ @IsOptional()
22
+ answer?: string;
23
+
24
+ @IsString()
25
+ @IsOptional()
26
+ story?: string;
27
+
28
+ @IsString()
29
+ @IsOptional()
30
+ question?: string;
31
+
32
+ @IsOptional()
33
+ options?: string[];
34
+
35
+ @IsOptional()
36
+ correctIndex?: number;
37
+
38
+ @IsString()
39
+ @IsNotEmpty()
40
+ reveal: string;
41
+ }
src/courses/dto/synthesize-podcast.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class SynthesizePodcastDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ nodeId: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ scriptText: string;
11
+ }
src/courses/dto/update-chapter.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional, IsNumber } from 'class-validator';
2
+
3
+ export class UpdateChapterDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ title?: string;
7
+
8
+ @IsNumber()
9
+ @IsOptional()
10
+ orderIndex?: number;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ parentChapterId?: string;
15
+ }
src/courses/dto/update-component-progress.dto.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
2
+
3
+ export class UpdateComponentProgressDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ activeComponentId?: string;
7
+
8
+ @IsNumber()
9
+ @IsOptional()
10
+ currentComponentIndex?: number;
11
+
12
+ @IsArray()
13
+ @IsOptional()
14
+ completedComponentIds?: string[];
15
+
16
+ @IsOptional()
17
+ componentResult?: any;
18
+ }
src/courses/dto/update-course.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional } from 'class-validator';
2
+
3
+ export class UpdateCourseDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ title?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ description?: string;
11
+ }
src/courses/dto/update-node.dto.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional, IsNumber } from 'class-validator';
2
+
3
+ export class UpdateNodeDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ title?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ summary?: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ originalText?: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ quickTake?: string;
19
+
20
+ @IsString()
21
+ @IsOptional()
22
+ difficulty?: string;
23
+
24
+ @IsString()
25
+ @IsOptional()
26
+ timeToRead?: string;
27
+
28
+ @IsString()
29
+ @IsOptional()
30
+ videoUrl?: string;
31
+
32
+ @IsNumber()
33
+ @IsOptional()
34
+ orderIndex?: number;
35
+
36
+ @IsString()
37
+ @IsOptional()
38
+ lessonType?: string;
39
+
40
+ @IsOptional()
41
+ lessonFlow?: any;
42
+ }
src/courses/dto/update-podcast.dto.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional } from 'class-validator';
2
+
3
+ export class UpdatePodcastDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ audioUrl?: string;
7
+
8
+ @IsOptional()
9
+ transcript?: any;
10
+ }
src/courses/dto/update-progress.dto.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional, IsBoolean } from 'class-validator';
2
+
3
+ export class UpdateProgressDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ userId?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ status?: string;
11
+
12
+ @IsOptional()
13
+ @IsBoolean()
14
+ lessonCompleted?: boolean;
15
+
16
+ @IsOptional()
17
+ @IsBoolean()
18
+ flashcardCompleted?: boolean;
19
+
20
+ @IsOptional()
21
+ @IsBoolean()
22
+ podcastCompleted?: boolean;
23
+
24
+ @IsOptional()
25
+ @IsBoolean()
26
+ quizCompleted?: boolean;
27
+ }
src/courses/dto/upload-doc.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class UploadDocDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ fileName: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ content: string;
11
+ }
src/courses/nodes.controller.ts ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, Post, Body, Param, Query, Patch, Put, Delete, UseGuards, Req } from '@nestjs/common';
2
+ import { CoursesService } from './courses.service';
3
+ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
+ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
+ import { RolesGuard } from '../auth/roles.guard';
6
+ import { Roles } from '../auth/roles.decorator';
7
+ import { CreateNodeDto } from './dto/create-node.dto';
8
+ import { UpdateNodeDto } from './dto/update-node.dto';
9
+ import { UpdateProgressDto } from './dto/update-progress.dto';
10
+ import { UpdateComponentProgressDto } from './dto/update-component-progress.dto';
11
+ import { CreateWarmupDto } from './dto/create-warmup.dto';
12
+ import { CreateCommentDto } from './dto/create-comment.dto';
13
+ import { CreateDocumentRefDto } from './dto/create-document-ref.dto';
14
+
15
+ @ApiTags('Courses & Roadmaps')
16
+ @Controller()
17
+ @UseGuards(JwtAuthGuard, RolesGuard)
18
+ @ApiBearerAuth()
19
+ export class NodesController {
20
+ constructor(private coursesService: CoursesService) {}
21
+
22
+ // ==================== CONCEPT NODES ====================
23
+
24
+ @Get('courses/:id/journey')
25
+ @ApiOperation({ summary: 'Get course journey roadmap nodes' })
26
+ async getCourseJourney(@Param('id') courseId: string, @Req() req: any) {
27
+ return this.coursesService.getCourseJourney(courseId, req.user.id);
28
+ }
29
+
30
+ @Get('courses/nodes/:nodeId')
31
+ @ApiOperation({ summary: 'Retrieve comprehensive learn detail for a concept node' })
32
+ async getNodeDetails(@Param('nodeId') nodeId: string, @Req() req: any) {
33
+ return this.coursesService.getNodeDetails(nodeId, req.user.id);
34
+ }
35
+
36
+ @Get('courses/nodes/:nodeId/core')
37
+ @ApiOperation({ summary: 'Retrieve core progress and type info for a concept node' })
38
+ async getNodeCore(@Param('nodeId') nodeId: string, @Req() req: any) {
39
+ return this.coursesService.getNodeCore(nodeId, req.user.id);
40
+ }
41
+
42
+ @Post('courses/nodes/:nodeId/complete')
43
+ @ApiOperation({ summary: 'Mark node as completed and auto-unlock next node' })
44
+ async completeNode(
45
+ @Param('nodeId') nodeId: string,
46
+ @Req() req: any,
47
+ ) {
48
+ return this.coursesService.completeNode(nodeId, req.user.id);
49
+ }
50
+
51
+ @Patch('courses/nodes/:nodeId/progress')
52
+ @ApiOperation({ summary: 'Update node learn progress status' })
53
+ async updateNodeProgress(
54
+ @Param('nodeId') nodeId: string,
55
+ @Body() dto: UpdateProgressDto,
56
+ @Req() req: any,
57
+ ) {
58
+ return this.coursesService.updateNodeProgress(
59
+ req.user.id,
60
+ nodeId,
61
+ dto.status,
62
+ dto.lessonCompleted,
63
+ dto.flashcardCompleted,
64
+ dto.podcastCompleted,
65
+ dto.quizCompleted,
66
+ );
67
+ }
68
+
69
+ @Patch('courses/nodes/:nodeId/component-progress')
70
+ @ApiOperation({ summary: 'Update component-based lesson progress' })
71
+ async updateComponentProgress(
72
+ @Param('nodeId') nodeId: string,
73
+ @Body() dto: UpdateComponentProgressDto,
74
+ @Req() req: any,
75
+ ) {
76
+ return this.coursesService.updateComponentProgress(
77
+ req.user.id,
78
+ nodeId,
79
+ dto.activeComponentId,
80
+ dto.currentComponentIndex,
81
+ dto.completedComponentIds,
82
+ dto.componentResult,
83
+ );
84
+ }
85
+
86
+ @Post('nodes')
87
+ @Roles('admin')
88
+ @ApiOperation({ summary: 'Create a new concept node (Admin)' })
89
+ async createNode(@Body() dto: CreateNodeDto) {
90
+ return this.coursesService.createNode(dto);
91
+ }
92
+
93
+ @Get('nodes')
94
+ @Roles('admin')
95
+ @ApiOperation({ summary: 'List all concept nodes (Admin)' })
96
+ async getNodes(@Query('chapterId') chapterId?: string) {
97
+ return this.coursesService.getNodes(chapterId);
98
+ }
99
+
100
+ @Put('nodes/:nodeId')
101
+ @Roles('admin')
102
+ @ApiOperation({ summary: 'Update a concept node (Admin)' })
103
+ async updateNode(@Param('nodeId') nodeId: string, @Body() dto: UpdateNodeDto) {
104
+ return this.coursesService.updateNode(nodeId, dto);
105
+ }
106
+
107
+ @Delete('nodes/:nodeId')
108
+ @Roles('admin')
109
+ @ApiOperation({ summary: 'Delete a concept node (Admin)' })
110
+ async deleteNode(@Param('nodeId') nodeId: string) {
111
+ return this.coursesService.deleteNode(nodeId);
112
+ }
113
+
114
+ // ==================== WARMUPS ====================
115
+
116
+ @Post('nodes/:nodeId/warmups')
117
+ @Roles('admin')
118
+ @ApiOperation({ summary: 'Create a new warmup for a concept node (Admin)' })
119
+ async createWarmup(@Param('nodeId') nodeId: string, @Body() dto: CreateWarmupDto) {
120
+ return this.coursesService.createWarmup(nodeId, dto);
121
+ }
122
+
123
+ @Get('nodes/:nodeId/warmups')
124
+ @ApiOperation({ summary: 'List all warmups for a concept node' })
125
+ async getWarmups(@Param('nodeId') nodeId: string) {
126
+ return this.coursesService.getWarmups(nodeId);
127
+ }
128
+
129
+ @Delete('warmups/:id')
130
+ @Roles('admin')
131
+ @ApiOperation({ summary: 'Delete a warmup by ID (Admin)' })
132
+ async deleteWarmup(@Param('id') id: string) {
133
+ return this.coursesService.deleteWarmup(id);
134
+ }
135
+
136
+ // ==================== DISCUSSIONS / COMMENTS ====================
137
+
138
+ @Post('courses/nodes/:nodeId/comments')
139
+ @ApiOperation({ summary: 'Post a comment on a concept node discussion' })
140
+ async createComment(
141
+ @Param('nodeId') nodeId: string,
142
+ @Body() dto: CreateCommentDto,
143
+ @Req() req: any,
144
+ ) {
145
+ const role = req.user.role === 'admin' ? 'admin' : 'student';
146
+ return this.coursesService.createComment(nodeId, req.user.id, dto.content, role);
147
+ }
148
+
149
+ @Get('courses/nodes/:nodeId/comments')
150
+ @ApiOperation({ summary: 'Get all comments for a concept node discussion' })
151
+ async getComments(@Param('nodeId') nodeId: string) {
152
+ return this.coursesService.getComments(nodeId);
153
+ }
154
+
155
+ // ==================== PDF REFERENCE DOCUMENTS CRUD ====================
156
+
157
+ @Post('documents')
158
+ @Roles('admin')
159
+ @ApiOperation({ summary: 'Save a PDF document reference' })
160
+ async createDocument(
161
+ @Body() dto: CreateDocumentRefDto,
162
+ ) {
163
+ return this.coursesService.createDocument(dto.courseId, dto.fileName, dto.fileUrl, dto.status, dto.title, dto.description);
164
+ }
165
+
166
+ @Get('documents')
167
+ @ApiOperation({ summary: 'List all reference PDF documents' })
168
+ async listDocuments(@Query('courseId') courseId?: string) {
169
+ return this.coursesService.listDocuments(courseId);
170
+ }
171
+
172
+ @Delete('documents/:id')
173
+ @Roles('admin')
174
+ @ApiOperation({ summary: 'Delete a PDF reference document by ID' })
175
+ async deleteDocument(@Param('id') id: string) {
176
+ return this.coursesService.deleteDocument(id);
177
+ }
178
+ }
src/courses/podcasts.controller.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, Post, Body, Param, Put, Delete, UseGuards } from '@nestjs/common';
2
+ import { CoursesService } from './courses.service';
3
+ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
+ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
+ import { RolesGuard } from '../auth/roles.guard';
6
+ import { Roles } from '../auth/roles.decorator';
7
+ import { CreatePodcastDto } from './dto/create-podcast.dto';
8
+ import { UpdatePodcastDto } from './dto/update-podcast.dto';
9
+ import { SynthesizePodcastDto } from './dto/synthesize-podcast.dto';
10
+ import { Throttle } from '@nestjs/throttler';
11
+
12
+ @ApiTags('Courses & Roadmaps')
13
+ @Controller('podcasts')
14
+ @UseGuards(JwtAuthGuard, RolesGuard)
15
+ @ApiBearerAuth()
16
+ export class PodcastsController {
17
+ constructor(private coursesService: CoursesService) {}
18
+
19
+ @Get()
20
+ @Roles('admin')
21
+ @ApiOperation({ summary: 'List all podcasts (Admin)' })
22
+ async getPodcasts() {
23
+ return this.coursesService.getPodcasts();
24
+ }
25
+
26
+ @Get(':id')
27
+ @Roles('admin')
28
+ @ApiOperation({ summary: 'Get single podcast details (Admin)' })
29
+ async getPodcastById(@Param('id') id: string) {
30
+ return this.coursesService.getPodcastById(id);
31
+ }
32
+
33
+ @Post()
34
+ @Roles('admin')
35
+ @ApiOperation({ summary: 'Create a new podcast manually (Admin)' })
36
+ async createPodcast(@Body() dto: CreatePodcastDto) {
37
+ return this.coursesService.createPodcast(dto);
38
+ }
39
+
40
+ @Put(':id')
41
+ @Roles('admin')
42
+ @ApiOperation({ summary: 'Update a podcast (Admin)' })
43
+ async updatePodcast(@Param('id') id: string, @Body() dto: UpdatePodcastDto) {
44
+ return this.coursesService.updatePodcast(id, dto);
45
+ }
46
+
47
+ @Delete(':id')
48
+ @Roles('admin')
49
+ @ApiOperation({ summary: 'Delete a podcast (Admin)' })
50
+ async deletePodcast(@Param('id') id: string) {
51
+ return this.coursesService.deletePodcast(id);
52
+ }
53
+
54
+ @Post('synthesize')
55
+ @Throttle({ default: { limit: 5, ttl: 60000 } })
56
+ @Roles('admin')
57
+ @ApiOperation({ summary: 'Synthesize script text via TTS and return public audio URL for preview' })
58
+ async synthesizePodcast(@Body() dto: SynthesizePodcastDto) {
59
+ return this.coursesService.synthesizePodcast(dto.nodeId, dto.scriptText);
60
+ }
61
+ }
src/courses/validators/node-schema.validator.ts ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BadRequestException } from '@nestjs/common';
2
+
3
+ const SUPPORTED_COMPONENT_TYPES = new Set([
4
+ 'dialogue',
5
+ 'media',
6
+ 'markdown',
7
+ 'target_matching',
8
+ 'category_sorting',
9
+ 'mindmap_reveal',
10
+ 'mcq',
11
+ 'matching_columns',
12
+ 'true_false',
13
+ 'sequence_sorting',
14
+ 'final_summary',
15
+ ]);
16
+
17
+ function requireArray(value: any, label: string) {
18
+ if (!Array.isArray(value)) {
19
+ throw new BadRequestException(`${label} must be an array`);
20
+ }
21
+ }
22
+
23
+ function requireString(value: any, label: string) {
24
+ if (typeof value !== 'string' || value.trim() === '') {
25
+ throw new BadRequestException(`${label} must be a non-empty string`);
26
+ }
27
+ }
28
+
29
+ function requireObject(value: any, label: string) {
30
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
31
+ throw new BadRequestException(`${label} must be a JSON object`);
32
+ }
33
+ }
34
+
35
+ export class NodeSchemaValidator {
36
+ static validateLessonFlow(data: any) {
37
+ requireArray(data, 'lessonFlow');
38
+
39
+ const ids = new Set<string>();
40
+ data.forEach((component: any, idx: number) => {
41
+ requireObject(component, `lessonFlow[${idx}]`);
42
+ requireString(component.id, `lessonFlow[${idx}].id`);
43
+ requireString(component.type, `lessonFlow[${idx}].type`);
44
+
45
+ if (ids.has(component.id)) {
46
+ throw new BadRequestException(`lessonFlow[${idx}].id duplicates "${component.id}"`);
47
+ }
48
+ ids.add(component.id);
49
+
50
+ if (!SUPPORTED_COMPONENT_TYPES.has(component.type)) {
51
+ throw new BadRequestException(`lessonFlow[${idx}].type "${component.type}" is not supported`);
52
+ }
53
+
54
+ requireObject(component.config, `lessonFlow[${idx}].config`);
55
+ this.validateComponentConfig(component, idx);
56
+
57
+ if (component.completionRule) {
58
+ requireObject(component.completionRule, `lessonFlow[${idx}].completionRule`);
59
+ requireString(component.completionRule.type, `lessonFlow[${idx}].completionRule.type`);
60
+ }
61
+ });
62
+ }
63
+
64
+ private static validateComponentConfig(component: any, idx: number) {
65
+ const config = component.config;
66
+ const label = `lessonFlow[${idx}].config`;
67
+
68
+ if (component.type === 'dialogue') {
69
+ requireArray(config.lines, `${label}.lines`);
70
+ config.lines.forEach((line: any, lineIdx: number) => {
71
+ requireString(line.text, `${label}.lines[${lineIdx}].text`);
72
+ });
73
+ }
74
+
75
+ if (component.type === 'media') {
76
+ requireString(config.url, `${label}.url`);
77
+ }
78
+
79
+ if (component.type === 'markdown') {
80
+ requireString(config.content, `${label}.content`);
81
+ }
82
+
83
+ if (component.type === 'target_matching') {
84
+ requireArray(config.targets, `${label}.targets`);
85
+ requireArray(config.items, `${label}.items`);
86
+ }
87
+
88
+ if (component.type === 'category_sorting') {
89
+ requireArray(config.categories, `${label}.categories`);
90
+ requireArray(config.cards, `${label}.cards`);
91
+ }
92
+
93
+ if (component.type === 'mindmap_reveal') {
94
+ requireArray(config.nodes, `${label}.nodes`);
95
+ }
96
+
97
+ if (component.type === 'mcq') {
98
+ requireString(config.question, `${label}.question`);
99
+ requireArray(config.options, `${label}.options`);
100
+ if (!config.options.some((option: any) => option.isCorrect === true || option.correct === true)) {
101
+ throw new BadRequestException(`${label}.options must contain at least one correct option`);
102
+ }
103
+ }
104
+
105
+ if (component.type === 'matching_columns') {
106
+ requireArray(config.leftColumn, `${label}.leftColumn`);
107
+ requireArray(config.rightColumn, `${label}.rightColumn`);
108
+ requireArray(config.correctPairs, `${label}.correctPairs`);
109
+ }
110
+
111
+ if (component.type === 'true_false') {
112
+ requireString(config.statement, `${label}.statement`);
113
+ if (typeof config.correctAnswer !== 'boolean') {
114
+ throw new BadRequestException(`${label}.correctAnswer must be a boolean`);
115
+ }
116
+ }
117
+
118
+ if (component.type === 'sequence_sorting') {
119
+ requireArray(config.items, `${label}.items`);
120
+ }
121
+
122
+ if (component.type === 'final_summary') {
123
+ if (config.keyTakeaways !== undefined) {
124
+ requireArray(config.keyTakeaways, `${label}.keyTakeaways`);
125
+ }
126
+ }
127
+ }
128
+
129
+ static validateNode(lessonFlow: any) {
130
+ this.validateLessonFlow(lessonFlow);
131
+ }
132
+ }
src/debate/debates.controller.ts CHANGED
@@ -1,34 +1,12 @@
1
  import { Controller, Get, Post, Body, Param, Query, Delete, Put, UseGuards, Req } from '@nestjs/common';
2
  import { DebatesService } from './debates.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
-
9
- class DebateMessageDto {
10
- @IsString()
11
- @IsOptional()
12
- userId: string;
13
-
14
- @IsString()
15
- @IsNotEmpty()
16
- message: string;
17
- }
18
-
19
- class CreateDebateTopicDto {
20
- @IsString()
21
- @IsNotEmpty()
22
- title: string;
23
-
24
- @IsString()
25
- @IsNotEmpty()
26
- description: string;
27
-
28
- @IsString()
29
- @IsNotEmpty()
30
- initialPrompt: string;
31
- }
32
 
33
  @ApiTags('Socratic AI Debate')
34
  @Controller('debates')
@@ -75,6 +53,7 @@ export class DebatesController {
75
  }
76
 
77
  @Post('topic/:topicId/message')
 
78
  @ApiOperation({ summary: 'Post a user argument to a topic debate session and get rebuttal' })
79
  async sendTopicDebateMessage(
80
  @Param('topicId') topicId: string,
@@ -100,6 +79,7 @@ export class DebatesController {
100
  }
101
 
102
  @Post(':nodeId/message')
 
103
  @ApiOperation({ summary: 'Post a user argument and get a Socratic rebuttal' })
104
  async sendDebateMessage(
105
  @Param('nodeId') nodeId: string,
 
1
  import { Controller, Get, Post, Body, Param, Query, Delete, Put, UseGuards, Req } from '@nestjs/common';
2
  import { DebatesService } from './debates.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
  import { RolesGuard } from '../auth/roles.guard';
6
  import { Roles } from '../auth/roles.decorator';
7
+ import { DebateMessageDto } from './dto/debate-message.dto';
8
+ import { CreateDebateTopicDto } from './dto/create-debate-topic.dto';
9
+ import { Throttle } from '@nestjs/throttler';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  @ApiTags('Socratic AI Debate')
12
  @Controller('debates')
 
53
  }
54
 
55
  @Post('topic/:topicId/message')
56
+ @Throttle({ default: { limit: 5, ttl: 60000 } })
57
  @ApiOperation({ summary: 'Post a user argument to a topic debate session and get rebuttal' })
58
  async sendTopicDebateMessage(
59
  @Param('topicId') topicId: string,
 
79
  }
80
 
81
  @Post(':nodeId/message')
82
+ @Throttle({ default: { limit: 5, ttl: 60000 } })
83
  @ApiOperation({ summary: 'Post a user argument and get a Socratic rebuttal' })
84
  async sendDebateMessage(
85
  @Param('nodeId') nodeId: string,
src/debate/dto/create-debate-topic.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class CreateDebateTopicDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ title: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ description: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ initialPrompt: string;
15
+ }
src/debate/dto/debate-message.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class DebateMessageDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ userId?: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ message: string;
11
+ }
src/flashcards/dto/create-flashcard.dto.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class CreateFlashcardDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ nodeId: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ tag: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ question: string;
15
+
16
+ @IsString()
17
+ @IsNotEmpty()
18
+ answer: string;
19
+ }
src/flashcards/dto/review-card.dto.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsNumber, Min, Max, IsOptional } from 'class-validator';
2
+
3
+ export class ReviewCardDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ userId?: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ flashcardId: string;
11
+
12
+ @IsNumber()
13
+ @Min(1)
14
+ @Max(4)
15
+ ease: number;
16
+ }
src/flashcards/dto/update-flashcard.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional } from 'class-validator';
2
+
3
+ export class UpdateFlashcardDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ tag?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ question?: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ answer?: string;
15
+ }
src/flashcards/flashcards.controller.ts CHANGED
@@ -1,57 +1,12 @@
1
  import { Controller, Get, Post, Body, Query, Put, Delete, Param, UseGuards, Req } from '@nestjs/common';
2
  import { FlashcardsService } from './flashcards.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsNumber, Min, Max, IsOptional } from 'class-validator';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
-
9
- class ReviewCardDto {
10
- @IsString()
11
- @IsOptional()
12
- userId: string;
13
-
14
- @IsString()
15
- @IsNotEmpty()
16
- flashcardId: string;
17
-
18
- @IsNumber()
19
- @Min(1)
20
- @Max(4)
21
- ease: number; // 1 | 2 | 3 | 4
22
- }
23
-
24
- class CreateFlashcardDto {
25
- @IsString()
26
- @IsNotEmpty()
27
- nodeId: string;
28
-
29
- @IsString()
30
- @IsNotEmpty()
31
- tag: string;
32
-
33
- @IsString()
34
- @IsNotEmpty()
35
- question: string;
36
-
37
- @IsString()
38
- @IsNotEmpty()
39
- answer: string;
40
- }
41
-
42
- class UpdateFlashcardDto {
43
- @IsString()
44
- @IsOptional()
45
- tag?: string;
46
-
47
- @IsString()
48
- @IsOptional()
49
- question?: string;
50
-
51
- @IsString()
52
- @IsOptional()
53
- answer?: string;
54
- }
55
 
56
  @ApiTags('Spaced Repetition Flashcards')
57
  @Controller('flashcards')
 
1
  import { Controller, Get, Post, Body, Query, Put, Delete, Param, UseGuards, Req } from '@nestjs/common';
2
  import { FlashcardsService } from './flashcards.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
  import { RolesGuard } from '../auth/roles.guard';
6
  import { Roles } from '../auth/roles.decorator';
7
+ import { ReviewCardDto } from './dto/review-card.dto';
8
+ import { CreateFlashcardDto } from './dto/create-flashcard.dto';
9
+ import { UpdateFlashcardDto } from './dto/update-flashcard.dto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  @ApiTags('Spaced Repetition Flashcards')
12
  @Controller('flashcards')
src/philosofun/dto/create-philosofun.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreatePhilosofunDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ title: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ description?: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ videoUrl: string;
15
+ }
src/philosofun/dto/update-philosofun.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional } from 'class-validator';
2
+
3
+ export class UpdatePhilosofunDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ title?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ description?: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ videoUrl?: string;
15
+ }
src/philosofun/philosofun.controller.ts CHANGED
@@ -1,38 +1,11 @@
1
  import { Controller, Get, Post, Body, Param, Put, Delete, UseGuards } from '@nestjs/common';
2
  import { PhilosofunService } from './philosofun.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
-
9
- class CreatePhilosofunDto {
10
- @IsString()
11
- @IsNotEmpty()
12
- title: string;
13
-
14
- @IsString()
15
- @IsOptional()
16
- description?: string;
17
-
18
- @IsString()
19
- @IsNotEmpty()
20
- videoUrl: string;
21
- }
22
-
23
- class UpdatePhilosofunDto {
24
- @IsString()
25
- @IsOptional()
26
- title?: string;
27
-
28
- @IsString()
29
- @IsOptional()
30
- description?: string;
31
-
32
- @IsString()
33
- @IsOptional()
34
- videoUrl?: string;
35
- }
36
 
37
  @ApiTags('Philosofun')
38
  @Controller('philosofun')
 
1
  import { Controller, Get, Post, Body, Param, Put, Delete, UseGuards } from '@nestjs/common';
2
  import { PhilosofunService } from './philosofun.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
  import { RolesGuard } from '../auth/roles.guard';
6
  import { Roles } from '../auth/roles.decorator';
7
+ import { CreatePhilosofunDto } from './dto/create-philosofun.dto';
8
+ import { UpdatePhilosofunDto } from './dto/update-philosofun.dto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  @ApiTags('Philosofun')
11
  @Controller('philosofun')
src/quizzes/dto/create-quiz.dto.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateQuizDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ nodeId?: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ type: string;
11
+
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ title: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ description?: string;
19
+
20
+ @IsNotEmpty()
21
+ questions: any;
22
+ }
src/quizzes/dto/update-quiz.dto.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional } from 'class-validator';
2
+
3
+ export class UpdateQuizDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ nodeId?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ type?: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ title?: string;
15
+
16
+ @IsString()
17
+ @IsOptional()
18
+ description?: string;
19
+
20
+ @IsOptional()
21
+ questions?: any;
22
+ }
src/quizzes/quizzes.controller.ts CHANGED
@@ -1,52 +1,11 @@
1
  import { Controller, Get, Post, Body, Param, Query, Put, Delete, UseGuards } from '@nestjs/common';
2
  import { QuizzesService } from './quizzes.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
-
9
- class CreateQuizDto {
10
- @IsString()
11
- @IsOptional()
12
- nodeId?: string;
13
-
14
- @IsString()
15
- @IsNotEmpty()
16
- type: string; // "mcq" | "matching" | "essay" | "image" | "analysis"
17
-
18
- @IsString()
19
- @IsNotEmpty()
20
- title: string;
21
-
22
- @IsString()
23
- @IsOptional()
24
- description?: string;
25
-
26
- @IsNotEmpty()
27
- questions: any;
28
- }
29
-
30
- class UpdateQuizDto {
31
- @IsString()
32
- @IsOptional()
33
- nodeId?: string;
34
-
35
- @IsString()
36
- @IsOptional()
37
- type?: string;
38
-
39
- @IsString()
40
- @IsOptional()
41
- title?: string;
42
-
43
- @IsString()
44
- @IsOptional()
45
- description?: string;
46
-
47
- @IsOptional()
48
- questions?: any;
49
- }
50
 
51
  @ApiTags('Quizzes & Interactive Systems')
52
  @Controller('quizzes')
 
1
  import { Controller, Get, Post, Body, Param, Query, Put, Delete, UseGuards } from '@nestjs/common';
2
  import { QuizzesService } from './quizzes.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
  import { RolesGuard } from '../auth/roles.guard';
6
  import { Roles } from '../auth/roles.decorator';
7
+ import { CreateQuizDto } from './dto/create-quiz.dto';
8
+ import { UpdateQuizDto } from './dto/update-quiz.dto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  @ApiTags('Quizzes & Interactive Systems')
11
  @Controller('quizzes')
src/users/dto/create-feedback.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2
+
3
+ export class CreateFeedbackDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ userId?: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ content: string;
11
+ }
src/users/dto/google-login.dto.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class GoogleLoginDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ idToken: string;
7
+ }
src/users/dto/login.dto.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional, IsEmail } from 'class-validator';
2
+
3
+ export class LoginDto {
4
+ @IsEmail()
5
+ @IsNotEmpty()
6
+ email: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ password?: string;
11
+ }
src/users/dto/register.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty, IsOptional, IsEmail } from 'class-validator';
2
+
3
+ export class RegisterDto {
4
+ @IsEmail()
5
+ @IsNotEmpty()
6
+ email: string;
7
+
8
+ @IsString()
9
+ @IsNotEmpty()
10
+ name: string;
11
+
12
+ @IsString()
13
+ @IsOptional()
14
+ password?: string;
15
+ }
src/users/dto/supabase-login.dto.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { IsString, IsNotEmpty } from 'class-validator';
2
+
3
+ export class SupabaseLoginDto {
4
+ @IsString()
5
+ @IsNotEmpty()
6
+ token: string;
7
+ }
src/users/dto/update-user.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString, IsOptional, IsNumber } from 'class-validator';
2
+
3
+ export class UpdateUserDto {
4
+ @IsString()
5
+ @IsOptional()
6
+ name?: string;
7
+
8
+ @IsString()
9
+ @IsOptional()
10
+ email?: string;
11
+
12
+ @IsNumber()
13
+ @IsOptional()
14
+ streak?: number;
15
+ }
src/users/users.controller.ts CHANGED
@@ -1,70 +1,15 @@
1
  import { Controller, Get, Post, Body, Param, Query, Put, Delete, HttpCode, UseGuards, Req, ForbiddenException } from '@nestjs/common';
2
  import { UsersService } from './users.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
4
- import { IsString, IsNotEmpty, IsOptional, IsEmail, IsNumber } from 'class-validator';
5
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
6
  import { RolesGuard } from '../auth/roles.guard';
7
  import { Roles } from '../auth/roles.decorator';
8
-
9
- class RegisterDto {
10
- @IsEmail()
11
- @IsNotEmpty()
12
- email: string;
13
-
14
- @IsString()
15
- @IsNotEmpty()
16
- name: string;
17
-
18
- @IsString()
19
- @IsOptional()
20
- password?: string;
21
- }
22
-
23
- class LoginDto {
24
- @IsEmail()
25
- @IsNotEmpty()
26
- email: string;
27
-
28
- @IsString()
29
- @IsOptional()
30
- password?: string;
31
- }
32
-
33
- class GoogleLoginDto {
34
- @IsString()
35
- @IsNotEmpty()
36
- idToken: string;
37
- }
38
-
39
- class SupabaseLoginDto {
40
- @IsString()
41
- @IsNotEmpty()
42
- token: string;
43
- }
44
-
45
- class UpdateUserDto {
46
- @IsString()
47
- @IsOptional()
48
- name?: string;
49
-
50
- @IsString()
51
- @IsOptional()
52
- email?: string;
53
-
54
- @IsNumber()
55
- @IsOptional()
56
- streak?: number;
57
- }
58
-
59
- class CreateFeedbackDto {
60
- @IsString()
61
- @IsOptional()
62
- userId: string;
63
-
64
- @IsString()
65
- @IsNotEmpty()
66
- content: string;
67
- }
68
 
69
  @ApiTags('Authentication & User Management')
70
  @Controller()
 
1
  import { Controller, Get, Post, Body, Param, Query, Put, Delete, HttpCode, UseGuards, Req, ForbiddenException } from '@nestjs/common';
2
  import { UsersService } from './users.service';
3
  import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
 
4
  import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
  import { RolesGuard } from '../auth/roles.guard';
6
  import { Roles } from '../auth/roles.decorator';
7
+ import { RegisterDto } from './dto/register.dto';
8
+ import { LoginDto } from './dto/login.dto';
9
+ import { GoogleLoginDto } from './dto/google-login.dto';
10
+ import { SupabaseLoginDto } from './dto/supabase-login.dto';
11
+ import { UpdateUserDto } from './dto/update-user.dto';
12
+ import { CreateFeedbackDto } from './dto/create-feedback.dto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  @ApiTags('Authentication & User Management')
15
  @Controller()