-
Notifications
You must be signed in to change notification settings - Fork 1
Allow updating files (instead of overriding) for multiple file uploads #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| next-env.d.ts merge=ours |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import db from "@/utils/db"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUserId } from "@/utils/user"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isAdminFor } from "@/api/user"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUploadDir } from "@/utils/zip"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Functions in this module get user's accessToken rather than id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| because id's can be faked. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +33,7 @@ export type PostAnswerResult = | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: "error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: string} | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: "ok", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storedAnswer: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isCorrect: boolean | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| points: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| correctAnswer?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,15 +63,18 @@ export const postAnswer = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!question) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {status: "error", message: "Question id and book id don't match"}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nPastAnswers = (await db.get(` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SELECT COUNT(*) as count FROM answers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pastAnswers = await db.all(` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SELECT answer FROM answers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE userId = ? AND bookId = ? AND groupId IS ? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND questionId = ? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `, [userId, bookId, groupId, question.id])).count; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (question.maxAttempts && nPastAnswers >= question.maxAttempts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `, [userId, bookId, groupId, question.id]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (question.maxAttempts && pastAnswers.length >= question.maxAttempts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {status: "error", message: "Maximum number of attempts reached"}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pastFiles = question.type !== "uploads" || !pastAnswers.length ? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : pastAnswers[pastAnswers.length - 1].answer.split(":"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* If we have the answer in the database, we check the submitted answer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and assign points. Otherwise, we trust the received isCorrect and points. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,18 +86,23 @@ export const postAnswer = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !question.answer ? points | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : actCorrect && question.maxPoints || 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const actAnswer = !pastFiles.length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? answer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : [...pastFiles, ...answer.split(":").filter((f) => !pastFiles.includes(f))] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join(":"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+89
to
+92
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await db.run( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `INSERT INTO answers (userId, bookId, groupId, questionId, answer, isCorrect, points) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VALUES (?, ?, ?, ?, ?, ?, ?)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [userId, bookId, groupId, question.id, answer, actCorrect, actPoints] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [userId, bookId, groupId, question.id, actAnswer, actCorrect, actPoints] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shownAnswer = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (question.maxAttempts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && nPastAnswers + 1 >= question.maxAttempts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && pastAnswers.length + 1 >= question.maxAttempts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && question.answer) ? { correctAnswer: question.answer } : {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "ok", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storedAnswer: actAnswer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isCorrect: actCorrect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| points: actPoints, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...shownAnswer}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -296,6 +308,52 @@ export const getUserFilesInBook = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) as (Omit<UserFileAnswersInBook, "fileNames"> & {answer: string})[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).map(({answer, ...rest}) => ({fileNames: answer.split(":"), ...rest})); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const removeUserFile = async ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {accessToken, bookId, groupId, questionId, fileName}: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accessToken: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bookId: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupId: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| questionId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileName: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<string | null> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastAnswer = (await db.get(` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SELECT a.id, a.answer FROM answers a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JOIN questions q ON a.questionId = q.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JOIN books_chapters bc ON q.chapterId = bc.chapterId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JOIN users ON a.userId = users.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE users.accessToken = ? AND groupId IS ? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND q.questionId = ? AND bc.bookId = ? AND q.type LIKE 'upload%' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ORDER BY a.createdAt DESC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LIMIT 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `, [accessToken, groupId, questionId, bookId] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) as ({id: number, answer: string} | undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!lastAnswer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const answers = lastAnswer.answer.split(":"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!answers.includes(fileName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const {dir, error} = await getUploadDir( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {accessToken, bookId, groupId, qId: questionId}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (dir && !error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fname = path.join(dir, fileName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fname = path.join(dir, fileName); | |
| const safeFileName = path.basename(fileName); | |
| // Prevent path traversal: if basename changed, the input contained path separators | |
| if (safeFileName !== fileName) { | |
| return null; | |
| } | |
| const fname = path.join(dir, safeFileName); |
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When all files are removed, the filter operation will create an empty array, and join(":") will return an empty string. However, if there's ever a case where answer is already an empty string, split(":") returns [""], and if fileName is also "", this could match and try to filter it out. Consider adding a check to handle empty answers more explicitly, or filter out empty strings after split: answers.filter(f => f && f !== fileName)
| const newAnswers = answers.filter((f) => f !== fileName).join(":"); | |
| const newAnswers = answers.filter((f) => f && f !== fileName).join(":"); |
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removeFile function has a potential race condition. If the database update succeeds but the file system deletion fails (or vice versa), the system state becomes inconsistent. Consider either handling errors from fs.rmSync or wrapping the entire operation in a transaction-like pattern. Additionally, if answer is null, the function returns false but doesn't provide any error feedback to the user.
| return null; | |
| } | |
| const answers = lastAnswer.answer.split(":"); | |
| if (!answers.includes(fileName)) { | |
| return null; | |
| } | |
| const {dir, error} = await getUploadDir( | |
| {accessToken, bookId, groupId, qId: questionId}); | |
| if (dir && !error) { | |
| const fname = path.join(dir, fileName); | |
| if (fs.existsSync(fname)) { | |
| fs.rmSync(fname); | |
| } | |
| } | |
| const newAnswers = answers.filter((f) => f !== fileName).join(":"); | |
| await db.run(` | |
| UPDATE answers | |
| SET answer = ? | |
| WHERE id = ?`, | |
| [newAnswers, lastAnswer.id] | |
| ); | |
| console.warn("removeUserFile: no previous answer found for given parameters", { | |
| bookId, | |
| groupId, | |
| questionId, | |
| fileName, | |
| }); | |
| return null; | |
| } | |
| const answers = lastAnswer.answer.split(":"); | |
| if (!answers.includes(fileName)) { | |
| console.warn("removeUserFile: requested file name not found in last answer", { | |
| bookId, | |
| groupId, | |
| questionId, | |
| fileName, | |
| }); | |
| return null; | |
| } | |
| const oldAnswers = lastAnswer.answer; | |
| const newAnswers = answers.filter((f) => f !== fileName).join(":"); | |
| // First, update the database. If this fails, do not touch the filesystem. | |
| try { | |
| await db.run(` | |
| UPDATE answers | |
| SET answer = ? | |
| WHERE id = ?`, | |
| [newAnswers, lastAnswer.id] | |
| ); | |
| } catch (err) { | |
| console.error("removeUserFile: failed to update answers in database", { | |
| error: err, | |
| answerId: lastAnswer.id, | |
| }); | |
| return null; | |
| } | |
| // After a successful DB update, attempt to remove the file. On failure, try to roll back the DB change. | |
| const {dir, error} = await getUploadDir( | |
| {accessToken, bookId, groupId, qId: questionId}); | |
| if (dir && !error) { | |
| const fname = path.join(dir, fileName); | |
| if (fs.existsSync(fname)) { | |
| try { | |
| fs.rmSync(fname); | |
| } catch (fsErr) { | |
| console.error("removeUserFile: failed to delete file from filesystem, attempting to roll back DB change", { | |
| error: fsErr, | |
| filePath: fname, | |
| answerId: lastAnswer.id, | |
| }); | |
| try { | |
| await db.run(` | |
| UPDATE answers | |
| SET answer = ? | |
| WHERE id = ?`, | |
| [oldAnswers, lastAnswer.id] | |
| ); | |
| } catch (rollbackErr) { | |
| console.error("removeUserFile: failed to roll back database after filesystem error; state may be inconsistent", { | |
| error: rollbackErr, | |
| answerId: lastAnswer.id, | |
| }); | |
| } | |
| return null; | |
| } | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import path from "path"; | ||
| import { existsSync, rmSync, mkdirSync, writeFileSync } from "fs"; | ||
| import { existsSync, mkdirSync, writeFileSync } from "fs"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getUploadDir } from "@/utils/zip"; | ||
|
|
||
|
|
@@ -27,10 +27,9 @@ export async function POST(req: Request) { | |
| { status: 500 }); | ||
| } | ||
|
|
||
| if (existsSync(dir)) { | ||
| rmSync(dir, { force: true, recursive: true }); | ||
| if (!existsSync(dir)) { | ||
| mkdirSync(dir, { recursive: true }); | ||
| } | ||
|
Comment on lines
+30
to
32
|
||
| mkdirSync(dir, { recursive: true }); | ||
|
|
||
| for (const file of files) { | ||
| const bytes = await file.arrayBuffer(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,7 +14,7 @@ export const FileQuestion = ({id, submitDisabled, setSubmitted, accept, multiple | |||||
| ref?: React.RefObject<FileDropFunction | null>; | ||||||
| }) => { | ||||||
| const {t} = useIntl(); | ||||||
| const {answer, uploadFiles} = useLastAnswer(id); | ||||||
| const {answer, uploadFiles, removeFile} = useLastAnswer(id); | ||||||
| const [files, setFiles] = React.useState<File[]>([]); | ||||||
|
|
||||||
| const onSubmitFiles = React.useCallback(async (e: React.MouseEvent) => { | ||||||
|
|
@@ -69,14 +69,34 @@ export const FileQuestion = ({id, submitDisabled, setSubmitted, accept, multiple | |||||
|
|
||||||
| React.useImperativeHandle(ref, () => onFileDrop, [onFileDrop]); | ||||||
|
|
||||||
| const onRemoveUploadedFile = React.useCallback(async (file: string) => { | ||||||
| await removeFile(file); | ||||||
| }, [removeFile]); | ||||||
|
|
||||||
| return <> | ||||||
| { answer && | ||||||
| { answer && submitDisabled && | ||||||
| <div className="mb-4"> | ||||||
| { `${t("quiz.uploaded-file")} ${answer.replaceAll(":", ", ")}.` } | ||||||
| </div> | ||||||
| } | ||||||
| { !submitDisabled && | ||||||
| <> | ||||||
| { answer && | ||||||
| <div className="mb-1 flex flex-row gap-2"> | ||||||
| {t("quiz.uploaded-file")}: | ||||||
| { answer.split(":").map((file, i) => | ||||||
| <div key={i} className="flex flex-row"> | ||||||
| {file} | ||||||
| <RiDeleteBin2Line | ||||||
| onClick={() => onRemoveUploadedFile(file)} | ||||||
| style={{cursor: "pointer"}} | ||||||
| className="hover:text-red-700" | ||||||
| /> | ||||||
|
Comment on lines
+90
to
+94
|
||||||
| </div> | ||||||
| ) | ||||||
| } | ||||||
| </div> | ||||||
| } | ||||||
| <div className="flex flex-col gap-1 my-4 border-dashed border-1 rounded p-3" | ||||||
| > | ||||||
| <div className="grid gap-x-5 px-1 mb-3 items-center" | ||||||
|
|
@@ -92,7 +112,7 @@ export const FileQuestion = ({id, submitDisabled, setSubmitted, accept, multiple | |||||
| /> | ||||||
| } | ||||||
| </React.Fragment> | ||||||
| )} | ||||||
| )} | ||||||
| </div> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <input id="file" type="file" multiple={multiple} onChange={onFileChange} | ||||||
|
|
@@ -101,7 +121,7 @@ export const FileQuestion = ({id, submitDisabled, setSubmitted, accept, multiple | |||||
| htmlFor="file" | ||||||
| className={`px-10 mr-4 submit-quiz-popup-button border border-black rounded cursor-pointer transition inline-block`} | ||||||
| > | ||||||
| {t("quiz.select-files")(files.length, multiple)} | ||||||
| {t("quiz.select-files")(files.length || answer, multiple)} | ||||||
|
||||||
| {t("quiz.select-files")(files.length || answer, multiple)} | |
| {t("quiz.select-files")(files.length || (answer ? 1 : 0), multiple)} |
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The container div has the class "text-red-500" to make text red, but the small elements inside have class "text-muted" which likely overrides the red color. The red color class should be moved to the individual small elements or removed from the parent if it's not needed.
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid automated semicolon insertion (90% of all statements in the enclosing function have an explicit semicolon).
| </> | |
| </>; |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||
| import React from "react"; | ||||||||
|
|
||||||||
| import { getQId, postAnswer, PostAnswerResult, CorrectAnswers } from "@/api/quiz"; | ||||||||
| import { getQId, postAnswer, PostAnswerResult, CorrectAnswers, removeUserFile } from "@/api/quiz"; | ||||||||
| import { ChapterDef } from "@/types"; | ||||||||
| import { logger } from "@/utils/logger"; | ||||||||
| import { UserContext } from "@/context/UserContextProvider"; | ||||||||
|
|
@@ -124,6 +124,7 @@ export const QuizContext = React.createContext<{ | |||||||
| threshold: number | null; | ||||||||
| answerQuestion: (value: AnswerWithQuestionId) => Promise<boolean>; | ||||||||
| uploadFiles: (questionId: string, files: File[]) => Promise<boolean>; | ||||||||
| removeFile: (questionId: string, fileName: string) => Promise<boolean>; | ||||||||
| getAnswers: (questionId: string) => Answer[]; | ||||||||
| getCorrectAnswer: (questionId: string) => string | undefined; | ||||||||
| submissionErrored: (questionId: string) => boolean | string; | ||||||||
|
|
@@ -145,6 +146,7 @@ export const QuizContext = React.createContext<{ | |||||||
| threshold: null, | ||||||||
| answerQuestion: async () => false, | ||||||||
| uploadFiles: async () => false, | ||||||||
| removeFile: async () => false, | ||||||||
| getAnswers: () => [], | ||||||||
| getCorrectAnswer: () => undefined, | ||||||||
| submissionErrored: () => false, | ||||||||
|
|
@@ -204,7 +206,7 @@ export const QuizContextProvider = ({ | |||||||
| type: "ANSWER", | ||||||||
| value: { | ||||||||
| questionId, | ||||||||
| answer, | ||||||||
| answer: postResult.storedAnswer, | ||||||||
| ...postResult | ||||||||
| } | ||||||||
| }); | ||||||||
|
|
@@ -263,6 +265,40 @@ export const QuizContextProvider = ({ | |||||||
| }, | ||||||||
| [user, bookId, quizReducer, answerQuestion, userGroup, t]); | ||||||||
|
|
||||||||
| const removeFile = React.useCallback( | ||||||||
| async(questionId: string, fileName: string): Promise<boolean> => { | ||||||||
| if (!user) { | ||||||||
| quizReducer({ | ||||||||
| type: "ERROR", | ||||||||
| value: {questionId, error: t("quiz.not-logged-in")}}); | ||||||||
| return false; | ||||||||
| } | ||||||||
| const groupId = userGroup !== null ? await getGroupId(userGroup, bookId) : null; | ||||||||
| if (userGroup && groupId === null) { | ||||||||
| quizReducer({ | ||||||||
| type: "ERROR", | ||||||||
| value: {questionId, error: t("quiz.invalid-group")}}); | ||||||||
| return false; | ||||||||
| } | ||||||||
|
|
||||||||
| const answer = await removeUserFile({ | ||||||||
| accessToken: user.accessToken, bookId, questionId, groupId, fileName}); | ||||||||
| if (answer === null) { | ||||||||
|
||||||||
| if (answer === null) { | |
| if (answer === null) { | |
| quizReducer({type: "ERROR", value: {questionId}}); |
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closing brace has inconsistent indentation with 7 spaces instead of 8 (like the opening brace on line 289). This should be aligned properly for consistency.
| }); | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| import type { NextConfig } from "next"; | ||
|
|
||
| const nextConfig: NextConfig = { | ||
| /* config options here */ | ||
| }; | ||
|
|
||
| export default nextConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition
question.type !== "uploads"only checks for the plural form, but according to the codebase, upload questions can have type "upload" (singular) or "uploads" (plural). This means the file merging logic won't work for singular "upload" type questions. The condition should bequestion.type !== "uploads" && question.type !== "upload"or use!question.type.startsWith("upload")to handle both cases.