This commit is contained in:
Leo Wen
2025-12-06 17:06:46 -08:00
parent d848595488
commit d0cd39434f
2 changed files with 391 additions and 346 deletions

View File

@@ -6,83 +6,89 @@ import { prisma } from "..";
const router = Router(); const router = Router();
router.get("/", async (req, res) => { router.get("/", async (req, res) => {
if (req.files === null || req.files === undefined) if (req.files === null || req.files === undefined)
return res.status(400).json({ msg: "No file uploaded" }); return res.status(400).json({ msg: "No file uploaded" });
if (req.files.upload === null) if (req.files.upload === null)
return res.status(400).json({ msg: "No file uploaded" }); return res.status(400).json({ msg: "No file uploaded" });
if (Array.isArray(req.files.upload)) if (Array.isArray(req.files.upload))
return res.status(400).json({ msg: "Multiple files uploaded" }); return res.status(400).json({ msg: "Multiple files uploaded" });
const file = req.files.upload as fileUpload.UploadedFile; const file = req.files.upload as fileUpload.UploadedFile;
if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype)) return res.status(400).json({ msg: "Invalid file type" }); if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype))
if (file.size > 5_000_000) { return res.status(400).json({ msg: "Invalid file type" });
return res.status(400).json({ msg: "File too large (max 5MB)" }); if (file.size > 5_000_000) {
return res.status(400).json({ msg: "File too large (max 5MB)" });
}
// Add error handling for missing required fields
if (!req.body.schoolName || !req.body.className || !req.body.professor) {
return res.status(400).json({ msg: "Missing required fields" });
}
// Add proper error handling for file operations
try {
let school = await prisma.school.findUnique({
where: { name: req.body.schoolName },
});
if (!school) {
return res.status(404).json({ msg: "School not found" });
} }
let classRecord = await prisma.class.findFirst({
// Change findUnique to findFirst
where: {
className: req.body.className,
schoolId: school.id,
},
});
// Add error handling for missing required fields if (!classRecord) {
if (!req.body.schoolName || !req.body.className || !req.body.professor) { return res.status(404).json({ msg: "Class not found" });
return res.status(400).json({ msg: "Missing required fields" });
} }
// Add proper error handling for file operations let professorObject = await prisma.professor.findUnique({
try { where: { name: req.body.professor },
let school = await prisma.school.findUnique({ });
where: { name: req.body.schoolName },
});
if (!school) { if (!professorObject) {
return res.status(404).json({ msg: "School not found" }); professorObject = await prisma.professor.create({
} data: {
name: req.body.professor,
let classRecord = await prisma.class.findFirst({ // Change findUnique to findFirst schoolId: school.id,
where: { },
className: req.body.className, });
schoolId: school.id,
},
});
if (!classRecord) {
return res.status(404).json({ msg: "Class not found" });
}
let professorObject = await prisma.professor.findUnique({
where: { name: req.body.professor },
});
if (!professorObject) {
professorObject = await prisma.professor.create({
data: {
name: req.body.professor,
schoolId: school.id,
},
});
}
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength ? parseInt(req.body.classLength, 10) : undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
professorObject: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error('Error creating syllabus:', error);
if (error instanceof Error) {
return res.status(500).json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
} }
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
professor: req.body.professor, // Required string field
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength
? parseInt(req.body.classLength, 10)
: undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
Professor: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error("Error creating syllabus:", error);
if (error instanceof Error) {
return res
.status(500)
.json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
}
}); });
export default router; export default router;

View File

