@learnpack/learnpack 5.0.309 → 5.0.310

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 (87) 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/logout.js +3 -3
  6. package/lib/commands/serve.js +16 -16
  7. package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  8. package/lib/creatorDist/index.html +1 -1
  9. package/lib/managers/config/index.js +77 -77
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  12. package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  13. package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  14. package/lib/utils/templates/scorm/config/index.html +209 -209
  15. package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
  16. package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  17. package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
  18. package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
  19. package/package.json +1 -1
  20. package/src/commands/audit.ts +487 -487
  21. package/src/commands/breakToken.ts +67 -67
  22. package/src/commands/clean.ts +30 -30
  23. package/src/commands/logout.ts +38 -38
  24. package/src/commands/publish.ts +517 -517
  25. package/src/commands/serve.ts +3179 -3179
  26. package/src/commands/start.ts +333 -333
  27. package/src/commands/translate.ts +123 -123
  28. package/src/creator/README.md +54 -54
  29. package/src/creator/package-lock.json +6621 -6621
  30. package/src/creator/package.json +55 -55
  31. package/src/creator/src/App.tsx +611 -608
  32. package/src/creator/src/components/FileUploader.tsx +340 -302
  33. package/src/creator/src/components/Icon.tsx +18 -18
  34. package/src/creator/src/components/LessonItem.tsx +152 -152
  35. package/src/creator/src/components/Login.tsx +259 -259
  36. package/src/creator/src/components/ParamsChecker.tsx +25 -25
  37. package/src/creator/src/components/Uploader.tsx +3 -6
  38. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  39. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
  40. package/src/creator/src/i18n.ts +28 -28
  41. package/src/creator/src/locales/en.json +139 -138
  42. package/src/creator/src/locales/es.json +139 -138
  43. package/src/creator/src/utils/configTypes.ts +122 -122
  44. package/src/creator/src/utils/constants.ts +13 -13
  45. package/src/creator/src/utils/creatorUtils.ts +46 -46
  46. package/src/creator/src/utils/eventBus.ts +2 -2
  47. package/src/creator/src/utils/rigo.ts +1 -1
  48. package/src/creator/src/utils/socket.ts +61 -61
  49. package/src/creator/src/utils/store.ts +222 -222
  50. package/src/creator/src/vite-env.d.ts +1 -1
  51. package/src/creator/vite.config.ts +13 -13
  52. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  53. package/src/creatorDist/index.html +1 -1
  54. package/src/managers/config/defaults.ts +49 -49
  55. package/src/managers/config/exercise.ts +364 -364
  56. package/src/managers/config/index.ts +775 -775
  57. package/src/managers/file.ts +236 -236
  58. package/src/managers/server/routes.ts +554 -554
  59. package/src/managers/telemetry.ts +188 -188
  60. package/src/models/action.ts +13 -13
  61. package/src/models/config-manager.ts +28 -28
  62. package/src/models/config.ts +106 -106
  63. package/src/models/exercise-obj.ts +30 -30
  64. package/src/models/session.ts +39 -39
  65. package/src/models/socket.ts +61 -61
  66. package/src/models/status.ts +16 -16
  67. package/src/ui/_app/app.css +1 -1
  68. package/src/ui/_app/app.js +477 -407
  69. package/src/ui/app.tar.gz +0 -0
  70. package/src/utils/BaseCommand.ts +56 -56
  71. package/src/utils/api.ts +665 -665
  72. package/src/utils/audit.ts +392 -392
  73. package/src/utils/checkNotInstalled.ts +267 -267
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -178
  77. package/src/utils/incrementVersion.js +74 -74
  78. package/src/utils/misc.ts +58 -58
  79. package/src/utils/sidebarGenerator.ts +195 -195
  80. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  82. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  83. package/src/utils/templates/scorm/config/index.html +209 -209
  84. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  85. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  86. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  87. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
@@ -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
+ }