@learnpack/learnpack 5.0.270 → 5.0.274

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 (77) 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/serve.js +48 -20
  8. package/lib/creatorDist/assets/{index-CQXTTbaZ.js → index-BfLyIQVh.js} +11535 -11409
  9. package/lib/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  10. package/lib/creatorDist/index.html +2 -2
  11. package/lib/managers/config/index.js +77 -77
  12. package/lib/models/creator.d.ts +1 -0
  13. package/lib/utils/api.js +1 -0
  14. package/lib/utils/creatorUtilities.js +14 -14
  15. package/package.json +1 -1
  16. package/src/commands/audit.ts +487 -487
  17. package/src/commands/breakToken.ts +67 -67
  18. package/src/commands/clean.ts +30 -30
  19. package/src/commands/init.ts +650 -650
  20. package/src/commands/logout.ts +38 -38
  21. package/src/commands/publish.ts +522 -522
  22. package/src/commands/serve.ts +64 -33
  23. package/src/commands/start.ts +333 -333
  24. package/src/commands/translate.ts +123 -123
  25. package/src/creator/README.md +54 -54
  26. package/src/creator/eslint.config.js +28 -28
  27. package/src/creator/src/components/syllabus/ContentIndex.tsx +1 -1
  28. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +3 -1
  29. package/src/creator/src/i18n.ts +28 -28
  30. package/src/creator/src/index.css +217 -217
  31. package/src/creator/src/locales/en.json +1 -0
  32. package/src/creator/src/locales/es.json +1 -0
  33. package/src/creator/src/utils/configTypes.ts +122 -122
  34. package/src/creator/src/utils/constants.ts +13 -13
  35. package/src/creator/src/utils/creatorUtils.ts +46 -46
  36. package/src/creator/src/utils/eventBus.ts +2 -2
  37. package/src/creator/src/utils/lib.ts +468 -468
  38. package/src/creator/src/utils/rigo.ts +26 -26
  39. package/src/creator/src/utils/socket.ts +61 -61
  40. package/src/creator/src/utils/store.ts +222 -222
  41. package/src/creator/src/vite-env.d.ts +1 -1
  42. package/src/creator/vite.config.ts +13 -13
  43. package/src/creatorDist/assets/{index-CQXTTbaZ.js → index-BfLyIQVh.js} +11535 -11409
  44. package/src/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  45. package/src/creatorDist/index.html +2 -2
  46. package/src/managers/config/defaults.ts +49 -49
  47. package/src/managers/config/exercise.ts +364 -364
  48. package/src/managers/config/index.ts +775 -775
  49. package/src/managers/file.ts +236 -236
  50. package/src/managers/server/routes.ts +554 -554
  51. package/src/managers/session.ts +182 -182
  52. package/src/managers/telemetry.ts +188 -188
  53. package/src/models/action.ts +13 -13
  54. package/src/models/config-manager.ts +28 -28
  55. package/src/models/config.ts +106 -106
  56. package/src/models/creator.ts +40 -39
  57. package/src/models/exercise-obj.ts +30 -30
  58. package/src/models/session.ts +39 -39
  59. package/src/models/socket.ts +61 -61
  60. package/src/models/status.ts +16 -16
  61. package/src/ui/_app/app.css +1 -1
  62. package/src/ui/_app/app.js +435 -414
  63. package/src/ui/_app/learnpack.svg +7 -7
  64. package/src/ui/app.tar.gz +0 -0
  65. package/src/utils/BaseCommand.ts +56 -56
  66. package/src/utils/api.ts +31 -30
  67. package/src/utils/audit.ts +392 -392
  68. package/src/utils/checkNotInstalled.ts +267 -267
  69. package/src/utils/configBuilder.ts +82 -82
  70. package/src/utils/convertCreds.js +34 -34
  71. package/src/utils/creatorUtilities.ts +504 -504
  72. package/src/utils/incrementVersion.js +74 -74
  73. package/src/utils/misc.ts +58 -58
  74. package/src/utils/rigoActions.ts +500 -500
  75. package/src/utils/sidebarGenerator.ts +195 -195
  76. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  77. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -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
+ }