@@ -5,356 +5,395 @@ import fileUpload from "express-fileupload";
import http from "node:http"; import http from "node:http";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import mime from "mime-types"; import mime from "mime-types";
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import argon2 from 'argon2'; import argon2 from "argon2";
import { idText } from "typescript"; import { idText } from "typescript";
import 'dotenv/config'; import "dotenv/config";
import { jwtMiddleware } from "./authMiddleware"; import { jwtMiddleware } from "./authMiddleware";
const app = express(); const app = express();
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
const prisma = new PrismaClient(); export const prisma = new PrismaClient();
app.use(cors({ app.use(
cors({
origin: "*", // TODO change this to the actual domain origin: "*", // TODO change this to the actual domain
})) }),
);
app.use(express.json()); app.use(express.json());
app.use(fileUpload()); app.use(fileUpload());
app.use('/files', express.static('syllabi')); app.use("/files", express.static("syllabi"));
app.post("/create", async (req, res) => { app.post("/create", async (req, res) => {
console.debug(req.body); console.debug(req.body);
if (req.files === null || req.files === undefined) if (req.files === null || req.files === undefined)
return res.status(400).json({ msg: "No file uploaded" }); return res.status(400).json({ msg: "No file uploaded" });
if (req.files.upload === null) if (req.files.upload === null)
return res.status(400).json({ msg: "No file uploaded" }); return res.status(400).json({ msg: "No file uploaded" });
if (Array.isArray(req.files.upload)) if (Array.isArray(req.files.upload))
return res.status(400).json({ msg: "Multiple files uploaded" }); return res.status(400).json({ msg: "Multiple files uploaded" });
const file = req.files.upload as fileUpload.UploadedFile; const file = req.files.upload as fileUpload.UploadedFile;
if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype)) return res.status(400).json({ msg: "Invalid file type" }); if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype))
if (file.size > 5_000_000) { return res.status(400).json({ msg: "Invalid file type" });
return res.status(400).json({ msg: "File too large (max 5MB)" }); if (file.size > 5_000_000) {
return res.status(400).json({ msg: "File too large (max 5MB)" });
}
// Add error handling for missing required fields
if (!req.body.schoolName || !req.body.className || !req.body.professor) {
return res.status(400).json({ msg: "Missing required fields" });
}
// Add proper error handling for file operations
try {
let school = await prisma.school.findUnique({
where: { name: req.body.schoolName },
});
if (!school) {
return res.status(404).json({ msg: "School not found" });
} }
let classRecord = await prisma.class.findFirst({
// Change findUnique to findFirst
where: {
className: req.body.className,
schoolId: school.id,
},
});
// Add error handling for missing required fields if (!classRecord) {
if (!req.body.schoolName || !req.body.className || !req.body.professor) { return res.status(404).json({ msg: "Class not found" });
return res.status(400).json({ msg: "Missing required fields" });
} }
// Add proper error handling for file operations let professorObject = await prisma.professor.findUnique({
try { where: { name: req.body.professor },
let school = await prisma.school.findUnique({ });
where: { name: req.body.schoolName },
});
if (!school) { if (!professorObject) {
return res.status(404).json({ msg: "School not found" }); professorObject = await prisma.professor.create({
} data: {
name: req.body.professor,
let classRecord = await prisma.class.findFirst({ // Change findUnique to findFirst schoolId: school.id,
where: { },
className: req.body.className, });
schoolId: school.id,
},
});
if (!classRecord) {
return res.status(404).json({ msg: "Class not found" });
}
let professorObject = await prisma.professor.findUnique({
where: { name: req.body.professor },
});
if (!professorObject) {
professorObject = await prisma.professor.create({
data: {
name: req.body.professor,
schoolId: school.id,
},
});
}
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength ? parseInt(req.body.classLength, 10) : undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
professorObject: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error('Error creating syllabus:', error);
if (error instanceof Error) {
return res.status(500).json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
} }
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
professor: req.body.professor, // Required string field
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength
? parseInt(req.body.classLength, 10)
: undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
Professor: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error("Error creating syllabus:", error);
if (error instanceof Error) {
return res
.status(500)
.json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
}
}); });
app.get("/schools", async (req, res) => { app.get("/schools", async (req, res) => {
let colleges = await prisma.school.findMany({}); let colleges = await prisma.school.findMany({});
res.json(colleges); res.json(colleges);
}) });
app.post("/search/class/", async (req, res) => { app.post("/search/class/", async (req, res) => {
let take = req.body.take || 10; let take = req.body.take || 10;
let skip = req.body.skip || 0; let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" }); if (
(await prisma.school.findUnique({
where: { name: req.query.s as string },
})) === null
)
return res.status(404).json({ msg: "School not found" });
let classes = await prisma.class.findMany({ let classes = await prisma.class.findMany({
where: { where: {
school: { school: {
name: req.query.s as string, name: req.query.s as string,
}, },
}, },
}); });
let results = classes;
let results = classes; if (req.query.q) {
results = fuzzysort
.go(req.query.q as string, classes, {
keys: ["className", "fullClassName"],
})
.map((i) => i.obj);
}
if (req.query.q) { res.json(results.slice(skip, skip + take));
results = fuzzysort.go(req.query.q as string, classes, { keys: ["className", "fullClassName"] }).map(i => i.obj);
}
res.json(results.slice(skip, skip + take));
}); });
app.post("/search/professor/", async (req, res) => { app.post("/search/professor/", async (req, res) => {
let take = req.body.take || 10; let take = req.body.take || 10;
let skip = req.body.skip || 0; let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" }); if (
(await prisma.school.findUnique({
where: { name: req.query.s as string },
})) === null
)
return res.status(404).json({ msg: "School not found" });
let professors = await prisma.professor.findMany({ let professors = await prisma.professor.findMany({
where: { where: {
school: { school: {
name: req.query.s as string, name: req.query.s as string,
}, },
}, },
}); });
let results = professors; let results = professors;
if (req.query.q) { if (req.query.q) {
results = fuzzysort.go(req.query.q as string, professors, { keys: ["name"] }).map(i => i.obj); results = fuzzysort
} .go(req.query.q as string, professors, { keys: ["name"] })
.map((i) => i.obj);
}
res.json(results.slice(skip, skip + take)); res.json(results.slice(skip, skip + take));
}); });
app.get("/professor/:id", async (req, res) => { app.get("/professor/:id", async (req, res) => {
let professor = await prisma.professor.findUnique({ let professor = await prisma.professor.findUnique({
where: { id: req.params.id }, where: { id: req.params.id },
include: {
school: true,
syllabi: {
include: { include: {
school: true, class: true,
syllabi: {
include: {
class: true,
},
},
}, },
}); },
if (professor === null) return res.status(404).json({ msg: "Professor not found" }); },
return res.json({ });
name: professor.name, if (professor === null)
school: professor.school?.name, return res.status(404).json({ msg: "Professor not found" });
syllabi: professor.syllabi.map(i => { return res.json({
return { name: professor.name,
id: i.id, school: professor.school?.name,
className: i.class?.className, syllabi: professor.syllabi.map((i) => {
fullClassName: i.fullClassName, return {
textbookCost: i.textbookCost, id: i.id,
classLength: i.classLength, className: i.class?.className,
description: i.description, fullClassName: i.fullClassName,
dateCreated: i.dateCreated.toISOString(), textbookCost: i.textbookCost,
fileName: `${i.id}.${mime.extension(i.mimeType)}`, classLength: i.classLength,
}; description: i.description,
}), dateCreated: i.dateCreated.toISOString(),
}); fileName: `${i.id}.${mime.extension(i.mimeType)}`,
};
}),
});
}); });
app.get("/syllabus/:id", async (req, res) => { app.get("/syllabus/:id", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({ let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id}, where: { id: req.params.id },
include: {
class: {
include: { include: {
class: { discipline: true,
include: {
discipline: true,
},
},
professorObject: true,
}, },
}); },
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" }); Professor: true,
return res.json({ },
className: syllabus.class?.className, });
description: syllabus.description, if (syllabus === null)
fullClassName: syllabus.fullClassName, return res.status(404).json({ msg: "Syllabus not found" });
textbookCost: syllabus.textbookCost, return res.json({
content: syllabus.content, className: syllabus.class?.className,
classLength: syllabus.classLength, description: syllabus.description,
professor: syllabus.professorObject?.name, fullClassName: syllabus.fullClassName,
dateCreated: syllabus.dateCreated.toISOString(), textbookCost: syllabus.textbookCost,
fileName: `${syllabus.id}.${mime.extension(syllabus.mimeType)}`, content: syllabus.content,
class: { classLength: syllabus.classLength,
className: syllabus.class?.className ?? '', professor: syllabus.Professor?.[0]?.name,
fullClassName: syllabus.class?.fullClassName ?? '', dateCreated: syllabus.dateCreated.toISOString(),
discipline: syllabus.class?.discipline?.name ?? '', fileName: `${syllabus.id}.${mime.extension(syllabus.mimeType)}`,
}, class: {
}); className: syllabus.class?.className ?? "",
fullClassName: syllabus.class?.fullClassName ?? "",
discipline: syllabus.class?.discipline?.name ?? "",
},
});
}); });
app.get("/syllabus/:id/download", async (req, res) => { app.get("/syllabus/:id/download", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({ let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id, reviewed: true }, where: { id: req.params.id, reviewed: true },
include: { include: {
class: true, class: true,
}, },
}); });
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" }); if (syllabus === null)
res.download(`./syllabi/${syllabus.id}.${mime.extension(syllabus.mimeType)}`); return res.status(404).json({ msg: "Syllabus not found" });
res.download(`./syllabi/${syllabus.id}.${mime.extension(syllabus.mimeType)}`);
}); });
app.get("/search/", async (req, res) => { app.get("/search/", async (req, res) => {
try { try {
let syllabi = await prisma.syllabus.findMany({ let syllabi = await prisma.syllabus.findMany({
include: { include: {
class: { class: {
include: { include: {
discipline: true, discipline: true,
}, },
}, },
professorObject: true, Professor: true,
school: true, school: true,
}, },
}); });
let results = syllabi; let results = syllabi;
if (req.query.s) { if (req.query.s) {
results = results.filter(i => i.school?.name === req.query.s); results = results.filter((i) => i.school?.name === req.query.s);
}
if (req.query.c) {
results = results.filter(i => i.class?.className === req.query.c);
}
if (req.query.p) {
results = results.filter(i => i.professorObject?.name === req.query.p);
}
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, results, { keys: ["class.className", "professorObject.name", "class.fullClassName"] }).map(i => i.obj);
}
let take = req.query.take as unknown as number || 10;
let skip = req.query.skip as unknown as number || 0;
res.json(results.map(i => {
return {
id: i.id,
className: i.class?.className,
professor: i.professorObject?.name ?? '',
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
description: i.description,
classLength: i.classLength,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
class: {
className: i.class?.className ?? '',
fullClassName: i.class?.fullClassName ?? '',
discipline: i.class?.discipline?.name ?? '',
},
professorId: i.professorObject?.id ?? '',
school: {
name: i.school?.name ?? '',
id: i.school?.id ?? '',
fullName: i.school?.fullName ?? '',
}
};
}).slice(skip).slice(0, take));
} catch (e) {
console.error(e);
res.status(500).json({ msg: "Internal server error" });
} }
if (req.query.c) {
results = results.filter((i) => i.class?.className === req.query.c);
}
if (req.query.p) {
results = results.filter((i) => i.Professor?.[0]?.name === req.query.p);
}
if (req.query.q) {
results = fuzzysort
.go(req.query.q as string, results, {
keys: ["class.className", "Professor.0.name", "class.fullClassName"],
})
.map((i) => i.obj);
}
let take = (req.query.take as unknown as number) || 10;
let skip = (req.query.skip as unknown as number) || 0;
res.json(
results
.map((i) => {
return {
id: i.id,
className: i.class?.className,
professor: i.Professor?.[0]?.name ?? "",
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
description: i.description,
classLength: i.classLength,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
class: {
className: i.class?.className ?? "",
fullClassName: i.class?.fullClassName ?? "",
discipline: i.class?.discipline?.name ?? "",
},
professorId: i.Professor?.[0]?.id ?? "",
school: {
name: i.school?.name ?? "",
id: i.school?.id ?? "",
fullName: i.school?.fullName ?? "",
},
};
})
.slice(skip)
.slice(0, take),
);
} catch (e) {
console.error(e);
res.status(500).json({ msg: "Internal server error" });
}
}); });
app.post("/report/:id", async (req, res) => { app.post("/report/:id", async (req, res) => {
try { try {
const { reportType, reportTitle, reportBody, reportBy } = req.body; const { reportType, reportTitle, reportBody, reportBy } = req.body;
const syllabusId = req.params.id; const syllabusId = req.params.id;
// Check if the syllabus exists // Check if the syllabus exists
const syllabus = await prisma.syllabus.findUnique({ where: { id: syllabusId } }); const syllabus = await prisma.syllabus.findUnique({
if (!syllabus) { where: { id: syllabusId },
return res.status(404).json({ msg: "Syllabus not found" }); });
} if (!syllabus) {
return res.status(404).json({ msg: "Syllabus not found" });
// Create the report
const report = await prisma.report.create({
data: {
reportType,
reportTitle,
reportBody,
reportBy,
syllabusId,
},
});
res.json(report);
} catch (e) {
console.error(e);
res.status(500).json({ msg: `Internal Server Error ${e}` });
} }
})
// Create the report
const report = await prisma.report.create({
data: {
reportType,
reportTitle,
reportBody,
reportBy,
syllabusId,
},
});
res.json(report);
} catch (e) {
console.error(e);
res.status(500).json({ msg: `Internal Server Error ${e}` });
}
});
app.post("/moderator/login", async (req, res) => { app.post("/moderator/login", async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ msg: "Missing username or password" });
}
const moderator = await prisma.moderator.findUnique({ where: { username } }); if (!username || !password) {
if (!moderator) { return res.status(400).json({ msg: "Missing username or password" });
return res.status(401).json({ msg: "Invalid username or password" }); }
}
const valid = await argon2.verify(moderator.password, password); const moderator = await prisma.moderator.findUnique({ where: { username } });
if (!valid) { if (!moderator) {
return res.status(401).json({ msg: "Invalid username or password" }); return res.status(401).json({ msg: "Invalid username or password" });
} }
const token = jwt.sign({ id: moderator.id }, process.env.SECRET_KEY as string, { expiresIn: '2h' }); const valid = await argon2.verify(moderator.password, password);
if (!valid) {
res.json({ token }); return res.status(401).json({ msg: "Invalid username or password" });
return; }
const token = jwt.sign(
{ id: moderator.id },
process.env.SECRET_KEY as string,
{ expiresIn: "2h" },
);
res.json({ token });
return;
}); });
app.post("/moderator/flag", jwtMiddleware, async (req, res) => { app.post("/moderator/flag", jwtMiddleware, async (req, res) => {
res.json(req.body); res.json(req.body);
}); });
await new Promise<void>((resolve) => await new Promise<void>((resolve) =>
httpServer.listen({ port: 4000, host: "0.0.0.0" }, resolve) httpServer.listen({ port: 4000, host: "0.0.0.0" }, resolve),
); );