@learnpack/learnpack 5.0.275 → 5.0.277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +55 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/lib/utils/export/epub.d.ts +2 -0
  15. package/lib/utils/export/epub.js +298 -0
  16. package/lib/utils/export/index.d.ts +3 -0
  17. package/lib/utils/export/index.js +7 -0
  18. package/lib/utils/export/scorm.d.ts +2 -0
  19. package/lib/utils/export/scorm.js +84 -0
  20. package/lib/utils/export/shared.d.ts +4 -0
  21. package/lib/utils/export/shared.js +61 -0
  22. package/lib/utils/export/types.d.ts +15 -0
  23. package/lib/utils/export/types.js +2 -0
  24. package/package.json +2 -1
  25. package/src/commands/audit.ts +487 -487
  26. package/src/commands/breakToken.ts +67 -67
  27. package/src/commands/clean.ts +30 -30
  28. package/src/commands/init.ts +650 -650
  29. package/src/commands/logout.ts +38 -38
  30. package/src/commands/publish.ts +20 -25
  31. package/src/commands/serve.ts +69 -4
  32. package/src/commands/start.ts +333 -333
  33. package/src/commands/translate.ts +123 -123
  34. package/src/creator/README.md +54 -54
  35. package/src/creator/eslint.config.js +7 -7
  36. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  37. package/src/creator/src/i18n.ts +28 -28
  38. package/src/creator/src/index.css +217 -217
  39. package/src/creator/src/locales/en.json +126 -126
  40. package/src/creator/src/locales/es.json +126 -126
  41. package/src/creator/src/utils/configTypes.ts +122 -122
  42. package/src/creator/src/utils/constants.ts +13 -13
  43. package/src/creator/src/utils/creatorUtils.ts +46 -46
  44. package/src/creator/src/utils/eventBus.ts +2 -2
  45. package/src/creator/src/utils/lib.ts +468 -468
  46. package/src/creator/src/utils/socket.ts +61 -61
  47. package/src/creator/src/utils/store.ts +222 -222
  48. package/src/creator/src/vite-env.d.ts +1 -1
  49. package/src/creator/vite.config.ts +13 -13
  50. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  51. package/src/managers/config/defaults.ts +49 -49
  52. package/src/managers/config/exercise.ts +364 -364
  53. package/src/managers/config/index.ts +775 -775
  54. package/src/managers/file.ts +236 -236
  55. package/src/managers/server/routes.ts +554 -554
  56. package/src/managers/session.ts +182 -182
  57. package/src/managers/telemetry.ts +188 -188
  58. package/src/models/action.ts +13 -13
  59. package/src/models/config-manager.ts +28 -28
  60. package/src/models/config.ts +106 -106
  61. package/src/models/creator.ts +47 -47
  62. package/src/models/exercise-obj.ts +30 -30
  63. package/src/models/session.ts +39 -39
  64. package/src/models/socket.ts +61 -61
  65. package/src/models/status.ts +16 -16
  66. package/src/ui/_app/app.css +1 -1
  67. package/src/ui/_app/app.js +400 -397
  68. package/src/ui/app.tar.gz +0 -0
  69. package/src/utils/BaseCommand.ts +56 -56
  70. package/src/utils/api.ts +53 -39
  71. package/src/utils/audit.ts +392 -392
  72. package/src/utils/checkNotInstalled.ts +267 -267
  73. package/src/utils/configBuilder.ts +82 -82
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -0
  77. package/src/utils/export/epub.ts +400 -0
  78. package/src/utils/export/index.ts +3 -0
  79. package/src/utils/export/scorm.ts +121 -0
  80. package/src/utils/export/shared.ts +61 -0
  81. package/src/utils/export/types.ts +17 -0
  82. package/src/utils/incrementVersion.js +74 -74
  83. package/src/utils/misc.ts +58 -58
  84. package/src/utils/rigoActions.ts +500 -500
  85. package/src/utils/sidebarGenerator.ts +195 -195
  86. package/src/utils/templates/epub/epub.css +133 -0
  87. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  88. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  89. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -0
  90. package/src/utils/templates/scorm/config/api.js +175 -0
  91. package/src/utils/templates/scorm/config/index.html +210 -0
  92. package/src/utils/templates/scorm/ims_xml.xsd +1 -0
  93. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -0
  94. package/src/utils/templates/scorm/imsmanifest.xml +38 -0
  95. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -0
