@learnpack/learnpack 4.0.6 → 4.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,330 +1,327 @@
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 { eventManager } from "../../utils/osOperations"
16
-
17
- const withHandler =
18
- (func: (req: express.Request, res: express.Response) => void) =>
19
- (req: express.Request, res: express.Response) => {
20
- try {
21
- func(req, res)
22
- } catch (error) {
23
- Console.debug(error)
24
- const _err = {
25
- message: (error as TypeError).message || "There has been an error",
26
- status: (error as any).status || 500,
27
- type: (error as any).type || null,
28
- }
29
- Console.error(_err.message)
30
-
31
- // send rep to the server
32
- res.status(_err.status)
33
- res.json(_err)
34
- }
35
- }
36
-
37
- export default async function (
38
- app: express.Application,
39
- configObject: IConfigObj,
40
- configManager: IConfigManager
41
- ) {
42
- const { config, exercises } = configObject
43
- const session = await SessionManager.get(configManager?.get())
44
-
45
- const dispatcher = queue.dispatcher({
46
- create: true,
47
- path: `${config?.dirPath}/vscode_queue.json`,
48
- })
49
- app.get(
50
- "/config",
51
- withHandler((_: express.Request, res: express.Response) => {
52
- res.json(configObject)
53
- })
54
- )
55
-
56
- // Added this line to parse the json body
57
-
58
- const jsonBodyParser = bodyParser.json()
59
- // Trying to log in from frontend
60
- app.post(
61
- "/login",
62
- jsonBodyParser,
63
- withHandler(async (req: express.Request, res: express.Response) => {
64
- const email = req.body.email
65
- const password = req.body.password
66
-
67
- SessionManager.destroy()
68
- const payload = await SessionManager.loginWeb(email, password)
69
-
70
- res.json(payload)
71
- })
72
- )
73
- app.post(
74
- "/set-rigobot-token",
75
- jsonBodyParser,
76
- withHandler(async (req: express.Request, res: express.Response) => {
77
- const token = req.body.token
78
- // Ensure token is provided in the request body
79
- if (!token) {
80
- return res.status(400).json({ error: "Token is required" })
81
- }
82
-
83
- try {
84
- const tokenSaved = await SessionManager.setRigoToken(token)
85
- // Check if the token was saved successfully
86
- if (tokenSaved) {
87
- res.json({ status: "ok" })
88
- } else {
89
- res.status(500).json({ error: "Failed to save the token" })
90
- }
91
- } catch {
92
- // Handle any unexpected errors during the process
93
- res.status(500).json({ error: "Internal server error" })
94
- }
95
- })
96
- )
97
- app.get(
98
- "/check/rigo/status",
99
- withHandler(async (_: express.Request, res: express.Response) => {
100
- const payload = await SessionManager.getPayload()
101
-
102
- if (payload && payload.rigobot && payload.rigobot.key) {
103
- res.json({ rigoToken: payload.rigobot.key })
104
- } else {
105
- res
106
- .status(400)
107
- .json({ details: `Rigobot token not found, please log in first!` })
108
- }
109
- })
110
- )
111
-
112
- // symbolic link to maintain path compatiblity
113
- const fetchStaticAsset = withHandler((req, res) => {
114
- const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
115
- if (!fs.existsSync(filePath))
116
- throw new Error("File not found: " + filePath)
117
- const content = fs.readFileSync(filePath)
118
- res.write(content)
119
- res.end()
120
- })
121
-
122
- app.get(
123
- `${
124
- config?.dirPath.indexOf("./") === 0 ?
125
- config.dirPath.slice(1) :
126
- config?.dirPath
127
- }/assets/:filePath`,
128
- fetchStaticAsset
129
- )
130
-
131
- app.get("/assets/:filePath", fetchStaticAsset)
132
-
133
- app.get(
134
- "/exercise",
135
- withHandler((_: express.Request, res: express.Response) => {
136
- res.json(exercises)
137
- })
138
- )
139
-
140
- app.get(
141
- "/exercise/:slug/readme",
142
- withHandler(
143
- (
144
- { params: { slug }, query: { lang } }: express.Request,
145
- res: express.Response
146
- ) => {
147
- const exercise: IExercise = configManager.getExercise(slug)
148
-
149
- if (exercise && exercise.getReadme) {
150
- const readme = exercise.getReadme((lang as string) || null)
151
- res.json(readme)
152
- } else {
153
- res.status(400)
154
- }
155
- }
156
- )
157
- )
158
-
159
- app.get(
160
- "/exercise/:slug/report",
161
- withHandler(
162
- ({ params: { slug } }: express.Request, res: express.Response) => {
163
- const exercise = configManager.getExercise(slug)
164
- if (exercise && exercise.getTestReport) {
165
- const report = exercise.getTestReport()
166
- res.json(JSON.stringify(report))
167
- }
168
- }
169
- )
170
- )
171
-
172
- app.get(
173
- "/exercise/:slug",
174
- withHandler((req: express.Request, res: express.Response) => {
175
- // no need to re-start exercise if it's already started
176
- if (
177
- configObject.currentExercise &&
178
- req.params.slug === configObject.currentExercise
179
- ) {
180
- const exercise = configManager.getExercise(req.params.slug)
181
- res.json(exercise)
182
- if (exercise.position) {
183
- TelemetryManager.registerStepEvent(
184
- exercise.position,
185
- "open_step",
186
- {}
187
- )
188
- }
189
-
190
- return
191
- }
192
-
193
- const exercise = configManager.startExercise(req.params.slug)
194
- if (exercise.position) {
195
- TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
196
- }
197
-
198
- if (configObject.config?.editor.agent === "os") {
199
- eventManager.enqueue(dispatcher.events.START_EXERCISE, exercise)
200
- } else {
201
- dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
202
- }
203
-
204
- type TEntry = "python3" | "html" | "node" | "react" | "java";
205
-
206
- const entries = new Set(
207
- Object.keys(config?.entries!).map(
208
- lang => config?.entries[lang as TEntry]
209
- )
210
- )
211
-
212
- // if we are in incremental grading, the entry file can by dinamically detected
213
- // based on the changes the student is making during the exercise
214
- if (config?.grading === "incremental") {
215
- const scanedFiles = fs.readdirSync("./")
216
-
217
- // update the file hierarchy with updates
218
- exercise.files = [
219
- ...exercise.files.filter(f => f.name.includes("test.")),
220
- ...filterFiles(scanedFiles),
221
- ]
222
- Console.debug(`Exercise updated files: `, exercise.files)
223
- }
224
-
225
- const detected = detect(
226
- configObject,
227
- exercise.files
228
- .filter(fileName => entries.has(fileName.name))
229
- .map(f => f.name || f) as string[]
230
- )
231
-
232
- // if a new language for the testing engine is detected, we replace it
233
- // if not we leave it as it was before
234
- if (config?.language && !["", "auto"].includes(config?.language)) {
235
- Console.debug(
236
- `Exercise language ignored, instead imported from configuration ${config?.language}`
237
- )
238
- exercise.language = detected?.language
239
- } else if (
240
- detected?.language &&
241
- (!config?.language || config?.language === "auto")
242
- ) {
243
- Console.debug(
244
- `Switching to ${detected.language} engine in this exercise`
245
- )
246
- exercise.language = detected.language
247
- }
248
-
249
- // WARNING: has to be the FULL PATH to the entry path
250
- // We need to detect entry in both gradings: Incremental and Isolate
251
- exercise.entry = detected?.entry
252
- Console.debug(
253
- `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
254
- )
255
-
256
- // exercises.graded and exercises.disableGrading deprecated.
257
- if (
258
- !exercise.graded ||
259
- config?.disableGrading ||
260
- config?.disabledActions?.includes("test")
261
- ) {
262
- socket.removeAllowed("test")
263
- } else {
264
- socket.addAllowed("test")
265
- }
266
-
267
- if (!exercise.entry || config?.disabledActions?.includes("build")) {
268
- socket.removeAllowed("build")
269
- } else {
270
- socket.addAllowed("build")
271
- }
272
-
273
- if (
274
- exercise.files.filter(
275
- (f: IFile) =>
276
- !f.name.toLowerCase().includes("readme.") &&
277
- !f.name.toLowerCase().includes("test.")
278
- ).length === 0 ||
279
- config?.disabledActions?.includes("reset")
280
- ) {
281
- socket.removeAllowed("reset")
282
- } else if (!config?.disabledActions?.includes("reset")) {
283
- socket.addAllowed("reset")
284
- }
285
-
286
- socket.log("ready")
287
- res.json(exercise)
288
- })
289
- )
290
-
291
- app.get(
292
- "/exercise/:slug/file/:fileName",
293
- withHandler((req: express.Request, res: express.Response) => {
294
- const exercise = configManager.getExercise(req.params.slug)
295
- if (exercise && exercise.getFile) {
296
- res.write(exercise.getFile(req.params.fileName))
297
- res.end()
298
- }
299
- })
300
- )
301
-
302
- /*
303
- app.post(
304
- "/exercise/:slug/file/:fileName",
305
- withHandler((req: express.Request, res: express.Response) => {
306
- get tokens but also, add allowed action for 'generate'
307
- use the sessionManager to keep compatibility with the cli login command.
308
- })
309
- );
310
- */
311
-
312
- const textBodyParser = bodyParser.text()
313
- app.put(
314
- "/exercise/:slug/file/:fileName",
315
- textBodyParser,
316
- withHandler((req: express.Request, res: express.Response) => {
317
- const exercise = configManager.getExercise(req.params.slug)
318
- if (exercise && exercise.saveFile) {
319
- exercise.saveFile(req.params.fileName, req.body)
320
- res.end()
321
- }
322
- })
323
- )
324
-
325
- if (config?.outputPath) {
326
- app.use("/preview", express.static(config.outputPath))
327
- }
328
-
329
- app.use("/", express.static(`${config?.dirPath}/_app`))
330
- }
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 { eventManager } from "../../utils/osOperations"
16
+
17
+ const withHandler =
18
+ (func: (req: express.Request, res: express.Response) => void) =>
19
+ (req: express.Request, res: express.Response) => {
20
+ try {
21
+ func(req, res)
22
+ } catch (error) {
23
+ Console.debug(error)
24
+ const _err = {
25
+ message: (error as TypeError).message || "There has been an error",
26
+ status: (error as any).status || 500,
27
+ type: (error as any).type || null,
28
+ }
29
+ Console.error(_err.message)
30
+
31
+ // send rep to the server
32
+ res.status(_err.status)
33
+ res.json(_err)
34
+ }
35
+ }
36
+
37
+ export default async function (
38
+ app: express.Application,
39
+ configObject: IConfigObj,
40
+ configManager: IConfigManager
41
+ ) {
42
+ const { config, exercises } = configObject
43
+ const session = await SessionManager.get(configManager?.get())
44
+
45
+ const dispatcher = queue.dispatcher({
46
+ create: true,
47
+ path: `${config?.dirPath}/vscode_queue.json`,
48
+ })
49
+ app.get(
50
+ "/config",
51
+ withHandler((_: express.Request, res: express.Response) => {
52
+ const confObject = configManager.get()
53
+ res.json(confObject)
54
+ })
55
+ )
56
+
57
+ // Added this line to parse the json body
58
+
59
+ const jsonBodyParser = bodyParser.json()
60
+ // Trying to log in from frontend
61
+ app.post(
62
+ "/login",
63
+ jsonBodyParser,
64
+ withHandler(async (req: express.Request, res: express.Response) => {
65
+ const email = req.body.email
66
+ const password = req.body.password
67
+
68
+ SessionManager.destroy()
69
+ const payload = await SessionManager.loginWeb(email, password)
70
+
71
+ res.json(payload)
72
+ })
73
+ )
74
+ app.post(
75
+ "/set-rigobot-token",
76
+ jsonBodyParser,
77
+ withHandler(async (req: express.Request, res: express.Response) => {
78
+ const token = req.body.token
79
+ // Ensure token is provided in the request body
80
+ if (!token) {
81
+ return res.status(400).json({ error: "Token is required" })
82
+ }
83
+
84
+ try {
85
+ const tokenSaved = await SessionManager.setRigoToken(token)
86
+ // Check if the token was saved successfully
87
+ if (tokenSaved) {
88
+ res.json({ status: "ok" })
89
+ } else {
90
+ res.status(500).json({ error: "Failed to save the token" })
91
+ }
92
+ } catch {
93
+ // Handle any unexpected errors during the process
94
+ res.status(500).json({ error: "Internal server error" })
95
+ }
96
+ })
97
+ )
98
+ app.get(
99
+ "/check/rigo/status",
100
+ withHandler(async (_: express.Request, res: express.Response) => {
101
+ const payload = await SessionManager.getPayload()
102
+
103
+ if (payload && payload.rigobot && payload.rigobot.key) {
104
+ res.json({ rigoToken: payload.rigobot.key })
105
+ } else {
106
+ res
107
+ .status(400)
108
+ .json({ details: `Rigobot token not found, please log in first!` })
109
+ }
110
+ })
111
+ )
112
+
113
+ // symbolic link to maintain path compatiblity
114
+ const fetchStaticAsset = withHandler((req, res) => {
115
+ const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
116
+ if (!fs.existsSync(filePath))
117
+ throw new Error("File not found: " + filePath)
118
+ const content = fs.readFileSync(filePath)
119
+ res.write(content)
120
+ res.end()
121
+ })
122
+
123
+ app.get(
124
+ `${
125
+ config?.dirPath.indexOf("./") === 0 ?
126
+ config.dirPath.slice(1) :
127
+ config?.dirPath
128
+ }/assets/:filePath`,
129
+ fetchStaticAsset
130
+ )
131
+
132
+ app.get("/assets/:filePath", fetchStaticAsset)
133
+
134
+ app.get(
135
+ "/exercise",
136
+ withHandler((_: express.Request, res: express.Response) => {
137
+ res.json(exercises)
138
+ })
139
+ )
140
+
141
+ app.get(
142
+ "/exercise/:slug/readme",
143
+ withHandler(
144
+ (
145
+ { params: { slug }, query: { lang } }: express.Request,
146
+ res: express.Response
147
+ ) => {
148
+ const exercise: IExercise = configManager.getExercise(slug)
149
+
150
+ if (exercise && exercise.getReadme) {
151
+ const readme = exercise.getReadme((lang as string) || null)
152
+ res.json(readme)
153
+ } else {
154
+ res.status(400)
155
+ }
156
+ }
157
+ )
158
+ )
159
+
160
+ app.get(
161
+ "/exercise/:slug/report",
162
+ withHandler(
163
+ ({ params: { slug } }: express.Request, res: express.Response) => {
164
+ const exercise = configManager.getExercise(slug)
165
+ if (exercise && exercise.getTestReport) {
166
+ const report = exercise.getTestReport()
167
+ res.json(JSON.stringify(report))
168
+ }
169
+ }
170
+ )
171
+ )
172
+
173
+ app.get(
174
+ "/exercise/:slug",
175
+ withHandler((req: express.Request, res: express.Response) => {
176
+ // no need to re-start exercise if it's already started
177
+ if (
178
+ configObject.currentExercise &&
179
+ req.params.slug === configObject.currentExercise
180
+ ) {
181
+ const exercise = configManager.getExercise(req.params.slug)
182
+ res.json(exercise)
183
+ if (exercise.position) {
184
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
185
+ }
186
+
187
+ return
188
+ }
189
+
190
+ const exercise = configManager.startExercise(req.params.slug)
191
+ if (exercise.position) {
192
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
193
+ }
194
+
195
+ if (configObject.config?.editor.agent === "os") {
196
+ eventManager.enqueue(dispatcher.events.START_EXERCISE, exercise)
197
+ } else {
198
+ dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
199
+ }
200
+
201
+ type TEntry = "python3" | "html" | "node" | "react" | "java"
202
+
203
+ const entries = new Set(
204
+ Object.keys(config?.entries!).map(
205
+ lang => config?.entries[lang as TEntry]
206
+ )
207
+ )
208
+
209
+ // if we are in incremental grading, the entry file can by dinamically detected
210
+ // based on the changes the student is making during the exercise
211
+ if (config?.grading === "incremental") {
212
+ const scanedFiles = fs.readdirSync("./")
213
+
214
+ // update the file hierarchy with updates
215
+ exercise.files = [
216
+ ...exercise.files.filter(f => f.name.includes("test.")),
217
+ ...filterFiles(scanedFiles),
218
+ ]
219
+ Console.debug(`Exercise updated files: `, exercise.files)
220
+ }
221
+
222
+ const detected = detect(
223
+ configObject,
224
+ exercise.files
225
+ .filter(fileName => entries.has(fileName.name))
226
+ .map(f => f.name || f) as string[]
227
+ )
228
+
229
+ // if a new language for the testing engine is detected, we replace it
230
+ // if not we leave it as it was before
231
+ if (config?.language && !["", "auto"].includes(config?.language)) {
232
+ Console.debug(
233
+ `Exercise language ignored, instead imported from configuration ${config?.language}`
234
+ )
235
+ exercise.language = detected?.language
236
+ } else if (
237
+ detected?.language &&
238
+ (!config?.language || config?.language === "auto")
239
+ ) {
240
+ Console.debug(
241
+ `Switching to ${detected.language} engine in this exercise`
242
+ )
243
+ exercise.language = detected.language
244
+ }
245
+
246
+ // WARNING: has to be the FULL PATH to the entry path
247
+ // We need to detect entry in both gradings: Incremental and Isolate
248
+ exercise.entry = detected?.entry
249
+ Console.debug(
250
+ `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
251
+ )
252
+
253
+ // exercises.graded and exercises.disableGrading deprecated.
254
+ if (
255
+ !exercise.graded ||
256
+ config?.disableGrading ||
257
+ config?.disabledActions?.includes("test")
258
+ ) {
259
+ socket.removeAllowed("test")
260
+ } else {
261
+ socket.addAllowed("test")
262
+ }
263
+
264
+ if (!exercise.entry || config?.disabledActions?.includes("build")) {
265
+ socket.removeAllowed("build")
266
+ } else {
267
+ socket.addAllowed("build")
268
+ }
269
+
270
+ if (
271
+ exercise.files.filter(
272
+ (f: IFile) =>
273
+ !f.name.toLowerCase().includes("readme.") &&
274
+ !f.name.toLowerCase().includes("test.")
275
+ ).length === 0 ||
276
+ config?.disabledActions?.includes("reset")
277
+ ) {
278
+ socket.removeAllowed("reset")
279
+ } else if (!config?.disabledActions?.includes("reset")) {
280
+ socket.addAllowed("reset")
281
+ }
282
+
283
+ socket.log("ready")
284
+ res.json(exercise)
285
+ })
286
+ )
287
+
288
+ app.get(
289
+ "/exercise/:slug/file/:fileName",
290
+ withHandler((req: express.Request, res: express.Response) => {
291
+ const exercise = configManager.getExercise(req.params.slug)
292
+ if (exercise && exercise.getFile) {
293
+ res.write(exercise.getFile(req.params.fileName))
294
+ res.end()
295
+ }
296
+ })
297
+ )
298
+
299
+ /*
300
+ app.post(
301
+ "/exercise/:slug/file/:fileName",
302
+ withHandler((req: express.Request, res: express.Response) => {
303
+ get tokens but also, add allowed action for 'generate'
304
+ use the sessionManager to keep compatibility with the cli login command.
305
+ })
306
+ );
307
+ */
308
+
309
+ const textBodyParser = bodyParser.text()
310
+ app.put(
311
+ "/exercise/:slug/file/:fileName",
312
+ textBodyParser,
313
+ withHandler((req: express.Request, res: express.Response) => {
314
+ const exercise = configManager.getExercise(req.params.slug)
315
+ if (exercise && exercise.saveFile) {
316
+ exercise.saveFile(req.params.fileName, req.body)
317
+ res.end()
318
+ }
319
+ })
320
+ )
321
+
322
+ if (config?.outputPath) {
323
+ app.use("/preview", express.static(config.outputPath))
324
+ }
325
+
326
+ app.use("/", express.static(`${config?.dirPath}/_app`))
327
+ }