From cdc75c37a083be29d7b60235d304d33b37b58f5b Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:25:24 +0100 Subject: [PATCH] submission endpoints --- src/classes/MainServer.ts | 4 +- src/models/SubmissionModel.ts | 62 ++++++ .../classroom/activities/submissions/index.ts | 206 ++++++++++++++++++ 3 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 src/models/SubmissionModel.ts create mode 100644 src/routes/classroom/activities/submissions/index.ts diff --git a/src/classes/MainServer.ts b/src/classes/MainServer.ts index b1aa572..59c73d8 100644 --- a/src/classes/MainServer.ts +++ b/src/classes/MainServer.ts @@ -6,6 +6,7 @@ import { authLoginRoute } from '../routes/auth/login'; import { authRegisterRoute } from '../routes/auth/register'; import { classroomsRoute } from '../routes/classroom'; import { classroomActivitiesRoute } from '../routes/classroom/activities'; +import { classroomActivitySubmissionsRoute } from '../routes/classroom/activities/submissions'; import { userRoute } from '../routes/user'; export class MainServer { @@ -25,7 +26,8 @@ export class MainServer { .use(authRegisterRoute) .use(userRoute) .use(classroomsRoute) - .use(classroomActivitiesRoute); + .use(classroomActivitiesRoute) + .use(classroomActivitySubmissionsRoute); if (process.env['ENABLE_SWAGGER'] === 'true') { console.log('Swagger enabled.'); diff --git a/src/models/SubmissionModel.ts b/src/models/SubmissionModel.ts new file mode 100644 index 0000000..e8d08ef --- /dev/null +++ b/src/models/SubmissionModel.ts @@ -0,0 +1,62 @@ +import { Schema, Types, model } from 'mongoose'; + +export interface ISubmission { + id: string; + activity: Types.ObjectId; + user: Types.ObjectId; + files: Array<{ + name: string; + url: string; + }>; + submittedAt: string; + comment?: string; +} + +const fileSchema = new Schema( + { + name: { + type: String, + required: true + }, + url: { + type: String, + required: true + } + }, + { _id: false } +); + +const submissionSchema = new Schema( + { + activity: { + type: Schema.Types.ObjectId, + ref: 'activity', + required: true, + index: true + }, + user: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true, + index: true + }, + files: { + type: [fileSchema], + required: true, + default: [] + }, + submittedAt: { + type: String, + required: true + }, + comment: { + type: String, + required: false + } + }, + { + timestamps: true + } +); + +export const SubmissionModel = model('Submission', submissionSchema); diff --git a/src/routes/classroom/activities/submissions/index.ts b/src/routes/classroom/activities/submissions/index.ts new file mode 100644 index 0000000..e4daec8 --- /dev/null +++ b/src/routes/classroom/activities/submissions/index.ts @@ -0,0 +1,206 @@ +import { Elysia, t } from 'elysia'; +import { minio } from '../../../..'; +import { ActivityModel } from '../../../../models/ActivityModel'; +import { SubmissionModel } from '../../../../models/SubmissionModel'; +import { UserModel } from '../../../../models/UserModel'; +import { headersPlugin } from '../../../../plugins/headers'; + +export const classroomActivitySubmissionsRoute = new Elysia({ + name: 'routes:classroomActivitySubmissionsRoute', + prefix: '/classrooms/:classroomId/activities/:activityId/submissions' +}) + .use(headersPlugin) + .get( + '/', + async ({ params: { classroomId, activityId }, status, token }) => { + const user = await UserModel.findOne({ token }, { __v: 0 }) + .lean() + .exec() + .catch(() => null); + + if (!user) return status(400, 'Bad Request'); + if (!user.classroomIds?.some((cId) => cId.toString() === classroomId)) return status(403, 'Forbidden'); + + const activity = await ActivityModel.findOne( + { + _id: activityId, + classroom: classroomId + }, + { __v: 0 } + ) + .lean() + .exec() + .catch(() => null); + + if (!activity) return status(404, 'Activity not found'); + + const submission = await SubmissionModel.findOne( + { + activity: activityId, + user: user._id + }, + { __v: 0 } + ) + .lean() + .exec() + .catch(() => null); + + if (!submission) return status(400, 'Bad Request'); + + return ({ + ...submission, + id: submission._id.toString(), + _id: undefined + }) + }, + { + params: t.Object({ + classroomId: t.String({ minLength: 1 }), + activityId: t.String({ minLength: 1 }) + }) + } + ) + .get( + '/all', + async ({ params: { classroomId, activityId }, status, token }) => { + const user = await UserModel.findOne({ token }, { __v: 0 }) + .lean() + .exec() + .catch(() => null); + + if (!user) return status(400, 'Bad Request'); + + const activity = await ActivityModel.findOne( + { + _id: activityId, + classroom: classroomId + }, + { __v: 0 } + ) + .lean() + .exec() + .catch(() => null); + + if (!activity) return status(404, 'Activity not found'); + + if (user._id.toString() !== activity.owner.toString()) return status(403, 'Forbidden'); + + const submissions = await SubmissionModel.find( + { + activity: activityId, + }, + { __v: 0 } + ) + .populate('user', { __v: 0, token: 0 }) + .lean() + .exec() + .catch(() => null); + + if (!submissions) return status(400, 'Bad Request'); + + return submissions.map((submission) => ({ + ...submission, + id: submission._id.toString(), + _id: undefined, + user: { + ...submission.user, + id: submission.user._id.toString(), + _id: undefined + } + })); + }, + { + params: t.Object({ + classroomId: t.String({ minLength: 1 }), + activityId: t.String({ minLength: 1 }) + }) + } + ) + .post( + '/', + async ({ body, params: { classroomId, activityId }, status, token }) => { + if (!body) return status(400, 'Bad Request'); + + const user = await UserModel.findOne({ token }, { __v: 0 }) + .lean() + .exec() + .catch(() => null); + + if (!user) return status(400, 'Bad Request'); + if (!user.classroomIds?.some((cId) => cId.toString() === classroomId)) return status(403, 'Forbidden'); + + const activity = await ActivityModel.findOne( + { + _id: activityId, + classroom: classroomId + }, + { __v: 0 } + ) + .lean() + .exec() + .catch(() => null); + + if (!activity) return status(404, 'Activity not found'); + + const { filename, comment } = body; + + const url = await minio.presignedPutObject( + 'submissions', + `${user._id.toString()}/${activityId}/${filename}`, + 1 * 60 * 60 + ); + + if (!url) return status(400, 'Error'); + + let submission = await SubmissionModel.findOne( + { + activity: activityId, + user: user._id + }, + { __v: 0 } + ) + .exec() + .catch(() => null); + + if (!submission) { + submission = new SubmissionModel({ + activity: activityId, + user: user._id, + files: [], + comment: comment, + submittedAt: new Date().toISOString() + }); + } + + submission.files.push({ + name: filename, + url: `${user._id.toString()}/${activityId}/${filename}` + }); + + if (comment !== undefined) { + submission.comment = comment; + } + + const save = await submission.save().catch(() => null); + + if (!save) return status(400, 'Bad Request'); + + return { url }; + }, + { + parse: 'json', + params: t.Object({ + classroomId: t.String({ minLength: 1 }), + activityId: t.String({ minLength: 1 }) + }), + body: t.Object( + { + filename: t.String({ minLength: 1 }), + comment: t.Optional(t.String({ minLength: 1 })) + }, + { + additionalProperties: false + } + ) + } + );