@@ -1,554 +1,554 @@
1
- import Console from "../../utils/console"
2
- import * as express from "express"
3
- import * as fs from "fs"
4
- import * as bodyParser from "body-parser"
5
- import socket from "../socket"
6
- import queue from "../../utils/fileQueue"
7
- // import gitpod from '../gitpod'
8
- import { detect, filterFiles } from "../config/exercise"
9
- import { IFile } from "../../models/file"
10
- import { IConfigObj, TEntries } from "../../models/config"
11
- import { IConfigManager } from "../../models/config-manager"
12
- import { IExercise } from "../../models/exercise-obj"
13
- import SessionManager from "../../managers/session"
14
- import TelemetryManager from "../telemetry"
15
- import { saveTranslatedReadme } from "../../utils/creatorUtilities"
16
- import { translateExercise } from "../../utils/rigoActions"
17
- import { addExerciseToSidebar } from "../../utils/sidebarGenerator"
18
- import path = require("path")
19
- // import { eventManager } from "../../utils/osOperations"
20
-
21
- const withHandler =
22
- (func: (req: express.Request, res: express.Response) => void) =>
23
- (req: express.Request, res: express.Response) => {
24
- try {
25
- func(req, res)
26
- } catch (error) {
27
- Console.debug(error)
28
- const _err = {
29
- message: (error as TypeError).message || "There has been an error",
30
- status: (error as any).status || 500,
31
- type: (error as any).type || null,
32
- }
33
- Console.error(_err.message)
34
-
35
- // send rep to the server
36
- res.status(_err.status)
37
- res.json(_err)
38
- }
39
- }
40
-
41
- export default async function (
42
- app: express.Application,
43
- configObject: IConfigObj,
44
- configManager: IConfigManager
45
- ) {
46
- const { config, exercises } = configObject
47
- const session = await SessionManager.get(configManager?.get())
48
-
49
- const dispatcher = queue.dispatcher({
50
- create: true,
51
- path: `${config?.dirPath}/vscode_queue.json`,
52
- })
53
-
54
- app.get(
55
- "/config",
56
- withHandler((_: express.Request, res: express.Response) => {
57
- const confObject = configManager.get()
58
- res.json(confObject)
59
- })
60
- )
61
-
62
- // Added this line to parse the json body
63
-
64
- const jsonBodyParser = bodyParser.json({ limit: "10mb" })
65
- // Trying to log in from frontend
66
- app.post(
67
- "/login",
68
- jsonBodyParser,
69
- withHandler(async (req: express.Request, res: express.Response) => {
70
- const email = req.body.email
71
- const password = req.body.password
72
-
73
- SessionManager.destroy()
74
- const payload = await SessionManager.loginWeb(email, password)
75
-
76
- res.json(payload)
77
- })
78
- )
79
-
80
- app.post(
81
- "/logout",
82
- jsonBodyParser,
83
- withHandler(async (req: express.Request, res: express.Response) => {
84
- SessionManager.destroy()
85
- res.json({ message: "You've logged out from Breathecode and Rigobot" })
86
- })
87
- )
88
-
89
- app.post(
90
- "/set-rigobot-token",
91
- jsonBodyParser,
92
- withHandler(async (req: express.Request, res: express.Response) => {
93
- const token = req.body.token
94
- // Ensure token is provided in the request body
95
- if (!token) {
96
- return res.status(400).json({ error: "Token is required" })
97
- }
98
-
99
- try {
100
- const tokenSaved = await SessionManager.setRigoToken(token)
101
- // Check if the token was saved successfully
102
- if (tokenSaved) {
103
- res.json({ status: "ok" })
104
- } else {
105
- res.status(500).json({ error: "Failed to save the token" })
106
- }
107
- } catch {
108
- // Handle any unexpected errors during the process
109
- res.status(500).json({ error: "Internal server error" })
110
- }
111
- })
112
- )
113
- app.post(
114
- "/set-tab-hash",
115
- jsonBodyParser,
116
- withHandler(async (req: express.Request, res: express.Response) => {
117
- const hash = req.body.hash
118
- if (!hash) {
119
- return res.status(400).json({ error: "Token is required" })
120
- }
121
-
122
- try {
123
- const hashSaved = await SessionManager.setTabHash(hash)
124
- if (hashSaved) {
125
- res.json({ status: "ok" })
126
- } else {
127
- res.status(500).json({ error: "Failed to save the token" })
128
- }
129
- } catch {
130
- res.status(500).json({ error: "Internal server error" })
131
- }
132
- })
133
- )
134
- app.post(
135
- "/set-session-key",
136
- jsonBodyParser,
137
- withHandler(async (req: express.Request, res: express.Response) => {
138
- const sessionKey = req.body.sessionKey
139
- if (!sessionKey) {
140
- return res.status(400).json({ error: "Token is required" })
141
- }
142
-
143
- try {
144
- const sessionSaved = await SessionManager.setSessionKey(sessionKey)
145
- if (sessionSaved) {
146
- res.json({ status: "ok" })
147
- } else {
148
- res.status(500).json({ error: "Failed to save the token" })
149
- }
150
- } catch {
151
- res.status(500).json({ error: "Internal server error" })
152
- }
153
- })
154
- )
155
- app.post(
156
- "/set-session",
157
- jsonBodyParser,
158
- withHandler(async (req: express.Request, res: express.Response) => {
159
- const payload = req.body.payload
160
- if (!payload) {
161
- return res.status(400).json({ error: "Token is required" })
162
- }
163
-
164
- try {
165
- const payloadSaved = await SessionManager.setPayload(payload)
166
- if (payloadSaved) {
167
- res.json({ status: "ok" })
168
- socket.sessionRefreshed(payload)
169
- } else {
170
- res.status(500).json({ error: "Failed to save the token" })
171
- }
172
- } catch {
173
- res.status(500).json({ error: "Internal server error" })
174
- }
175
- })
176
- )
177
-
178
- app.get("/telemetry", async (req, res) => {
179
- const telemetry = await TelemetryManager.retrieve()
180
- res.json(telemetry)
181
- })
182
-
183
- app.post("/telemetry", jsonBodyParser, async (req, res) => {
184
- const telemetry = req.body
185
- await TelemetryManager.save(telemetry)
186
- res.json({ message: "Telemetry saved successfully" })
187
- })
188
-
189
- app.get(
190
- "/check/rigo/status",
191
- withHandler(async (_: express.Request, res: express.Response) => {
192
- const payload = await SessionManager.getPayload()
193
-
194
- if (payload && payload.rigobot && payload.rigobot.key) {
195
- res.json({ rigoToken: payload.rigobot.key, payload: payload })
196
- } else {
197
- res
198
- .status(400)
199
- .json({ details: `Rigobot token not found, please log in first!` })
200
- }
201
- })
202
- )
203
-
204
- // symbolic link to maintain path compatiblity
205
- const fetchStaticAsset = withHandler((req, res) => {
206
- const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
207
- if (!fs.existsSync(filePath)) throw new Error("File not found: " + filePath)
208
- const content = fs.readFileSync(filePath)
209
- res.write(content)
210
- res.end()
211
- })
212
-
213
- app.get(
214
- `${
215
- config?.dirPath.indexOf("./") === 0 ?
216
- config.dirPath.slice(1) :
217
- config?.dirPath
218
- }/assets/:filePath`,
219
- fetchStaticAsset
220
- )
221
-
222
- app.get("/assets/:filePath", fetchStaticAsset)
223
-
224
- app.get(
225
- "/exercise",
226
- withHandler((_: express.Request, res: express.Response) => {
227
- res.json(exercises)
228
- })
229
- )
230
-
231
- app.get(
232
- "/exercise/:slug/readme",
233
- withHandler(
234
- (
235
- { params: { slug }, query: { lang } }: express.Request,
236
- res: express.Response
237
- ) => {
238
- const exercise: IExercise = configManager.getExercise(slug)
239
-
240
- if (exercise && exercise.getReadme) {
241
- const readme = exercise.getReadme((lang as string) || null)
242
- res.json(readme)
243
- } else {
244
- res.status(400)
245
- }
246
- }
247
- )
248
- )
249
-
250
- app.get(
251
- "/exercise/:slug/report",
252
- withHandler(
253
- ({ params: { slug } }: express.Request, res: express.Response) => {
254
- const exercise = configManager.getExercise(slug)
255
- if (exercise && exercise.getTestReport) {
256
- const report = exercise.getTestReport()
257
- res.json(JSON.stringify(report))
258
- }
259
- }
260
- )
261
- )
262
-
263
- app.get(
264
- "/exercise/:slug",
265
- withHandler((req: express.Request, res: express.Response) => {
266
- // no need to re-start exercise if it's already started
267
- if (
268
- configObject.currentExercise &&
269
- req.params.slug === configObject.currentExercise
270
- ) {
271
- const exercise = configManager.getExercise(req.params.slug)
272
- res.json(exercise)
273
- if (exercise.position) {
274
- TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
275
- }
276
-
277
- return
278
- }
279
-
280
- const exercise = configManager.startExercise(req.params.slug)
281
- if (exercise.position) {
282
- TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
283
- }
284
-
285
- if (configObject.config?.editor.agent !== "os") {
286
- dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
287
- }
288
-
289
- type TEntry = "python3" | "html" | "node" | "react" | "java"
290
-
291
- const entries = new Set(
292
- Object.keys(config?.entries!).map(
293
- lang => config?.entries[lang as TEntry]
294
- )
295
- )
296
-
297
- // if we are in incremental grading, the entry file can by dinamically detected
298
- // based on the changes the student is making during the exercise
299
- if (config?.grading === "incremental") {
300
- const scanedFiles = fs.readdirSync("./")
301
-
302
- // update the file hierarchy with updates
303
- exercise.files = [
304
- ...exercise.files.filter(f => f.name.includes("test.")),
305
- ...filterFiles(scanedFiles),
306
- ]
307
- Console.debug(`Exercise updated files: `, exercise.files)
308
- }
309
-
310
- const detected = detect(
311
- configObject,
312
- exercise.files
313
- .filter(fileName => entries.has(fileName.name))
314
- .map(f => f.name || f) as string[]
315
- )
316
-
317
- // if a new language for the testing engine is detected, we replace it
318
- // if not we leave it as it was before
319
- if (config?.language && !["", "auto"].includes(config?.language)) {
320
- Console.debug(
321
- `Exercise language ignored, instead imported from configuration ${config?.language}`
322
- )
323
- exercise.language = detected?.language
324
- } else if (
325
- detected?.language &&
326
- (!config?.language || config?.language === "auto")
327
- ) {
328
- Console.debug(
329
- `Switching to ${detected.language} engine in this exercise`
330
- )
331
- exercise.language = detected.language
332
- }
333
-
334
- // WARNING: has to be the FULL PATH to the entry path
335
- // We need to detect entry in both gradings: Incremental and Isolate
336
- exercise.entry = detected?.entry
337
- Console.debug(
338
- `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
339
- )
340
-
341
- // exercises.graded and exercises.disableGrading deprecated.
342
- if (
343
- !exercise.graded ||
344
- config?.disableGrading ||
345
- config?.disabledActions?.includes("test")
346
- ) {
347
- socket.removeAllowed("test")
348
- } else {
349
- socket.addAllowed("test")
350
- }
351
-
352
- if (!exercise.entry || config?.disabledActions?.includes("build")) {
353
- socket.removeAllowed("build")
354
- } else {
355
- socket.addAllowed("build")
356
- }
357
-
358
- if (
359
- exercise.files.filter(
360
- (f: IFile) =>
361
- !f.name.toLowerCase().includes("readme.") &&
362
- !f.name.toLowerCase().includes("test.")
363
- ).length === 0 ||
364
- config?.disabledActions?.includes("reset")
365
- ) {
366
- socket.removeAllowed("reset")
367
- } else if (!config?.disabledActions?.includes("reset")) {
368
- socket.addAllowed("reset")
369
- }
370
-
371
- socket.log("ready")
372
- res.json(exercise)
373
- })
374
- )
375
-
376
- app.get(
377
- "/exercise/:slug/file/:fileName",
378
- withHandler((req: express.Request, res: express.Response) => {
379
- const exercise = configManager.getExercise(req.params.slug)
380
- if (exercise && exercise.getFile) {
381
- res.write(exercise.getFile(req.params.fileName))
382
- res.end()
383
- }
384
- })
385
- )
386
-
387
- app.get(
388
- "/sidebar",
389
- withHandler((req: express.Request, res: express.Response) => {
390
- const sidebar = configManager.getSidebar()
391
- res.json(sidebar)
392
- })
393
- )
394
-
395
- app.delete(
396
- "/exercise/:slug/delete",
397
- withHandler(async (req: express.Request, res: express.Response) => {
398
- const exerciseDeleted = configManager.deleteExercise(req.params.slug)
399
- if (exerciseDeleted) {
400
- configManager.buildIndex()
401
- res.json({ status: "ok" })
402
- } else {
403
- res.status(500).json({ error: "Failed to delete exercise" })
404
- }
405
- })
406
- )
407
-
408
- app.post(
409
- "/actions/translate",
410
- jsonBodyParser,
411
- withHandler(async (req: express.Request, res: express.Response) => {
412
- const { exerciseSlugs, languages } = req.body
413
-
414
- const session = await SessionManager.getPayload()
415
- const rigoToken = session?.rigobot?.key
416
-
417
- const configDirPath = config?.dirPath
418
-
419
- if (!rigoToken) {
420
- return res.status(400).json({ error: "RigoToken not found" })
421
- }
422
-
423
- const languagesToTranslate: string[] = languages.split(",")
424
-
425
- try {
426
- await Promise.all(
427
- exerciseSlugs.map(async (slug: string) => {
428
- const exercise = configManager.getExercise(slug)
429
- if (!exercise) {
430
- throw new Error(`Exercise ${slug} not found`)
431
- }
432
-
433
- if (exercise.getReadme) {
434
- const readme = exercise.getReadme(null)
435
-
436
- await Promise.all(
437
- languagesToTranslate.map(async (language: string) => {
438
- const response = await translateExercise(
439
- rigoToken,
440
- {
441
- text_to_translate: readme.body,
442
- output_language: language,
443
- },
444
- `${process.env.HOST}/webhooks/translate-exercise`
445
- )
446
-
447
- await saveTranslatedReadme(
448
- slug,
449
- response.parsed.output_language_code,
450
- response.parsed.translation
451
- )
452
-
453
- addExerciseToSidebar(
454
- slug,
455
- response.parsed.output_language_code,
456
- response.parsed.translated_slug,
457
- configDirPath || ""
458
- )
459
-
460
- Console.success(
461
- `Translated ${slug} to ${language} successfully`
462
- )
463
- })
464
- )
465
- }
466
- })
467
- )
468
-
469
- configManager.buildIndex()
470
-
471
- return res.status(200).json({ message: "Translated exercises" })
472
- } catch (error) {
473
- console.log(error, "ERROR")
474
- return res.status(400).json({ error: (error as Error).message })
475
- }
476
- })
477
- )
478
-
479
- app.put(
480
- "/actions/rename",
481
- jsonBodyParser,
482
- withHandler(async (req: express.Request, res: express.Response) => {
483
- const { slug, newSlug } = req.body
484
- const exercise = configManager.getExercise(slug)
485
- if (!exercise) {
486
- return res.status(400).json({ error: "Exercise not found" })
487
- }
488
-
489
- try {
490
- if (exercise.renameFolder) {
491
- exercise.renameFolder(newSlug)
492
- res.json({ status: "ok" })
493
- } else {
494
- res.status(500).json({
495
- error:
496
- "Failed to rename exercise because it's not supported by the exercise",
497
- })
498
- }
499
- } catch {
500
- res.status(500).json({ error: "Failed to rename exercise" })
501
- }
502
- })
503
- )
504
-
505
- app.post(
506
- "/exercise/:slug/create",
507
- jsonBodyParser,
508
- withHandler(async (req: express.Request, res: express.Response) => {
509
- const { title, readme, language } = req.body
510
- const { slug } = req.params
511
-
512
- if (!title || !readme || !language) {
513
- return res.status(400).json({ error: "Missing required fields" })
514
- }
515
-
516
- try {
517
- const exerciseCreated = await configManager.createExercise(
518
- slug,
519
- readme,
520
- language
521
- )
522
- if (exerciseCreated) {
523
- configManager.buildIndex()
524
- res.json({ status: "ok" })
525
- } else {
526
- res.status(500).json({ error: "Failed to create exercise" })
527
- }
528
- } catch {
529
- res.status(500).json({ error: "Failed to create exercise" })
530
- }
531
- })
532
- )
533
-
534
- const textBodyParser = bodyParser.text()
535
- app.put(
536
- "/exercise/:slug/file/:fileName",
537
- textBodyParser,
538
- withHandler((req: express.Request, res: express.Response) => {
539
- const exercise = configManager.getExercise(req.params.slug)
540
- if (exercise && exercise.saveFile) {
541
- exercise.saveFile(req.params.fileName, req.body)
542
- res.end()
543
- }
544
- })
545
- )
546
-
547
- if (config?.outputPath) {
548
- app.use("/preview", express.static(config.outputPath))
549
- }
550
-
551
- app.use("/", express.static(`${config?.dirPath}/_app`))
552
-
553
- app.use("/creator", express.static(path.join(__dirname, "..", "creatorDist")))
554
- }
1
+ import Console from "../../utils/console"
2
+ import * as express from "express"
3
+ import * as fs from "fs"
4
+ import * as bodyParser from "body-parser"
5
+ import socket from "../socket"
6
+ import queue from "../../utils/fileQueue"
7
+ // import gitpod from '../gitpod'
8
+ import { detect, filterFiles } from "../config/exercise"
9
+ import { IFile } from "../../models/file"
10
+ import { IConfigObj, TEntries } from "../../models/config"
11
+ import { IConfigManager } from "../../models/config-manager"
12
+ import { IExercise } from "../../models/exercise-obj"
13
+ import SessionManager from "../../managers/session"
14
+ import TelemetryManager from "../telemetry"
15
+ import { saveTranslatedReadme } from "../../utils/creatorUtilities"
16
+ import { translateExercise } from "../../utils/rigoActions"
17
+ import { addExerciseToSidebar } from "../../utils/sidebarGenerator"
18
+ import path = require("path")
19
+ // import { eventManager } from "../../utils/osOperations"
20
+
21
+ const withHandler =
22
+ (func: (req: express.Request, res: express.Response) => void) =>
23
+ (req: express.Request, res: express.Response) => {
24
+ try {
25
+ func(req, res)
26
+ } catch (error) {
27
+ Console.debug(error)
28
+ const _err = {
29
+ message: (error as TypeError).message || "There has been an error",
30
+ status: (error as any).status || 500,
31
+ type: (error as any).type || null,
32
+ }
33
+ Console.error(_err.message)
34
+
35
+ // send rep to the server
36
+ res.status(_err.status)
37
+ res.json(_err)
38
+ }
39
+ }
40
+
41
+ export default async function (
42
+ app: express.Application,
43
+ configObject: IConfigObj,
44
+ configManager: IConfigManager
45
+ ) {
46
+ const { config, exercises } = configObject
47
+ const session = await SessionManager.get(configManager?.get())
48
+
49
+ const dispatcher = queue.dispatcher({
50
+ create: true,
51
+ path: `${config?.dirPath}/vscode_queue.json`,
52
+ })
53
+
54
+ app.get(
55
+ "/config",
56
+ withHandler((_: express.Request, res: express.Response) => {
57
+ const confObject = configManager.get()
58
+ res.json(confObject)
59
+ })
60
+ )
61
+
62
+ // Added this line to parse the json body
63
+
64
+ const jsonBodyParser = bodyParser.json({ limit: "10mb" })
65
+ // Trying to log in from frontend
66
+ app.post(
67
+ "/login",
68
+ jsonBodyParser,
69
+ withHandler(async (req: express.Request, res: express.Response) => {
70
+ const email = req.body.email
71
+ const password = req.body.password
72
+
73
+ SessionManager.destroy()
74
+ const payload = await SessionManager.loginWeb(email, password)
75
+
76
+ res.json(payload)
77
+ })
78
+ )
79
+
80
+ app.post(
81
+ "/logout",
82
+ jsonBodyParser,
83
+ withHandler(async (req: express.Request, res: express.Response) => {
84
+ SessionManager.destroy()
85
+ res.json({ message: "You've logged out from Breathecode and Rigobot" })
86
+ })
87
+ )
88
+
89
+ app.post(
90
+ "/set-rigobot-token",
91
+ jsonBodyParser,
92
+ withHandler(async (req: express.Request, res: express.Response) => {
93
+ const token = req.body.token
94
+ // Ensure token is provided in the request body
95
+ if (!token) {
96
+ return res.status(400).json({ error: "Token is required" })
97
+ }
98
+
99
+ try {
100
+ const tokenSaved = await SessionManager.setRigoToken(token)
101
+ // Check if the token was saved successfully
102
+ if (tokenSaved) {
103
+ res.json({ status: "ok" })
104
+ } else {
105
+ res.status(500).json({ error: "Failed to save the token" })
106
+ }
107
+ } catch {
108
+ // Handle any unexpected errors during the process
109
+ res.status(500).json({ error: "Internal server error" })
110
+ }
111
+ })
112
+ )
113
+ app.post(
114
+ "/set-tab-hash",
115
+ jsonBodyParser,
116
+ withHandler(async (req: express.Request, res: express.Response) => {
117
+ const hash = req.body.hash
118
+ if (!hash) {
119
+ return res.status(400).json({ error: "Token is required" })
120
+ }
121
+
122
+ try {
123
+ const hashSaved = await SessionManager.setTabHash(hash)
124
+ if (hashSaved) {
125
+ res.json({ status: "ok" })
126
+ } else {
127
+ res.status(500).json({ error: "Failed to save the token" })
128
+ }
129
+ } catch {
130
+ res.status(500).json({ error: "Internal server error" })
131
+ }
132
+ })
133
+ )
134
+ app.post(
135
+ "/set-session-key",
136
+ jsonBodyParser,
137
+ withHandler(async (req: express.Request, res: express.Response) => {
138
+ const sessionKey = req.body.sessionKey
139
+ if (!sessionKey) {
140
+ return res.status(400).json({ error: "Token is required" })
141
+ }
142
+
143
+ try {
144
+ const sessionSaved = await SessionManager.setSessionKey(sessionKey)
145
+ if (sessionSaved) {
146
+ res.json({ status: "ok" })
147
+ } else {
148
+ res.status(500).json({ error: "Failed to save the token" })
149
+ }
150
+ } catch {
151
+ res.status(500).json({ error: "Internal server error" })
152
+ }
153
+ })
154
+ )
155
+ app.post(
156
+ "/set-session",
157
+ jsonBodyParser,
158
+ withHandler(async (req: express.Request, res: express.Response) => {
159
+ const payload = req.body.payload
160
+ if (!payload) {
161
+ return res.status(400).json({ error: "Token is required" })
162
+ }
163
+
164
+ try {
165
+ const payloadSaved = await SessionManager.setPayload(payload)
166
+ if (payloadSaved) {
167
+ res.json({ status: "ok" })
168
+ socket.sessionRefreshed(payload)
169
+ } else {
170
+ res.status(500).json({ error: "Failed to save the token" })
171
+ }
172
+ } catch {
173
+ res.status(500).json({ error: "Internal server error" })
174
+ }
175
+ })
176
+ )
177
+
178
+ app.get("/telemetry", async (req, res) => {
179
+ const telemetry = await TelemetryManager.retrieve()
180
+ res.json(telemetry)
181
+ })
182
+
183
+ app.post("/telemetry", jsonBodyParser, async (req, res) => {
184
+ const telemetry = req.body
185
+ await TelemetryManager.save(telemetry)
186
+ res.json({ message: "Telemetry saved successfully" })
187
+ })
188
+
189
+ app.get(
190
+ "/check/rigo/status",
191
+ withHandler(async (_: express.Request, res: express.Response) => {
192
+ const payload = await SessionManager.getPayload()
193
+
194
+ if (payload && payload.rigobot && payload.rigobot.key) {
195
+ res.json({ rigoToken: payload.rigobot.key, payload: payload })
196
+ } else {
197
+ res
198
+ .status(400)
199
+ .json({ details: `Rigobot token not found, please log in first!` })
200
+ }
201
+ })
202
+ )
203
+
204
+ // symbolic link to maintain path compatiblity
205
+ const fetchStaticAsset = withHandler((req, res) => {
206
+ const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
207
+ if (!fs.existsSync(filePath)) throw new Error("File not found: " + filePath)
208
+ const content = fs.readFileSync(filePath)
209
+ res.write(content)
210
+ res.end()
211
+ })
212
+
213
+ app.get(
214
+ `${
215
+ config?.dirPath.indexOf("./") === 0 ?
216
+ config.dirPath.slice(1) :
217
+ config?.dirPath
218
+ }/assets/:filePath`,
219
+ fetchStaticAsset
220
+ )
221
+
222
+ app.get("/assets/:filePath", fetchStaticAsset)
223
+
224
+ app.get(
225
+ "/exercise",
226
+ withHandler((_: express.Request, res: express.Response) => {
227
+ res.json(exercises)
228
+ })
229
+ )
230
+
231
+ app.get(
232
+ "/exercise/:slug/readme",
233
+ withHandler(
234
+ (
235
+ { params: { slug }, query: { lang } }: express.Request,
236
+ res: express.Response
237
+ ) => {
238
+ const exercise: IExercise = configManager.getExercise(slug)
239
+
240
+ if (exercise && exercise.getReadme) {
241
+ const readme = exercise.getReadme((lang as string) || null)
242
+ res.json(readme)
243
+ } else {
244
+ res.status(400)
245
+ }
246
+ }
247
+ )
248
+ )
249
+
250
+ app.get(
251
+ "/exercise/:slug/report",
252
+ withHandler(
253
+ ({ params: { slug } }: express.Request, res: express.Response) => {
254
+ const exercise = configManager.getExercise(slug)
255
+ if (exercise && exercise.getTestReport) {
256
+ const report = exercise.getTestReport()
257
+ res.json(JSON.stringify(report))
258
+ }
259
+ }
260
+ )
261
+ )
262
+
263
+ app.get(
264
+ "/exercise/:slug",
265
+ withHandler((req: express.Request, res: express.Response) => {
266
+ // no need to re-start exercise if it's already started
267
+ if (
268
+ configObject.currentExercise &&
269
+ req.params.slug === configObject.currentExercise
270
+ ) {
271
+ const exercise = configManager.getExercise(req.params.slug)
272
+ res.json(exercise)
273
+ if (exercise.position) {
274
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
275
+ }
276
+
277
+ return
278
+ }
279
+
280
+ const exercise = configManager.startExercise(req.params.slug)
281
+ if (exercise.position) {
282
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
283
+ }
284
+
285
+ if (configObject.config?.editor.agent !== "os") {
286
+ dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
287
+ }
288
+
289
+ type TEntry = "python3" | "html" | "node" | "react" | "java"
290
+
291
+ const entries = new Set(
292
+ Object.keys(config?.entries!).map(
293
+ lang => config?.entries[lang as TEntry]
294
+ )
295
+ )
296
+
297
+ // if we are in incremental grading, the entry file can by dinamically detected
298
+ // based on the changes the student is making during the exercise
299
+ if (config?.grading === "incremental") {
300
+ const scanedFiles = fs.readdirSync("./")
301
+
302
+ // update the file hierarchy with updates
303
+ exercise.files = [
304
+ ...exercise.files.filter(f => f.name.includes("test.")),
305
+ ...filterFiles(scanedFiles),
306
+ ]
307
+ Console.debug(`Exercise updated files: `, exercise.files)
308
+ }
309
+
310
+ const detected = detect(
311
+ configObject,
312
+ exercise.files
313
+ .filter(fileName => entries.has(fileName.name))
314
+ .map(f => f.name || f) as string[]
315
+ )
316
+
317
+ // if a new language for the testing engine is detected, we replace it
318
+ // if not we leave it as it was before
319
+ if (config?.language && !["", "auto"].includes(config?.language)) {
320
+ Console.debug(
321
+ `Exercise language ignored, instead imported from configuration ${config?.language}`
322
+ )
323
+ exercise.language = detected?.language
324
+ } else if (
325
+ detected?.language &&
326
+ (!config?.language || config?.language === "auto")
327
+ ) {
328
+ Console.debug(
329
+ `Switching to ${detected.language} engine in this exercise`
330
+ )
331
+ exercise.language = detected.language
332
+ }
333
+
334
+ // WARNING: has to be the FULL PATH to the entry path
335
+ // We need to detect entry in both gradings: Incremental and Isolate
336
+ exercise.entry = detected?.entry
337
+ Console.debug(
338
+ `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
339
+ )
340
+
341
+ // exercises.graded and exercises.disableGrading deprecated.
342
+ if (
343
+ !exercise.graded ||
344
+ config?.disableGrading ||
345
+ config?.disabledActions?.includes("test")
346
+ ) {
347
+ socket.removeAllowed("test")
348
+ } else {
349
+ socket.addAllowed("test")
350
+ }
351
+
352
+ if (!exercise.entry || config?.disabledActions?.includes("build")) {
353
+ socket.removeAllowed("build")
354
+ } else {
355
+ socket.addAllowed("build")
356
+ }
357
+
358
+ if (
359
+ exercise.files.filter(
360
+ (f: IFile) =>
361
+ !f.name.toLowerCase().includes("readme.") &&
362
+ !f.name.toLowerCase().includes("test.")
363
+ ).length === 0 ||
364
+ config?.disabledActions?.includes("reset")
365
+ ) {
366
+ socket.removeAllowed("reset")
367
+ } else if (!config?.disabledActions?.includes("reset")) {
368
+ socket.addAllowed("reset")
369
+ }
370
+
371
+ socket.log("ready")
372
+ res.json(exercise)
373
+ })
374
+ )
375
+
376
+ app.get(
377
+ "/exercise/:slug/file/:fileName",
378
+ withHandler((req: express.Request, res: express.Response) => {
379
+ const exercise = configManager.getExercise(req.params.slug)
380
+ if (exercise && exercise.getFile) {
381
+ res.write(exercise.getFile(req.params.fileName))
382
+ res.end()
383
+ }
384
+ })
385
+ )
386
+
387
+ app.get(
388
+ "/sidebar",
389
+ withHandler((req: express.Request, res: express.Response) => {
390
+ const sidebar = configManager.getSidebar()
391
+ res.json(sidebar)
392
+ })
393
+ )
394
+
395
+ app.delete(
396
+ "/exercise/:slug/delete",
397
+ withHandler(async (req: express.Request, res: express.Response) => {
398
+ const exerciseDeleted = configManager.deleteExercise(req.params.slug)
399
+ if (exerciseDeleted) {
400
+ configManager.buildIndex()
401
+ res.json({ status: "ok" })
402
+ } else {
403
+ res.status(500).json({ error: "Failed to delete exercise" })
404
+ }
405
+ })
406
+ )
407
+
408
+ app.post(
409
+ "/actions/translate",
410
+ jsonBodyParser,
411
+ withHandler(async (req: express.Request, res: express.Response) => {
412
+ const { exerciseSlugs, languages } = req.body
413
+
414
+ const session = await SessionManager.getPayload()
415
+ const rigoToken = session?.rigobot?.key
416
+
417
+ const configDirPath = config?.dirPath
418
+
419
+ if (!rigoToken) {
420
+ return res.status(400).json({ error: "RigoToken not found" })
421
+ }
422
+
423
+ const languagesToTranslate: string[] = languages.split(",")
424
+
425
+ try {
426
+ await Promise.all(
427
+ exerciseSlugs.map(async (slug: string) => {
428
+ const exercise = configManager.getExercise(slug)
429
+ if (!exercise) {
430
+ throw new Error(`Exercise ${slug} not found`)
431
+ }
432
+
433
+ if (exercise.getReadme) {
434
+ const readme = exercise.getReadme(null)
435
+
436
+ await Promise.all(
437
+ languagesToTranslate.map(async (language: string) => {
438
+ const response = await translateExercise(
439
+ rigoToken,
440
+ {
441
+ text_to_translate: readme.body,
442
+ output_language: language,
443
+ },
444
+ `${process.env.HOST}/webhooks/translate-exercise`
445
+ )
446
+
447
+ await saveTranslatedReadme(
448
+ slug,
449
+ response.parsed.output_language_code,
450
+ response.parsed.translation
451
+ )
452
+
453
+ addExerciseToSidebar(
454
+ slug,
455
+ response.parsed.output_language_code,
456
+ response.parsed.translated_slug,
457
+ configDirPath || ""
458
+ )
459
+
460
+ Console.success(
461
+ `Translated ${slug} to ${language} successfully`
462
+ )
463
+ })
464
+ )
465
+ }
466
+ })
467
+ )
468
+
469
+ configManager.buildIndex()
470
+
471
+ return res.status(200).json({ message: "Translated exercises" })
472
+ } catch (error) {
473
+ console.log(error, "ERROR")
474
+ return res.status(400).json({ error: (error as Error).message })
475
+ }
476
+ })
477
+ )
478
+
479
+ app.put(
480
+ "/actions/rename",
481
+ jsonBodyParser,
482
+ withHandler(async (req: express.Request, res: express.Response) => {
483
+ const { slug, newSlug } = req.body
484
+ const exercise = configManager.getExercise(slug)
485
+ if (!exercise) {
486
+ return res.status(400).json({ error: "Exercise not found" })
487
+ }
488
+
489
+ try {
490
+ if (exercise.renameFolder) {
491
+ exercise.renameFolder(newSlug)
492
+ res.json({ status: "ok" })
493
+ } else {
494
+ res.status(500).json({
495
+ error:
496
+ "Failed to rename exercise because it's not supported by the exercise",
497
+ })
498
+ }
499
+ } catch {
500
+ res.status(500).json({ error: "Failed to rename exercise" })
501
+ }
502
+ })
503
+ )
504
+
505
+ app.post(
506
+ "/exercise/:slug/create",
507
+ jsonBodyParser,
508
+ withHandler(async (req: express.Request, res: express.Response) => {
509
+ const { title, readme, language } = req.body
510
+ const { slug } = req.params
511
+
512
+ if (!title || !readme || !language) {
513
+ return res.status(400).json({ error: "Missing required fields" })
514
+ }
515
+
516
+ try {
517
+ const exerciseCreated = await configManager.createExercise(
518
+ slug,
519
+ readme,
520
+ language
521
+ )
522
+ if (exerciseCreated) {
523
+ configManager.buildIndex()
524
+ res.json({ status: "ok" })
525
+ } else {
526
+ res.status(500).json({ error: "Failed to create exercise" })
527
+ }
528
+ } catch {
529
+ res.status(500).json({ error: "Failed to create exercise" })
530
+ }
531
+ })
532
+ )
533
+
534
+ const textBodyParser = bodyParser.text()
535
+ app.put(
536
+ "/exercise/:slug/file/:fileName",
537
+ textBodyParser,
538
+ withHandler((req: express.Request, res: express.Response) => {
539
+ const exercise = configManager.getExercise(req.params.slug)
540
+ if (exercise && exercise.saveFile) {
541
+ exercise.saveFile(req.params.fileName, req.body)
542
+ res.end()
543
+ }
544
+ })
545
+ )
546
+
547
+ if (config?.outputPath) {
548
+ app.use("/preview", express.static(config.outputPath))
549
+ }
550
+
551
+ app.use("/", express.static(`${config?.dirPath}/_app`))
552
+
553
+ app.use("/creator", express.static(path.join(__dirname, "..", "creatorDist")))
554
+ }