From 949415373e07d1260d4f6dbfca3f94844f68ab4d Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 01:30:31 -0400 Subject: [PATCH 01/33] refactor file structure --- server/dbDefs.ts | 138 ---- server/dbInit.test.ts | 181 ----- server/dbInit.ts | 627 ------------------ server/endpoints.ts | 12 +- server/endpoints/AdminChart.ts | 313 --------- server/endpoints/Search.test.ts | 195 ------ server/endpoints/{ => admin}/AdminActions.ts | 85 ++- server/endpoints/admin/AdminChart.ts | 362 ++++++++++ server/endpoints/{ => auth}/Auth.ts | 34 +- server/endpoints/{ => profile}/Profile.ts | 6 +- server/endpoints/{ => review}/Review.ts | 134 +++- server/endpoints/{ => search}/Search.ts | 79 ++- .../endpoints/{ => test}/AdminActions.test.ts | 52 +- .../endpoints/{ => test}/AdminChart.test.ts | 147 +++- server/endpoints/{ => test}/Auth.test.ts | 50 +- server/endpoints/{ => test}/Profile.test.ts | 154 +++-- server/endpoints/{ => test}/Review.test.ts | 167 +++-- server/endpoints/test/Search.test.ts | 313 +++++++++ server/endpoints/{ => test}/TestServer.ts | 62 +- server/endpoints/{ => utils}/utils.ts | 6 +- server/server.ts | 78 ++- server/tsconfig.json | 16 +- 22 files changed, 1430 insertions(+), 1781 deletions(-) delete mode 100644 server/dbDefs.ts delete mode 100644 server/dbInit.test.ts delete mode 100644 server/dbInit.ts delete mode 100644 server/endpoints/AdminChart.ts delete mode 100644 server/endpoints/Search.test.ts rename server/endpoints/{ => admin}/AdminActions.ts (78%) create mode 100644 server/endpoints/admin/AdminChart.ts rename server/endpoints/{ => auth}/Auth.ts (60%) rename server/endpoints/{ => profile}/Profile.ts (95%) rename server/endpoints/{ => review}/Review.ts (69%) rename server/endpoints/{ => search}/Search.ts (78%) rename server/endpoints/{ => test}/AdminActions.test.ts (65%) rename server/endpoints/{ => test}/AdminChart.test.ts (58%) rename server/endpoints/{ => test}/Auth.test.ts (52%) rename server/endpoints/{ => test}/Profile.test.ts (63%) rename server/endpoints/{ => test}/Review.test.ts (53%) create mode 100644 server/endpoints/test/Search.test.ts rename server/endpoints/{ => test}/TestServer.ts (51%) rename server/endpoints/{ => utils}/utils.ts (94%) diff --git a/server/dbDefs.ts b/server/dbDefs.ts deleted file mode 100644 index 9f02ceaac..000000000 --- a/server/dbDefs.ts +++ /dev/null @@ -1,138 +0,0 @@ -import mongoose, { Schema } from "mongoose"; -import { Class, Student, Subject, Review, Professor } from "common"; - -/* - - Database definitions file. Defines all collections in the local database, - with collection attributes, types, and required fields. - - Used by both the Server and Client to define local and minimongo database - structures. - -*/ - -/* # Classes collection. - # Holds data about each class in the course roster. -*/ -export interface ClassDocument extends mongoose.Document, Class { - _id: string; -} - -const ClassSchema = new Schema({ - _id: { type: String }, // overwritten _id field to play nice with our old db - classSub: { type: String }, // subject, like "PHIL" or "CS" - classNum: { type: String }, // course number, like 1110 - classTitle: { type: String }, // class title, like 'Introduction to Algorithms' - classPrereq: { type: [String], required: false }, // list of pre-req classes, a string of Classes _id. - crossList: { type: [String], required: false }, // list of classes that are crosslisted with this one, a string of Classes _id. - classFull: { type: String }, // full class title to search by, formated as 'classSub classNum: classTitle' - classSems: { type: [String] }, // list of semesters this class was offered, like ['FA17', 'FA16'] - classProfessors: { type: [String] }, // list of professors that have taught the course over past semesters - classRating: { type: Number }, // the average class rating from reviews - classWorkload: { type: Number }, // the average workload rating from reviews - classDifficulty: { type: Number }, // the average difficulty rating from reviews - -}); - -export const Classes = mongoose.model("classes", ClassSchema); -/* # Users collection. - # Holds data about each user. Data is collected via Cornell net-id login. -*/ - -export interface StudentDocument extends mongoose.Document, Student { - readonly _id: string; -} - -const StudentSchema = new Schema({ - _id: { type: String }, // overwritten _id field to play nice with our old db - firstName: { type: String }, // user first name - lastName: { type: String }, // user last name - netId: { type: String }, // user netId - affiliation: { type: String }, // user affliaition, like ENG or A&S - token: { type: String }, // random token generated during login process - privilege: { type: String }, // user privilege level - reviews: { type: [String] }, // the reviews that this user has posted. - likedReviews: { type: [String] }, -}); -export const Students = mongoose.model("students", StudentSchema); - -/* # Subjects Collection - # List of all course subject groups and their full text names - # ex: CS -> Computer Science -*/ - -export interface SubjectDocument extends mongoose.Document, Subject { - readonly _id: string; -} - -const SubjectSchema = new Schema({ - _id: { type: String }, // overwritten _id field to play nice with our old db - subShort: { type: String }, // subject, like "PHIL" or "CS" - subFull: { type: String }, // subject full name, like 'Computer Science' -}); -export const Subjects = mongoose.model("subjects", SubjectSchema); - -/* # Reviews Collection. - # Stores each review inputted by a user. Linked with the course that was - # reviewed via a mapping with on class, which holds the _id attribute of - # the class from the Classes collection -*/ - -export interface ReviewDocument extends mongoose.Document, Review { - readonly _id: string; -} - -const ReviewSchema = new Schema({ - _id: { type: String }, // overwritten _id field to play nice with our old db - user: { type: String, required: false }, // user who wrote this review, a Users _id - text: { type: String, required: false }, // text from the review - difficulty: { type: Number }, // difficulty measure from the review - rating: { type: Number }, // quality measure from the review - workload: { type: Number }, // quality measure from the review - class: { type: String }, // class the review was for, a Classes _id - date: { type: Date }, // date/timestamp the review was submited - visible: { type: Number }, // visibility flag - 1 if visible to users, 0 if only visible to admin - reported: { type: Number }, // reported flag - 1 if review was reported, 0 otherwise - professors: { type: [String] }, // list of professors that have thought the course over past semesters - likes: { type: Number, min: 0 }, // number of likes a review has - likedBy: { type: [String] }, - isCovid: { type: Boolean }, - grade: { type: String }, - major: { type: [String] }, - // The following was a temporary field used to keep track of reviews for a contest - // The full functional code for counting reviews can be found on the following branch: - // review-counting-feature -}); -export const Reviews = mongoose.model("reviews", ReviewSchema); - -/* # Professors collection. - # Holds data about each professor. -*/ - -export interface ProfessorDocument extends mongoose.Document, Professor { - readonly _id: string; -} - -const ProfessorSchema = new Schema({ - _id: { type: String }, // mongo-generated random id for this document - fullName: { type: String }, // the full name of the professor - courses: { type: [String] }, // a list of the ids all the courses - major: { type: String }, // professor affliation by probable major -}); -export const Professors = mongoose.model("professors", ProfessorSchema); - -/* # Validation Collection. - # Stores passwords and other sensitive application keys. - # Must be manually populated with data when the app is initialized. -*/ -const ValidationSchema = new Schema({ - _id: { type: String }, // mongo-generated random id for this document - adminPass: { type: String }, // admin password to validate against -}); - -interface ValidationDocument extends mongoose.Document { - _id: string; - adminPass?: string; -} - -export const Validation = mongoose.model("validation", ValidationSchema); diff --git a/server/dbInit.test.ts b/server/dbInit.test.ts deleted file mode 100644 index d07f77a2b..000000000 --- a/server/dbInit.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Set up fake endpoints to query -import express from "express"; -import axios from "axios"; -import { MongoMemoryServer } from "mongodb-memory-server"; -import mongoose from "mongoose"; -import { Subjects, Classes, Professors } from "./dbDefs"; -import { fetchSubjects, fetchClassesForSubject, fetchAddCourses } from "./dbInit"; - -let testServer: MongoMemoryServer; -let serverCloseHandle; - -const testingPort = 27760; -const testingEndpoint = `http://localhost:${testingPort}/`; - -// Configure a mongo server and fake endpoints for the tests to use -beforeAll(async () => { - // get mongoose all set up - testServer = new MongoMemoryServer(); - const mongoUri = await testServer.getUri(); - await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }); - mongoose.set('useFindAndModify', false); - - await new Subjects({ - _id: "some id", - subShort: "gork", - subFull: "Study of Angry Fungi", - }).save(); - - await new Classes({ - _id: "some other id", - classSub: "gork", - classNum: "1110", - classTitle: "Introduction to Angry Fungi", - classFull: "gork 1110 Introduction to Angry Fungi", - classSems: ["FA19"], - classProfessors: ["Prof. Thraka"], - classRating: 5, - classWorkload: 5, - classDifficulty: 5, - }).save(); - - // We need to pretend to have access to a cornell classes endpoint - const app = express(); - serverCloseHandle = app.listen(testingPort); - - app.get("/hello", (req, res) => { - res.send("Hello world"); - }); - - // Fake subjects endpoint - app.get("/config/subjects.json", (req, res) => { - // simulate only having FA20 data. - // express did not allow me to include a "?" literal in the path for some strange reason - // Maybe fix in the future? - if (!req.originalUrl.includes("FA20")) { - res.send({ - status: "failure", - }); - } - - res.send({ - status: "success", - data: { - subjects: [ - { descr: "Study of Fungi", descrformal: "The Study of Fungi", value: "gork" }, - { descr: "Study of Space", descrformal: "The Study of Where No One has Gone Before", value: "fedn" }, - ], - }, - }); - }); - - // Fake classes endpoint - app.get("/search/classes.json", (req, res) => { - // simulate only having data for the gork subject. - // see above - if (!req.originalUrl.includes("gork")) { - res.send({ - status: "failure", - }); - } - - res.send({ - status: "success", - data: { - classes: [ - { - subject: "gork", - catalogNbr: "1110", - titleLong: "Introduction to Angry Fungi", - randoJunk: "Making sure this scauses no issues", - enrollGroups: [{ classSections: [{ ssrComponent: "LEC", meetings: [{ instructors: [{ firstName: "Prof.", lastName: "Thraka" }] }] }] }], - }, - { - junk: "nada", - subject: "gork", - catalogNbr: "2110", - titleLong: "Advanced Study of Angry Fungi", - enrollGroups: [{ - classSections: [ - { ssrComponent: "LEC", meetings: [{ instructors: [{ firstName: "Prof.", lastName: "Thraka" }, { firstName: "Prof.", lastName: "Urgok" }] }] }, - ], - }], - }, - ], - }, - }); - }); -}); - -afterAll(async () => { - await mongoose.disconnect(); - await testServer.stop(); - serverCloseHandle.close(); -}); - -describe('tests', () => { - it("dbInit-db-works", async () => { - expect((await Subjects.findOne({ subShort: "gork" })).subShort).toBe("gork"); - expect((await Classes.findOne({ classSub: "gork", classNum: "1110" })).classSub).toBe("gork"); - }); - - // Does the internal testing endpoint exist? - it("dbInit-test-endpoint-exists", async () => { - const response = await axios.get(`${testingEndpoint}hello`); - expect(response.data).toBe("Hello world"); - expect(response.data).not.toBe("Something the endpoint is not to return!"); - }); - - // Does fetching the subjects collection work as expected? - it("fetching-roster-works", async () => { - const response = await fetchSubjects(testingEndpoint, "FA20"); - expect(response.length).toBe(2); - expect(response[0].descrformal).toBe("The Study of Fungi"); - expect(response[0].value).toBe("gork"); - expect(response[1].value).toBe("fedn"); - - // No data for FA19! - const nil = await fetchSubjects(testingEndpoint, "FA19"); - expect(nil).toBeNull(); - }); - - // Does fetching the classes collection work as expected? - it("fetching-classes-by-subject-works", async () => { - const response = await fetchClassesForSubject(testingEndpoint, "FA20", { descrformal: "The Study of AngryFungi", value: "gork" }); - expect(response.length).toBe(2); - expect(response[0].subject).toBe("gork"); - expect(response[0].catalogNbr).toBe("1110"); - expect(response[0].titleLong).toBe("Introduction to Angry Fungi"); - expect(response[1].titleLong).toBe("Advanced Study of Angry Fungi"); - - // No fedn classes, only gork classes! - const nil = await fetchClassesForSubject(testingEndpoint, "FA20", { descrformal: "The Study of Where No One has Gone Before", value: "fedn" }); - expect(nil).toBeNull(); - }); - - it("full-scraping-works", async () => { - const worked = await fetchAddCourses(testingEndpoint, "FA20"); - expect(worked).toBe(true); - - // did it add the fedn subject? - expect((await Subjects.findOne({ subShort: "fedn" }).exec()).subFull).toBe("The Study of Where No One has Gone Before"); - - // did it update the semesters on gork 1110? - // notice the .lean(), which changes some of the internals of what mongo returns - const class1 = await Classes.findOne({ classSub: "gork", classNum: "1110" }).lean().exec(); - expect(class1.classSems).toStrictEqual(["FA19", "FA20"]); - - // did it add the gork 2110 Class? - const class2 = await Classes.findOne({ classSub: "gork", classNum: "2110" }).exec(); - expect(class2.classTitle).toBe("Advanced Study of Angry Fungi"); - - // Did it update the classes for the first professor - const prof1 = await Professors.findOne({ fullName: "Prof. Thraka" }).lean().exec(); - expect(prof1.courses).toContain(class1._id); - expect(prof1.courses).toContain(class2._id); - - // Did it add the second professor with the right class id? - const prof2 = await Professors.findOne({ fullName: "Prof. Urgok" }).lean().exec(); - expect(prof2.courses).toStrictEqual([class2._id]); - }); -}); diff --git a/server/dbInit.ts b/server/dbInit.ts deleted file mode 100644 index 1b7b78c88..000000000 --- a/server/dbInit.ts +++ /dev/null @@ -1,627 +0,0 @@ -import axios from 'axios'; -import shortid from 'shortid'; -import { Classes, Subjects, Professors } from './dbDefs'; - -export const defaultEndpoint = "https://classes.cornell.edu/api/2.0/"; - -// Represents a subject which is scraped -// Note: there's a load of additional information when we scrape it. -// It's not relevant, so we just ignore it for now. -export interface ScrapingSubject { - descrformal: string; // Subject description, e.g. "Asian American Studies" - value: string; // Subject code, e.g. "AAS" -} - -// This only exists for compatibility with the API -export interface ScrapingInstructor { - firstName: string; - lastName: string; -} - -// This only exists for compatibility with the API -export interface ScrapingMeeting { - instructors: ScrapingInstructor[]; -} - -// This only exists for compatibility with the API -export interface ScrapingClassSection { - ssrComponent: string; // i.e. LEC, SEM, DIS - meetings: ScrapingMeeting[]; -} - -// This only exists for compatibility with the API -export interface ScrapingEnrollGroup { - classSections: ScrapingClassSection[]; // what sections the class has -} - -// Represents a class which is scraped -// Note: there's a load of additional information when we scrape it. -// It's not relevant, so we just ignore it for now. -export interface ScrapingClass { - subject: string; // Short: e.g. "CS" - catalogNbr: string; // e.g. 1110 - titleLong: string; // long variant of a title e.g. "Introduction to Computing Using Python" - enrollGroups: ScrapingEnrollGroup[]; // specified by the API -} - -/* - * Fetch the class roster for a semester. - * Returns the class roster on success, or null if there was an error. - */ -export async function fetchSubjects(endpoint: string, semester: string): Promise { - const result = await axios.get(`${endpoint}config/subjects.json?roster=${semester}`, { timeout: 10000 }); - if (result.status !== 200 || result.data.status !== "success") { - console.log(`Error fetching ${semester} subjects! HTTP: ${result.statusText} SERV: ${result.data.status}`); - return null; - } - - return result.data.data.subjects; -} - -/* - * Fetch all the classes for that semester/subject combination - * Returns a list of classes on success, or null if there was an error. - */ -export async function fetchClassesForSubject(endpoint: string, semester: string, subject: ScrapingSubject): Promise { - const result = await axios.get(`${endpoint}search/classes.json?roster=${semester}&subject=${subject.value}`, { timeout: 10000 }); - if (result.status !== 200 || result.data.status !== "success") { - console.log(`Error fetching subject ${semester}-${subject.value} classes! HTTP: ${result.statusText} SERV: ${result.data.status}`); - return null; - } - - return result.data.data.classes; -} - -export function isInstructorEqual(a: ScrapingInstructor, b: ScrapingInstructor) { - return (a.firstName === b.firstName) && (a.lastName === b.lastName); -} - -/* - * Extract an array of professors from the terribly deeply nested gunk that the api returns - * There are guaranteed to be no duplicates! - */ -export function extractProfessors(clas: ScrapingClass): ScrapingInstructor[] { - const raw = clas.enrollGroups.map((e) => e.classSections.map((s) => s.meetings.map((m) => m.instructors))); - // flatmap does not work :( - const f1: ScrapingInstructor[][][] = []; - raw.forEach((r) => f1.push(...r)); - const f2: ScrapingInstructor[][] = []; - f1.forEach((r) => f2.push(...r)); - const f3: ScrapingInstructor[] = []; - f2.forEach((r) => f3.push(...r)); - - const nonDuplicates: ScrapingInstructor[] = []; - - f3.forEach((inst) => { - // check if there is another instructor in nonDuplicates already! - if (nonDuplicates.filter((i) => isInstructorEqual(i, inst)).length === 0) { - // push the instructor if not present - nonDuplicates.push(inst); - } - }); - - return nonDuplicates; -} - -/* - * Fetch the relevant classes, and add them to the collections - * Returns true on success, and false on failure. - */ -export async function fetchAddCourses(endpoint: string, semester: string): Promise { - const subjects = await fetchSubjects(endpoint, semester); - - const v1 = await Promise.all(subjects.map(async (subject) => { - const subjectIfExists = await Subjects.findOne({ subShort: subject.value.toLowerCase() }).exec(); - - if (!subjectIfExists) { - console.log(`Adding new subject: ${subject.value}`); - const res = await new Subjects({ - _id: shortid.generate(), - subShort: subject.value.toLowerCase(), - subFull: subject.descrformal, - }).save().catch((err) => { - console.log(err); - return null; - }); - - // db operation was not successful - if (!res) { - throw new Error(); - } - } - - return true; - })).catch((err) => null); - - if (!v1) { - console.log("Something went wrong while updating subjects!"); - return false; - } - - // Update the Classes in the db - const v2 = await Promise.all(subjects.map(async (subject) => { - const classes = await fetchClassesForSubject(endpoint, semester, subject); - - // skip if something went wrong fetching classes - // it could be that there are not classes here (in tests, corresponds to FEDN) - if (!classes) { - return true; - } - - // Update or add all the classes to the collection - const v = await Promise.all(classes.map(async (cl) => { - const classIfExists = await Classes.findOne({ classSub: cl.subject.toLowerCase(), classNum: cl.catalogNbr }).exec(); - const professors = extractProfessors(cl); - - // figure out if the professor already exist in the collection, if not, add to the collection - // build a list of professor names to potentially add the the class - const profs: string[] = await Promise.all(professors.map(async (p) => { - // This has to be an atomic upset. Otherwise, this causes some race condition badness - const professorIfExists = await Professors.findOneAndUpdate({ fullName: `${p.firstName} ${p.lastName}` }, - { $setOnInsert: { fullName: `${p.firstName} ${p.lastName}`, _id: shortid.generate(), major: "None" /* TODO: change? */ } }, - { upsert: true, new: true }); - - return professorIfExists.fullName; - })).catch((err) => { - console.log(err); - return []; - }); - - // The class does not exist yet, so we add it - if (!classIfExists) { - console.log(`Adding new class ${cl.subject} ${cl.catalogNbr}`); - const res = await new Classes({ - _id: shortid.generate(), - classSub: cl.subject.toLowerCase(), - classNum: cl.catalogNbr, - classTitle: cl.titleLong, - classFull: `${cl.subject.toLowerCase()} ${cl.catalogNbr} ${cl.titleLong}`, - classSems: [semester], - classProfessors: profs, - classRating: null, - classWorkload: null, - classDifficulty: null, - }).save().catch((err) => { - console.log(err); - return null; - }); - - // update professors with new class information - profs.forEach(async (inst) => { - await Professors.findOneAndUpdate({ fullName: inst }, { $addToSet: { courses: res._id } }).catch((err) => console.log(err)); - }); - - if (!res) { - console.log(`Unable to insert class ${cl.subject} ${cl.catalogNbr}!`); - throw new Error(); - } - } else { // The class does exist, so we update semester information - console.log(`Updating class information for ${classIfExists.classSub} ${classIfExists.classNum}`); - - // Compute the new set of semesters for this class - const classSems = classIfExists.classSems.indexOf(semester) == -1 ? classIfExists.classSems.concat([semester]) : classIfExists.classSems; - - // Compute the new set of professors for this class - const classProfessors = classIfExists.classProfessors ? classIfExists.classProfessors : []; - - // Add any new professors to the class - profs.forEach((inst) => { - if (classProfessors.filter((i) => i == inst).length === 0) { - classProfessors.push(inst); - } - }); - - // update db with new semester information - const res = await Classes.findOneAndUpdate({ _id: classIfExists._id }, { $set: { classSems, classProfessors } }).exec() - .catch((err) => { - console.log(err); - return null; - }); - - // update professors with new class information - // Note the set update. We don't want to add duplicates here - classProfessors.forEach(async (inst) => { - await Professors.findOneAndUpdate({ fullName: inst }, { $addToSet: { courses: classIfExists._id } }).catch((err) => console.log(err)); - }); - - if (!res) { - console.log(`Unable to update class information for ${cl.subject} ${cl.catalogNbr}!`); - throw new Error(); - } - } - - return true; - })).catch((err) => { - console.log(err); - return null; - }); - - // something went wrong updating classes - if (!v) { - throw new Error(); - } - - return true; - })).catch((err) => null); - - if (!v2) { - console.log("Something went wrong while updating classes"); - return false; - } - - return true; -} - -/* - Course API scraper. Uses HTTP requests to get course data from the Cornell - Course API and stores the results in the local database. - - Functions defined here should be called during app initialization to populate - the local database or once a semester to add new semester data to the - local database. - - Functions are called by admins via the admin interface (Admin component). - -*/ - -/* # Populates the Classes and Subjects collections in the local database by grabbing - # all courses data for the semesters in the semsters array though requests - # sent to the Cornell Courses API - # - # example: semesters = ["SP17", "SP16", "SP15","FA17", "FA16", "FA15"]; - # - # Using the findAllSemesters() array as input, the function populates an - # empty database with all courses and subjects. - # Using findCurrSemester(), the function updates the existing database. - # -*/ -export async function addAllCourses(semesters: any) { - console.log(semesters); - Object.keys(semesters).forEach(async (semester) => { - // get all classes in this semester - console.log(`Adding classes for the following semester: ${semesters[semester]}`); - const result = await axios.get(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, { timeout: 30000 }); - if (result.status !== 200) { - console.log('Error in addAllCourses: 1'); - return 0; - } - const response = result.data; - // console.log(response); - const sub = response.data.subjects; - await Promise.all(Object.keys(sub).map(async (course) => { - const parent = sub[course]; - // if subject doesn't exist add to Subjects collection - const checkSub = await Subjects.find({ subShort: parent.value.toLowerCase() }).exec(); - if (checkSub.length === 0) { - console.log(`new subject: ${parent.value}`); - await new Subjects({ - subShort: (parent.value).toLowerCase(), - subFull: parent.descr, - }).save(); - } - - // for each subject, get all classes in that subject for this semester - const result2 = await axios.get(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, { timeout: 30000 }); - if (result2.status !== 200) { - console.log('Error in addAllCourses: 2'); - return 0; - } - const response2 = result2.data; - const courses = response2.data.classes; - - // add each class to the Classes collection if it doesnt exist already - for (const course in courses) { - try { - console.log(`${courses[course].subject} ${courses[course].catalogNbr}`); - const check = await Classes.find({ classSub: courses[course].subject.toLowerCase(), classNum: courses[course].catalogNbr }).exec(); - console.log(check); - if (check.length === 0) { - console.log(`new class: ${courses[course].subject} ${courses[course].catalogNbr},${semesters[semester]}`); - // insert new class with empty prereqs and reviews - await new Classes({ - classSub: (courses[course].subject).toLowerCase(), - classNum: courses[course].catalogNbr, - classTitle: courses[course].titleLong, - classPrereq: [], - classFull: `${(courses[course].subject).toLowerCase()} ${courses[course].catalogNbr} ${courses[course].titleLong.toLowerCase()}`, - classSems: [semesters[semester]], - }).save(); - } else { - const matchedCourse = check[0]; // only 1 should exist - const oldSems = matchedCourse.classSems; - if (oldSems && oldSems.indexOf(semesters[semester]) === -1) { - // console.log("update class " + courses[course].subject + " " + courses[course].catalogNbr + "," + semesters[semester]); - oldSems.push(semesters[semester]); // add this semester to the list - Classes.update({ _id: matchedCourse._id }, { $set: { classSems: oldSems } }); - } - } - } catch (error) { - console.log('Error in addAllCourses: 3'); - return 0; - } - } - })); - }); - console.log('Finished addAllCourses'); - return 1; -} - - -export async function updateProfessors(semesters: any) { - // You just want to go through all the classes in the Classes database and update the Professors field - // Don't want to go through the semesters - // Might want a helper function that returns that professors for you - console.log('In updateProfessors method'); - for (const semester in semesters) { - // get all classes in this semester - // console.log(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`); - try { - await axios.get(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, { timeout: 30000 }); - } catch (error) { - console.log('Error in updateProfessors: 1'); - console.log(error); - continue; - } - const result = await axios.get(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, { timeout: 30000 }); - // console.log(result) - if (result.status !== 200) { - console.log('Error in updateProfessors: 2'); - console.log(result.status); - continue; - } else { - const response = result.data; - // console.log(response); - const { subjects } = response.data; // array of the subjects - for (const department in subjects) { // for every subject - const parent = subjects[department]; - // console.log("https://classes.cornell.edu/api/2.0/search/classes.json?roster=" + semesters[semester] + "&subject="+ parent.value) - try { - await axios.get(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, { timeout: 30000 }); - } catch (error) { - console.log('Error in updateProfessors: 3'); - console.log(error); - continue; - } - const result2 = await axios.get(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, { timeout: 30000 }); - if (result2.status !== 200) { - console.log('Error in updateProfessors: 4'); - console.log(result2.status); - continue; - } else { - const response2 = result2.data; - const courses = response2.data.classes; - - // add each class to the Classes collection if it doesnt exist already - for (const course in courses) { - try { - const check = await Classes.find({ classSub: courses[course].subject.toLowerCase(), classNum: courses[course].catalogNbr }).exec(); - const matchedCourse = check[0]; // catch this if there is no class existing - if (typeof matchedCourse !== 'undefined') { - // console.log(courses[course].subject); - // console.log(courses[course].catalogNbr); - // console.log("This is the matchedCourse") - // console.log(matchedCourse) - let oldProfessors = matchedCourse.classProfessors; - if (oldProfessors == undefined) { - oldProfessors = []; - } - // console.log("This is the length of old profs") - // console.log(oldProfessors.length) - const { classSections } = courses[course].enrollGroups[0]; // This returns an array - for (const section in classSections) { - if (classSections[section].ssrComponent == 'LEC' - || classSections[section].ssrComponent == 'SEM') { - // Checks to see if class has scheduled meetings before checking them - if (classSections[section].meetings.length > 0) { - const professors = classSections[section].meetings[0].instructors; - // Checks to see if class has instructors before checking them - // Example of class without professors is: - // ASRC 3113 in FA16 - // ASRC 3113 returns an empty array for professors - if (professors.length > 0) { - for (const professor in professors) { - const { firstName } = professors[professor]; - const { lastName } = professors[professor]; - const fullName = `${firstName} ${lastName}`; - if (!oldProfessors.includes(fullName)) { - oldProfessors.push(fullName); - // console.log("This is a new professor") - // console.log(typeof oldProfessors) - // console.log(oldProfessors) - } - } - } else { - // console.log("This class does not have professors"); - } - } else { - // console.log("This class does not have meetings scheduled"); - } - } - } - await Classes.update({ _id: matchedCourse._id }, { $set: { classProfessors: oldProfessors } }).exec(); - } - } catch (error) { - console.log('Error in updateProfessors: 5'); - console.log(`Error on course ${courses[course].subject} ${courses[course].catalogNbr}`); - console.log(error); - - return 1; - } - } - } - } - } - } - console.log('Finished updateProfessors'); - return 0; -} - -export async function resetProfessorArray(semesters: any) { - // Initializes the classProfessors field in the Classes collection to an empty array so that - // we have a uniform empty array to fill with updateProfessors - // Will only have to be called ONCE - console.log('In resetProfessorArray method'); - for (const semester in semesters) { - // get all classes in this semester - const result = await axios.get(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, { timeout: 30000 }); - if (result.status !== 200) { - console.log('Error in resetProfessorArray: 1'); - console.log(result.status); - return 0; - } - - - const response = result.data; - // console.log(response); - const sub = response.data.subjects; // array of the subjects - for (const course in sub) { // for every subject - const parent = sub[course]; - console.log(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`); - const result2 = await axios.get(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, { timeout: 30000 }); - - if (result2.status !== 200) { - console.log('Error in resetProfessorArray: 2'); - return 0; - } - - - const response2 = result2.data; - // console.log("PRINTING ALL THE COURSES") - const courses = response2.data.classes; - // console.log(courses) - - // add each class to the Classes collection if it doesnt exist already - for (const course in courses) { - try { - const check = await Classes.find({ classSub: courses[course].subject.toLowerCase(), classNum: courses[course].catalogNbr }).exec(); - const matchedCourse = check[0]; // catch this if there is no class existing - if (typeof matchedCourse !== 'undefined') { - console.log(courses[course].subject); - console.log(courses[course].catalogNbr); - console.log('This is the matchedCourse'); - console.log(matchedCourse); - // var oldProfessors = matchedCourse.classProfessors - const oldProfessors = []; - console.log('This is the length of old profs'); - console.log(oldProfessors.length); - Classes.update({ _id: matchedCourse._id }, { $set: { classProfessors: oldProfessors } }); - } - } catch (error) { - console.log('Error in resetProfessorArray: 5'); - console.log(`Error on course ${courses[course].subject} ${courses[course].catalogNbr}`); - console.log(error); - return 0; - } - } - } - } - console.log('professors reset'); - return 1; -} - - -export async function getProfessorsForClass() { - // Need the method here to extract the Professor from the response - // return the array here -} - -/* # Grabs the API-required format of the current semester, to be given to the - # addAllCourses function. - # Return: String Array (length = 1) -*/ -export async function findCurrSemester() { - let response = await axios.get('https://classes.cornell.edu/api/2.0/config/rosters.json', { timeout: 30000 }); - if (response.status !== 200) { - console.log('Error in findCurrSemester'); - } else { - response = response.data; - const allSemesters = response.data.rosters; - const thisSem = allSemesters[allSemesters.length - 1].slug; - console.log(`Updating for following semester: ${thisSem}`); - return [thisSem]; - } -} - -/* # Grabs the API-required format of the all recent semesters to be given to the - # addAllCourses function. - # Return: String Array -*/ -export async function findAllSemesters(): Promise { - let response = await axios.get('https://classes.cornell.edu/api/2.0/config/rosters.json', { timeout: 30000 }); - if (response.status !== 200) { - console.log('error'); - return []; - } - response = response.data; - const allSemesters = response.data.rosters; - return allSemesters.map((semesterObject) => semesterObject.slug); -} - -/* # Look through all courses in the local database, and identify those - # that are cross-listed (have multiple official names). Link these classes - # by adding their course_id to all crosslisted class's crosslist array. - # - # Called once during intialization, only after all courses have been added. -*/ -export async function addCrossList() { - const semesters = await findAllSemesters(); - for (const semester in semesters) { - // get all classes in this semester - const result = await axios.get(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, { timeout: 30000 }); - if (result.status !== 200) { - console.log('Error in addCrossList: 1'); - return 0; - } - const response = result.data; - // console.log(response); - const sub = response.data.subjects; - for (const course in sub) { - const parent = sub[course]; - - // for each subject, get all classes in that subject for this semester - const result2 = await axios.get(`https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, { timeout: 30000 }); - if (result2.status !== 200) { - console.log('Error in addCrossList: 2'); - return 0; - } - const response2 = result2.data; - const courses = response2.data.classes; - - for (const course in courses) { - try { - const check = await Classes.find({ classSub: courses[course].subject.toLowerCase(), classNum: courses[course].catalogNbr }).exec(); - // console.log((courses[course].subject).toLowerCase() + " " + courses[course].catalogNbr); - // console.log(check); - if (check.length > 0) { - const crossList = courses[course].enrollGroups[0].simpleCombinations; - if (crossList.length > 0) { - const crossListIDs: string[] = await Promise.all(crossList.map(async (crossListedCourse: any) => { - console.log(crossListedCourse); - const dbCourse = await Classes.find({ classSub: crossListedCourse.subject.toLowerCase(), classNum: crossListedCourse.catalogNbr }).exec(); - // Added the following check because MUSIC 2340 - // was crosslisted with AMST 2340, which was not in our db - // so was causing an error here when calling 'dbCourse[0]._id' - // AMST 2340 exists in FA17 but not FA18 - if (dbCourse[0]) { - return dbCourse[0]._id; - } - - return null; - })); - console.log(`${courses[course].subject} ${courses[course].catalogNbr}`); - // console.log(crossListIDs); - const thisCourse = check[0]; - Classes.update({ _id: thisCourse._id }, { $set: { crossList: crossListIDs } }); - } - } - } catch (error) { - console.log('Error in addCrossList: 3'); - console.log(error); - return 0; - } - } - } - } - console.log('Finished addCrossList'); - return 1; -} diff --git a/server/endpoints.ts b/server/endpoints.ts index ca71630a0..29ece3dad 100644 --- a/server/endpoints.ts +++ b/server/endpoints.ts @@ -6,7 +6,7 @@ import { howManyEachClass, topSubjects, getReviewsOverTimeTop15, -} from "./endpoints/AdminChart"; +} from "./endpoints/admin/AdminChart"; import { getReviewsByCourseId, getCourseById, @@ -15,21 +15,21 @@ import { getCourseByInfo, updateLiked, userHasLiked, -} from "./endpoints/Review"; +} from "./endpoints/review/Review"; import { countReviewsByStudentId, getTotalLikesByStudentId, getReviewsByStudentId, getStudentEmailByToken, -} from "./endpoints/Profile"; -import { tokenIsAdmin } from "./endpoints/Auth"; +} from "./endpoints/profile/Profile"; +import { tokenIsAdmin } from "./endpoints/auth/Auth"; import { getCoursesByProfessor, getCoursesByMajor, getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, -} from "./endpoints/Search"; +} from "./endpoints/search/Search"; import { fetchReviewableClasses, reportReview, @@ -37,7 +37,7 @@ import { undoReportReview, removeReview, getRaffleWinner, -} from "./endpoints/AdminActions"; +} from "./endpoints/admin/AdminActions"; export interface Context { ip: string; diff --git a/server/endpoints/AdminChart.ts b/server/endpoints/AdminChart.ts deleted file mode 100644 index d641087e1..000000000 --- a/server/endpoints/AdminChart.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* eslint-disable spaced-comment */ -import { body } from "express-validator"; -import { verifyToken } from "./utils"; -import { Context, Endpoint } from "../endpoints"; -import { Reviews, Classes, Subjects } from "../dbDefs"; - -export interface Token { - token: string; -} - -interface GetReviewsOverTimeTop15Request { - token: string; - step: number; - range: number; -} - -/** - * Returns an key value object where key is a dept and value is an array of - * key-value objects where key is a data and value is number of reviews for - * that courses in that dept at the data given by the key: - * - * EX: - * { - * math: [ {'2020-12-09' : 10}, {'2020-12-10' : 9}], - * cs: [ {}, ... ,{} ], - * ... - * } - */ -export const getReviewsOverTimeTop15: Endpoint = { - guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, request: GetReviewsOverTimeTop15Request) => { - const { token, step, range } = request; - try { - const userIsAdmin = await verifyToken(token); - if (userIsAdmin) { - const top15 = await topSubjectsCB(ctx, { token }); - // contains cs, math, gov etc... - const retArr = []; - await Promise.all(top15.map(async (classs) => { - const [subject] = await Subjects.find({ - subFull: classs[0], - }, { - subShort: 1, - }).exec(); // EX: computer science--> cs - const subshort = subject.subShort; - retArr.push(subshort); - })); - const arrHM = []; // [ {"cs": {date1: totalNum}, math: {date1, totalNum} }, - // {"cs": {date2: totalNum}, math: {date2, totalNum} } ] - for (let i = 0; i < range * 30; i += step) { - // "data": -->this{"2017-01-01": 3, "2017-01-02": 4, ...} - // run on reviews. gets all classes and num of reviewa for each class, in x day - const pipeline = [{ - $match: { - date: { - $lte: new Date(new Date().setDate(new Date().getDate() - i)), - }, - }, - }, - { - $group: { - _id: '$class', - total: { - $sum: 1, - }, - }, - }, - ]; - const hashMap = { total: null }; // Object {"cs": {date1: totalNum, date2: totalNum, ...}, math: {date1, totalNum} } - // eslint-disable-next-line no-await-in-loop - const results = await Reviews.aggregate<{ _id: string; total: number }>(pipeline); - // eslint-disable-next-line no-await-in-loop - await Promise.all(results.map(async (data) => { // { "_id" : "KyeJxLouwDvgY8iEu", "total" : 1 } //all in same date - const res = await Classes.find({ - _id: data._id, - }, { - classSub: 1, - }).exec(); - - const sub = res[0]; // finds the class corresponding to "KyeJxLouwDvgY8iEu" ex: cs 2112 - // date of this review minus the hrs mins sec - const timeStringYMD = new Date(new Date().setDate(new Date().getDate() - i)).toISOString().split('T')[0]; - if (retArr.includes(sub.classSub)) { // if thos review is one of the top 15 we want. - if (hashMap[sub.classSub] == null) { - // if not in hm then add - hashMap[sub.classSub] = { - [timeStringYMD]: data.total, - }; - } else { - // increment totalnum - hashMap[sub.classSub] = { - [timeStringYMD]: hashMap[sub.classSub][timeStringYMD] + data.total, - }; - } - } - if (hashMap.total == null) { - hashMap.total = { - [timeStringYMD]: data.total, - }; - } else { - hashMap.total = { - [timeStringYMD]: hashMap.total[timeStringYMD] + data.total, - }; - } - })); - arrHM.push(hashMap); - } - - const hm2 = {}; // {cs: [{date1:totalNum}, {date2: totalNum}, ...], math: [{date1:total}, {date2: total}, ...], ... } - - // enrty:{"cs": {date1: totalNum}, math: {date1, totalNum} } - if (arrHM.length > 0) { - const entry = arrHM[0]; - const keys = Object.keys(entry); - - // "cs" - keys.forEach((key) => { - const t = arrHM.map((a) => a[key]); // for a key EX:"cs": [{date1:totalNum},{date2:totalNum}] - hm2[key] = t; - }); - } - return hm2; - } - - // user is not admin - return null; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getReviewsOverTimeTop15' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } - }, -}; - -/** - * Helper function for [topSubjects] - */ -const topSubjectsCB = async (_ctx: Context, request: Token) => { - const userIsAdmin = await verifyToken(request.token); - if (!userIsAdmin) { - return null; - } - - try { - // using the add-on library meteorhacks:aggregate, define pipeline aggregate functions - // to run complex queries - const pipeline = [ - // consider only visible reviews - { $match: { visible: 1 } }, - // group by class and get count of reviews - { $group: { _id: '$class', reviewCount: { $sum: 1 } } }, - // sort by decending count - // {$sort: {"reviewCount": -1}}, - // {$limit: 10} - ]; - // reviewedSubjects is a dictionary-like object of subjects (key) and - // number of reviews (value) associated with that subject - const reviewedSubjects = new DefaultDict(); - // run the query and return the class name and number of reviews written to it - const results = await Reviews.aggregate<{ reviewCount: number; _id: string }>(pipeline); - - await Promise.all(results.map(async (course) => { - const classObject = (await Classes.find({ _id: course._id }).exec())[0]; - // classSubject is the string of the full subject of classObject - const subjectArr = await Subjects.find({ subShort: classObject.classSub }).exec(); - if (subjectArr.length > 0) { - const classSubject = subjectArr[0].subFull; - // Adds the number of reviews to the ongoing count of reviews per subject - const curVal = reviewedSubjects.get(classSubject) || 0; - reviewedSubjects[classSubject] = curVal + course.reviewCount; - } - })); - - // Creates a map of subjects (key) and total number of reviews (value) - const subjectsMap = new Map(Object.entries(reviewedSubjects).filter((x): x is [string, number] => typeof x[1] === "number")); - let subjectsAndReviewCountArray = Array.from(subjectsMap); - // Sorts array by number of reviews each topic has - subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => (a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0)); - - // Returns the top 15 most reviewed classes - return subjectsAndReviewCountArray.slice(0, 15); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'topSubjects' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } -}; - -/** - * Returns the top 15 subjects (in terms of number of reviews) - */ -export const topSubjects: Endpoint = { - guard: [body("token").notEmpty()], - callback: topSubjectsCB, -}; - -/** - * Returns an array of key-val objects where keys are dept and vals are number - * of courses in that dept. - */ -export const howManyEachClass: Endpoint = { - guard: [body("token").notEmpty().isAscii()], - callback: async (_ctx: Context, request: Token) => { - const { token } = request; - try { - const userIsAdmin = await verifyToken(token); - if (userIsAdmin) { - const pipeline = [ - { - $group: { - _id: '$classSub', - total: { - $sum: 1, - }, - }, - }, - ]; - return await Classes.aggregate(pipeline); - } - return null; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'howManyEachClass' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } - }, -}; - -/** - * Gets total number of reviews in db - */ -export const totalReviews: Endpoint = { - // eslint-disable-next-line no-undef - guard: [body("token").notEmpty().isAscii()], - callback: async (_ctx: Context, request: Token) => { - const { token } = request; - try { - const userIsAdmin = await verifyToken(token); - if (userIsAdmin) { - return Reviews.find({}).count(); - } - return -1; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'totalReviews' method"); - // eslint-disable-next-line no-console - console.log(error); - return -2; - } - }, -}; - -/** - * Gets a array of key-value objects where each key a is class and value is - * the number of reviews of that class. - */ -export const howManyReviewsEachClass: Endpoint = { - guard: [body("token").notEmpty().isAscii()], - callback: async (_ctx: Context, request: Token) => { - const { token } = request; - try { - const userIsAdmin = await verifyToken(token); - if (userIsAdmin) { - const pipeline = [ - { - $group: { - _id: '$class', - total: { - $sum: 1, - }, - }, - }, - ]; - const results = await Reviews.aggregate<{ _id: string; total: number }>(pipeline); - - const ret = await Promise.all(results.map(async (data) => { - const subNum = (await Classes.find({ _id: data._id }, { classSub: 1, classNum: 1 }).exec())[0]; - const id = `${subNum.classSub} ${subNum.classNum}`; - return { _id: id, total: data.total }; - })); - - return ret; - } - return null; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'howManyReviewsEachClass' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } - }, -}; - -// Recreation of Python's defaultdict to be used in topSubjects method -export class DefaultDict { - [key: string]: T | Function; - - get(key: string): T | null { - const val = this[key]; - - if (Object.prototype.hasOwnProperty.call(this, key) && typeof val !== "function") { - return val; - } - return null; - } -} diff --git a/server/endpoints/Search.test.ts b/server/endpoints/Search.test.ts deleted file mode 100644 index 2b04725d7..000000000 --- a/server/endpoints/Search.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import axios from 'axios'; -import { Student, Class, Subject, Professor } from 'common'; -import TestingServer, { testingPort } from './TestServer'; - - -const testServer = new TestingServer(testingPort); - -beforeAll(async () => { - const testStudents: Student[] = [ - { - _id: "Irrelevant", - firstName: "John", - lastName: "Smith", - netId: "js0", - affiliation: null, - token: null, - privilege: "regular", - reviews: [], - likedReviews: [], - }, - - ]; - - const testClasses: Class[] = [ - { - _id: "newCourse1", - classSub: "MORK", - classNum: "1110", - classTitle: "Introduction to Testing", - classFull: "MORK 1110: Introduction to Testing", - classSems: ["FA19"], - classProfessors: ["Gazghul Thraka"], - classRating: 1, - classWorkload: 2, - classDifficulty: 3, - classPrereq: [], - crossList: [], - }, - { - _id: "newCourse2", - classSub: "MORK", - classNum: "2110", - classTitle: "Intermediate Testing", - classFull: "MORK 2110: Intermediate Testing", - classSems: ["SP20"], - classPrereq: ["newCourse1"], // the class above - classProfessors: ["Gazghul Thraka"], - classRating: 3, - classWorkload: 4, - classDifficulty: 5, - crossList: [], - }, - ]; - - const testSubjects: Subject[] = [ - { - _id: "newSubject1", - subShort: "MORK", - subFull: "Study of Angry Fungi", - }, - { - _id: "angry subject", - subShort: "MAD", - subFull: "The Study of Anger Issues", - }, - { - _id: "federation subject", - subShort: "FEDN", - subFull: "The Study of Where No Man has Gone Before!", - }, - - ]; - - const testProfessors: Professor[] = [ - { - _id: "prof_1", - fullName: "Gazghul Thraka", - courses: ["newCourse1", "newCourse2"], - major: "MORK", - }, - { - _id: "prof_2", - fullName: "Jean-Luc Picard", - courses: [], - major: "FEDN", - }, - ]; - - await testServer.setUpDB(undefined, testStudents, testClasses, testProfessors, testSubjects); -}); - -afterAll(async () => { - await testServer.shutdownTestingServer(); -}); - -describe('tests', () => { - it('getClassesByQuery-works', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res1 = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "MORK 1" }); - // we expect it to be MORK 1110 first, and then MORK 2110 - expect(res1.data.result.map((e) => e.classFull)).toStrictEqual(["MORK 1110: Introduction to Testing", "MORK 2110: Intermediate Testing"]); - }); - - it('getClassesByQuery-works "MORK1" ', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "MORK1" }); - expect(res.data.result.map((e) => e.classFull)).toStrictEqual(["MORK 1110: Introduction to Testing", "MORK 2110: Intermediate Testing"]); - }); - - it('getClassesByQuery-works "MORK 1110" ', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "MORK1110" }); - expect(res.data.result.map((e) => e.classFull)).toStrictEqual(["MORK 1110: Introduction to Testing"]); - }); - - it('getSubjectsByQuery-works', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { query: "MORK" }); - expect(res.data.result.map((e) => e.subShort)).toContain("MORK"); - expect(res.data.result.map((e) => e.subShort)).not.toContain("MAD"); - expect(res.data.result.map((e) => e.subShort)).not.toContain("FEDN"); - }); - - it('getProfessorsByQuery-works', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res1 = await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { query: "Gazghul Thraka" }); - expect(res1.data.result.map((e) => e.fullName)).toContain("Gazghul Thraka"); - expect(res1.data.result.map((e) => e.fullName)).not.toContain("Jean-Luc Picard"); - - const res2 = await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { query: "Jean-Luc Picard" }); - expect(res2.data.result.map((e) => e.fullName)).not.toContain("Gazghul Thraka"); - expect(res2.data.result.map((e) => e.fullName)).toContain("Jean-Luc Picard"); - }); - - // Query has no matching results: - it('getClassesByQuery-no matching classes', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "random" }); - // we expect no results to be returned - expect(res.data.result.map((e) => e.classFull)).toStrictEqual([]); - expect(res.data.result.map((e) => e.classFull)).not.toContain(["MORK 1110: Introduction to Testing", "MORK 2110: Intermediate Testing"]); - }); - - it('getSubjectsByQuery-no matching subjects', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { query: "RAND" }); - // we expect no results to be returned - expect(res.data.result.map((e) => e.subShort)).toStrictEqual([]); - expect(res.data.result.map((e) => e.subShort)).not.toContain("MORK"); - expect(res.data.result.map((e) => e.subShort)).not.toContain("MAD"); - expect(res.data.result.map((e) => e.subShort)).not.toContain("FEDN"); - - const res2 = await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { query: "RAND1" }); - expect(res2.data.result.map((e) => e.subShort)).toStrictEqual([]); - }); - - it('getProfessorsByQuery-no matching professors', async () => { - expect(await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { "not query": "other" }).catch((e) => "failed!")).toBe("failed!"); - - const res = await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { query: "Random Professor" }); - // we expect no results to be returned - expect(res.data.result.map((e) => e.fullName)).toStrictEqual([]); - expect(res.data.result.map((e) => e.fullName)).not.toContain("Gazghul Thraka"); - expect(res.data.result.map((e) => e.fullName)).not.toContain("Jean-Luc Picard"); - }); - - // Will accept ascii, but give no guarantees as to what is returned. - it('getClassesByQuery-non Ascii', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "भारत" }).catch((e) => e); - expect(res.data.result).toBeTruthy(); - }); - - // Not for these however. - it('getSubjectsByQuery-non Ascii', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { query: "भारत" }).catch((e) => e); - expect(res.message).toBe("Request failed with status code 400"); - }); - - it('getProfessorsByQuery-non Ascii', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { query: "भारत" }).catch((e) => e); - expect(res.message).toBe("Request failed with status code 400"); - }); - - it('getClassesByQuery- empty query', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { query: "" }).catch((e) => e); - expect(res.message).toBe("Request failed with status code 400"); - }); -}); diff --git a/server/endpoints/AdminActions.ts b/server/endpoints/admin/AdminActions.ts similarity index 78% rename from server/endpoints/AdminActions.ts rename to server/endpoints/admin/AdminActions.ts index 7fed572aa..4b1924958 100644 --- a/server/endpoints/AdminActions.ts +++ b/server/endpoints/admin/AdminActions.ts @@ -1,12 +1,15 @@ import { body } from "express-validator"; import { getCrossListOR, getMetricValues } from "common/CourseCard"; -import { Context, Endpoint } from "../endpoints"; -import { Reviews, ReviewDocument, Classes, Students } from "../dbDefs"; -import { updateProfessors, findAllSemesters, resetProfessorArray } from "../dbInit"; -import { getCourseById, verifyToken } from "./utils"; -import { ReviewRequest } from "./Review"; - +import { Context, Endpoint } from "../../endpoints"; +import { Reviews, ReviewDocument, Classes, Students } from "../../db/dbDefs"; +import { + updateProfessors, + findAllSemesters, + resetProfessorArray, +} from "../../db/dbInit"; +import { getCourseById, verifyToken } from "../utils/utils"; +import { ReviewRequest } from "../review/Review"; // The type for a request with an admin action for a review interface AdminReviewRequest { @@ -31,17 +34,32 @@ export const updateCourseMetrics = async (courseId) => { const course = await getCourseById({ courseId }); if (course) { const crossListOR = getCrossListOR(course); - const reviews = await Reviews.find({ visible: 1, reported: 0, $or: crossListOR }, {}, { sort: { date: -1 }, limit: 700 }).exec(); + const reviews = await Reviews.find( + { visible: 1, reported: 0, $or: crossListOR }, + {}, + { sort: { date: -1 }, limit: 700 }, + ).exec(); const state = getMetricValues(reviews); - await Classes.updateOne({ _id: courseId }, + await Classes.updateOne( + { _id: courseId }, { $set: { // If no data is available, getMetricValues returns "-" for metric - classDifficulty: (state.diff !== "-" && !isNaN(Number(state.diff)) ? Number(state.diff) : null), - classRating: (state.rating !== "-" && !isNaN(Number(state.rating)) ? Number(state.rating) : null), - classWorkload: (state.workload !== "-" && !isNaN(Number(state.workload)) ? Number(state.workload) : null), + classDifficulty: + state.diff !== "-" && !isNaN(Number(state.diff)) + ? Number(state.diff) + : null, + classRating: + state.rating !== "-" && !isNaN(Number(state.rating)) + ? Number(state.rating) + : null, + classWorkload: + state.workload !== "-" && !isNaN(Number(state.workload)) + ? Number(state.workload) + : null, }, - }); + }, + ); return { resCode: 1 }; } @@ -68,7 +86,10 @@ export const makeReviewVisible: Endpoint = { const userIsAdmin = await verifyToken(adminReviewRequest.token); const regex = new RegExp(/^(?=.*[A-Z0-9])/i); if (regex.test(adminReviewRequest.review._id) && userIsAdmin) { - await Reviews.updateOne({ _id: adminReviewRequest.review._id }, { $set: { visible: 1 } }); + await Reviews.updateOne( + { _id: adminReviewRequest.review._id }, + { $set: { visible: 1 } }, + ); await updateCourseMetrics(adminReviewRequest.review.class); return { resCode: 1 }; @@ -89,12 +110,19 @@ export const makeReviewVisible: Endpoint = { * "Undo" the reporting of a flagged review and make it visible again */ export const undoReportReview: Endpoint = { - guard: [body("review").notEmpty(), body("token").notEmpty().isAscii(), body("review._id").isAscii()], + guard: [ + body("review").notEmpty(), + body("token").notEmpty().isAscii(), + body("review._id").isAscii(), + ], callback: async (ctx: Context, adminReviewRequest: AdminReviewRequest) => { try { const userIsAdmin = await verifyToken(adminReviewRequest.token); if (userIsAdmin) { - await Reviews.updateOne({ _id: adminReviewRequest.review._id }, { $set: { visible: 1, reported: 0 } }); + await Reviews.updateOne( + { _id: adminReviewRequest.review._id }, + { $set: { visible: 1, reported: 0 } }, + ); await updateCourseMetrics(adminReviewRequest.review.class); return { resCode: 1 }; } @@ -113,7 +141,11 @@ export const undoReportReview: Endpoint = { * Remove a review, used for both submitted and flagged reviews */ export const removeReview: Endpoint = { - guard: [body("review").notEmpty(), body("token").notEmpty().isAscii(), body("review._id").isAscii()], + guard: [ + body("review").notEmpty(), + body("token").notEmpty().isAscii(), + body("review._id").isAscii(), + ], callback: async (ctx: Context, adminReviewRequest: AdminReviewRequest) => { try { const userIsAdmin = await verifyToken(adminReviewRequest.token); @@ -138,7 +170,10 @@ export const removeReview: Endpoint = { */ export const setProfessors: Endpoint = { guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, adminProfessorsRequest: AdminProfessorsRequest) => { + callback: async ( + ctx: Context, + adminProfessorsRequest: AdminProfessorsRequest, + ) => { try { const userIsAdmin = await verifyToken(adminProfessorsRequest.token); if (userIsAdmin) { @@ -166,7 +201,10 @@ export const setProfessors: Endpoint = { */ export const resetProfessors: Endpoint = { guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, adminProfessorsRequest: AdminProfessorsRequest) => { + callback: async ( + ctx: Context, + adminProfessorsRequest: AdminProfessorsRequest, + ) => { try { const userIsAdmin = await verifyToken(adminProfessorsRequest.token); if (userIsAdmin) { @@ -192,7 +230,10 @@ export const reportReview: Endpoint = { guard: [body("id").notEmpty().isAscii()], callback: async (ctx: Context, request: ReviewRequest) => { try { - await Reviews.updateOne({ _id: request.id }, { $set: { visible: 0, reported: 1 } }); + await Reviews.updateOne( + { _id: request.id }, + { $set: { visible: 0, reported: 1 } }, + ); const course = (await Reviews.findOne({ _id: request.id })).class; const res = await updateCourseMetrics(course); return { resCode: res.resCode }; @@ -212,7 +253,11 @@ export const fetchReviewableClasses: Endpoint = { try { const userIsAdmin = await verifyToken(request.token); if (userIsAdmin) { - return Reviews.find({ visible: 0 }, {}, { sort: { date: -1 }, limit: 700 }); + return Reviews.find( + { visible: 0 }, + {}, + { sort: { date: -1 }, limit: 700 }, + ); } return { resCode: 0 }; } catch (error) { diff --git a/server/endpoints/admin/AdminChart.ts b/server/endpoints/admin/AdminChart.ts new file mode 100644 index 000000000..6333058da --- /dev/null +++ b/server/endpoints/admin/AdminChart.ts @@ -0,0 +1,362 @@ +/* eslint-disable spaced-comment */ +import { body } from "express-validator"; +import { verifyToken } from "../utils/utils"; +import { Context, Endpoint } from "../../endpoints"; +import { Reviews, Classes, Subjects } from "../../db/dbDefs"; + +export interface Token { + token: string; +} + +interface GetReviewsOverTimeTop15Request { + token: string; + step: number; + range: number; +} + +/** + * Returns an key value object where key is a dept and value is an array of + * key-value objects where key is a data and value is number of reviews for + * that courses in that dept at the data given by the key: + * + * EX: + * { + * math: [ {'2020-12-09' : 10}, {'2020-12-10' : 9}], + * cs: [ {}, ... ,{} ], + * ... + * } + */ +export const getReviewsOverTimeTop15: Endpoint = + { + guard: [body("token").notEmpty().isAscii()], + callback: async (ctx: Context, request: GetReviewsOverTimeTop15Request) => { + const { token, step, range } = request; + try { + const userIsAdmin = await verifyToken(token); + if (userIsAdmin) { + const top15 = await topSubjectsCB(ctx, { token }); + // contains cs, math, gov etc... + const retArr = []; + await Promise.all( + top15.map(async (classs) => { + const [subject] = await Subjects.find( + { + subFull: classs[0], + }, + { + subShort: 1, + }, + ).exec(); // EX: computer science--> cs + const subshort = subject.subShort; + retArr.push(subshort); + }), + ); + const arrHM = []; // [ {"cs": {date1: totalNum}, math: {date1, totalNum} }, + // {"cs": {date2: totalNum}, math: {date2, totalNum} } ] + for (let i = 0; i < range * 30; i += step) { + // "data": -->this{"2017-01-01": 3, "2017-01-02": 4, ...} + // run on reviews. gets all classes and num of reviewa for each class, in x day + const pipeline = [ + { + $match: { + date: { + $lte: new Date( + new Date().setDate(new Date().getDate() - i), + ), + }, + }, + }, + { + $group: { + _id: "$class", + total: { + $sum: 1, + }, + }, + }, + ]; + const hashMap = { total: null }; // Object {"cs": {date1: totalNum, date2: totalNum, ...}, math: {date1, totalNum} } + // eslint-disable-next-line no-await-in-loop + const results = await Reviews.aggregate<{ + _id: string; + total: number; + }>(pipeline); + // eslint-disable-next-line no-await-in-loop + await Promise.all( + results.map(async (data) => { + // { "_id" : "KyeJxLouwDvgY8iEu", "total" : 1 } //all in same date + const res = await Classes.find( + { + _id: data._id, + }, + { + classSub: 1, + }, + ).exec(); + + const sub = res[0]; // finds the class corresponding to "KyeJxLouwDvgY8iEu" ex: cs 2112 + // date of this review minus the hrs mins sec + const timeStringYMD = new Date( + new Date().setDate(new Date().getDate() - i), + ) + .toISOString() + .split("T")[0]; + if (retArr.includes(sub.classSub)) { + // if thos review is one of the top 15 we want. + if (hashMap[sub.classSub] == null) { + // if not in hm then add + hashMap[sub.classSub] = { + [timeStringYMD]: data.total, + }; + } else { + // increment totalnum + hashMap[sub.classSub] = { + [timeStringYMD]: + hashMap[sub.classSub][timeStringYMD] + data.total, + }; + } + } + if (hashMap.total == null) { + hashMap.total = { + [timeStringYMD]: data.total, + }; + } else { + hashMap.total = { + [timeStringYMD]: hashMap.total[timeStringYMD] + data.total, + }; + } + }), + ); + arrHM.push(hashMap); + } + + const hm2 = {}; // {cs: [{date1:totalNum}, {date2: totalNum}, ...], math: [{date1:total}, {date2: total}, ...], ... } + + // enrty:{"cs": {date1: totalNum}, math: {date1, totalNum} } + if (arrHM.length > 0) { + const entry = arrHM[0]; + const keys = Object.keys(entry); + + // "cs" + keys.forEach((key) => { + const t = arrHM.map((a) => a[key]); // for a key EX:"cs": [{date1:totalNum},{date2:totalNum}] + hm2[key] = t; + }); + } + return hm2; + } + + // user is not admin + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getReviewsOverTimeTop15' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } + }, + }; + +/** + * Helper function for [topSubjects] + */ +const topSubjectsCB = async (_ctx: Context, request: Token) => { + const userIsAdmin = await verifyToken(request.token); + if (!userIsAdmin) { + return null; + } + + try { + // using the add-on library meteorhacks:aggregate, define pipeline aggregate functions + // to run complex queries + const pipeline = [ + // consider only visible reviews + { $match: { visible: 1 } }, + // group by class and get count of reviews + { $group: { _id: "$class", reviewCount: { $sum: 1 } } }, + // sort by decending count + // {$sort: {"reviewCount": -1}}, + // {$limit: 10} + ]; + // reviewedSubjects is a dictionary-like object of subjects (key) and + // number of reviews (value) associated with that subject + const reviewedSubjects = new DefaultDict(); + // run the query and return the class name and number of reviews written to it + const results = await Reviews.aggregate<{ + reviewCount: number; + _id: string; + }>(pipeline); + + await Promise.all( + results.map(async (course) => { + const classObject = (await Classes.find({ _id: course._id }).exec())[0]; + // classSubject is the string of the full subject of classObject + const subjectArr = await Subjects.find({ + subShort: classObject.classSub, + }).exec(); + if (subjectArr.length > 0) { + const classSubject = subjectArr[0].subFull; + // Adds the number of reviews to the ongoing count of reviews per subject + const curVal = reviewedSubjects.get(classSubject) || 0; + reviewedSubjects[classSubject] = curVal + course.reviewCount; + } + }), + ); + + // Creates a map of subjects (key) and total number of reviews (value) + const subjectsMap = new Map( + Object.entries(reviewedSubjects).filter( + (x): x is [string, number] => typeof x[1] === "number", + ), + ); + let subjectsAndReviewCountArray = Array.from(subjectsMap); + // Sorts array by number of reviews each topic has + subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => + a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0, + ); + + // Returns the top 15 most reviewed classes + return subjectsAndReviewCountArray.slice(0, 15); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'topSubjects' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; + +/** + * Returns the top 15 subjects (in terms of number of reviews) + */ +export const topSubjects: Endpoint = { + guard: [body("token").notEmpty()], + callback: topSubjectsCB, +}; + +/** + * Returns an array of key-val objects where keys are dept and vals are number + * of courses in that dept. + */ +export const howManyEachClass: Endpoint = { + guard: [body("token").notEmpty().isAscii()], + callback: async (_ctx: Context, request: Token) => { + const { token } = request; + try { + const userIsAdmin = await verifyToken(token); + if (userIsAdmin) { + const pipeline = [ + { + $group: { + _id: "$classSub", + total: { + $sum: 1, + }, + }, + }, + ]; + return await Classes.aggregate(pipeline); + } + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'howManyEachClass' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } + }, +}; + +/** + * Gets total number of reviews in db + */ +export const totalReviews: Endpoint = { + // eslint-disable-next-line no-undef + guard: [body("token").notEmpty().isAscii()], + callback: async (_ctx: Context, request: Token) => { + const { token } = request; + try { + const userIsAdmin = await verifyToken(token); + if (userIsAdmin) { + return Reviews.find({}).count(); + } + return -1; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'totalReviews' method"); + // eslint-disable-next-line no-console + console.log(error); + return -2; + } + }, +}; + +/** + * Gets a array of key-value objects where each key a is class and value is + * the number of reviews of that class. + */ +export const howManyReviewsEachClass: Endpoint = { + guard: [body("token").notEmpty().isAscii()], + callback: async (_ctx: Context, request: Token) => { + const { token } = request; + try { + const userIsAdmin = await verifyToken(token); + if (userIsAdmin) { + const pipeline = [ + { + $group: { + _id: "$class", + total: { + $sum: 1, + }, + }, + }, + ]; + const results = await Reviews.aggregate<{ _id: string; total: number }>( + pipeline, + ); + + const ret = await Promise.all( + results.map(async (data) => { + const subNum = ( + await Classes.find( + { _id: data._id }, + { classSub: 1, classNum: 1 }, + ).exec() + )[0]; + const id = `${subNum.classSub} ${subNum.classNum}`; + return { _id: id, total: data.total }; + }), + ); + + return ret; + } + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'howManyReviewsEachClass' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } + }, +}; + +// Recreation of Python's defaultdict to be used in topSubjects method +export class DefaultDict { + [key: string]: T | Function; + + get(key: string): T | null { + const val = this[key]; + + if ( + Object.prototype.hasOwnProperty.call(this, key) && + typeof val !== "function" + ) { + return val; + } + return null; + } +} diff --git a/server/endpoints/Auth.ts b/server/endpoints/auth/Auth.ts similarity index 60% rename from server/endpoints/Auth.ts rename to server/endpoints/auth/Auth.ts index 12bd20680..667b9516b 100644 --- a/server/endpoints/Auth.ts +++ b/server/endpoints/auth/Auth.ts @@ -1,10 +1,12 @@ import { body } from "express-validator"; -import { OAuth2Client } from 'google-auth-library'; -import { Context, Endpoint } from "../endpoints"; -import { Students } from "../dbDefs"; -import { verifyToken } from "./utils"; +import { OAuth2Client } from "google-auth-library"; +import { Context, Endpoint } from "../../endpoints"; +import { Students } from "../../db/dbDefs"; +import { verifyToken } from "../utils/utils"; -const client = new OAuth2Client("836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com"); +const client = new OAuth2Client( + "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com", +); // The type for a search query interface AdminRequest { @@ -12,14 +14,14 @@ interface AdminRequest { } /** - * Returns true if [netid] matches the netid in the email of the JSON - * web token. False otherwise. - * This method authenticates the user token through the Google API. - * @param token: google auth token - * @param netid: netid to verify - * @requires that you have a handleVerifyError, like as follows: - * verify(token, function(){//do whatever}).catch(function(error){ - */ + * Returns true if [netid] matches the netid in the email of the JSON + * web token. False otherwise. + * This method authenticates the user token through the Google API. + * @param token: google auth token + * @param netid: netid to verify + * @requires that you have a handleVerifyError, like as follows: + * verify(token, function(){//do whatever}).catch(function(error){ + */ export const getVerificationTicket = async (token?: string) => { try { if (token === null) { @@ -29,7 +31,8 @@ export const getVerificationTicket = async (token?: string) => { } const ticket = await client.verifyIdToken({ idToken: token, - audience: "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com", + audience: + "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com", }); return ticket.getPayload(); } catch (error) { @@ -63,5 +66,6 @@ export const getUserByNetId = async (netId: string) => { */ export const tokenIsAdmin: Endpoint = { guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), + callback: async (ctx: Context, adminRequest: AdminRequest) => + await verifyToken(adminRequest.token), }; diff --git a/server/endpoints/Profile.ts b/server/endpoints/profile/Profile.ts similarity index 95% rename from server/endpoints/Profile.ts rename to server/endpoints/profile/Profile.ts index 0fdc2db0a..670787755 100644 --- a/server/endpoints/Profile.ts +++ b/server/endpoints/profile/Profile.ts @@ -1,8 +1,8 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../endpoints"; -import { ReviewDocument, Reviews, Students } from "../dbDefs"; +import { Context, Endpoint } from "../../endpoints"; +import { ReviewDocument, Reviews, Students } from "../../db/dbDefs"; -import { getVerificationTicket } from "./Auth"; +import { getVerificationTicket } from "../auth/Auth"; // The type of a query with a studentId export interface NetIdQuery { diff --git a/server/endpoints/Review.ts b/server/endpoints/review/Review.ts similarity index 69% rename from server/endpoints/Review.ts rename to server/endpoints/review/Review.ts index a147627a2..2cb62685f 100644 --- a/server/endpoints/Review.ts +++ b/server/endpoints/review/Review.ts @@ -1,10 +1,14 @@ import { body } from "express-validator"; import { getCrossListOR } from "common/CourseCard"; import { Review } from "common"; -import { Context, Endpoint } from "../endpoints"; -import { Classes, ReviewDocument, Reviews, Students } from "../dbDefs"; -import { getCourseById as getCourseByIdCallback, insertUser as insertUserCallback, JSONNonempty } from "./utils"; -import { getVerificationTicket } from "./Auth"; +import { Context, Endpoint } from "../../endpoints"; +import { Classes, ReviewDocument, Reviews, Students } from "../../db/dbDefs"; +import { + getCourseById as getCourseByIdCallback, + insertUser as insertUserCallback, + JSONNonempty, +} from "../utils/utils"; +import { getVerificationTicket } from "../auth/Auth"; import shortid = require("shortid"); @@ -50,14 +54,16 @@ export const sanitizeReview = (doc: ReviewDocument) => { * @param lst the list of reviews to sanitize. Possibly a singleton list. * @returns a copy of the reviews, but with the user id field removed. */ -export const sanitizeReviews = (lst: ReviewDocument[]) => (lst.map((doc) => sanitizeReview(doc))); +export const sanitizeReviews = (lst: ReviewDocument[]) => + lst.map((doc) => sanitizeReview(doc)); /** * Get a course with this course_id from the Classes collection */ export const getCourseById: Endpoint = { guard: [body("courseId").notEmpty().isAscii()], - callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg), + callback: async (ctx: Context, arg: CourseIdQuery) => + await getCourseByIdCallback(arg), }; /* @@ -65,10 +71,16 @@ export const getCourseById: Endpoint = { * See also: getCourseById above */ export const getCourseByInfo: Endpoint = { - guard: [body("number").notEmpty().isNumeric(), body("subject").notEmpty().isAscii()], + guard: [ + body("number").notEmpty().isNumeric(), + body("subject").notEmpty().isAscii(), + ], callback: async (ctx: Context, query: ClassByInfoQuery) => { try { - return await Classes.findOne({ classSub: query.subject, classNum: query.number }).exec(); + return await Classes.findOne({ + classSub: query.subject, + classNum: query.number, + }).exec(); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getCourseByInfo' endpoint"); @@ -89,7 +101,11 @@ export const getReviewsByCourseId: Endpoint = { const course = await getCourseByIdCallback(courseId); if (course) { const crossListOR = getCrossListOR(course); - const reviews = await Reviews.find({ visible: 1, reported: 0, $or: crossListOR }, {}, { sort: { date: -1 }, limit: 700 }).exec(); + const reviews = await Reviews.find( + { visible: 1, reported: 0, $or: crossListOR }, + {}, + { sort: { date: -1 }, limit: 700 }, + ).exec(); return sanitizeReviews(reviews); } @@ -112,7 +128,8 @@ export const getReviewsByCourseId: Endpoint = { */ export const insertUser: Endpoint = { guard: [body("googleObject").notEmpty()], - callback: async (ctx: Context, arg: InsertUserRequest) => await insertUserCallback(arg), + callback: async (ctx: Context, arg: InsertUserRequest) => + await insertUserCallback(arg), }; /** @@ -122,8 +139,19 @@ export const insertUser: Endpoint = { * Returns 1 on a success */ export const insertReview: Endpoint = { - guard: [body("token").notEmpty().isAscii(), body("classId").notEmpty().isAscii()] - .concat(JSONNonempty("review", ["text", "difficulty", "rating", "workload", "professors", "isCovid"])), + guard: [ + body("token").notEmpty().isAscii(), + body("classId").notEmpty().isAscii(), + ].concat( + JSONNonempty("review", [ + "text", + "difficulty", + "rating", + "workload", + "professors", + "isCovid", + ]), + ), callback: async (ctx: Context, request: InsertReviewRequest) => { try { const { token } = request; @@ -140,11 +168,15 @@ export const insertReview: Endpoint = { await insertUserCallback({ googleObject: ticket }); const netId = ticket.email.replace("@cornell.edu", ""); - const student = (await Students.findOne({ netId })); + const student = await Students.findOne({ netId }); const related = await Reviews.find({ class: classId }); if (related.find((v) => v.text === review.text)) { - return { resCode: 1, error: "Review is a duplicate of an already existing review for this class!" }; + return { + resCode: 1, + error: + "Review is a duplicate of an already existing review for this class!", + }; } try { @@ -169,8 +201,13 @@ export const insertReview: Endpoint = { await fullReview.save(); - const newReviews = student.reviews ? student.reviews.concat([fullReview._id]) : [fullReview._id]; - await Students.updateOne({ netId }, { $set: { reviews: newReviews } }).exec(); + const newReviews = student.reviews + ? student.reviews.concat([fullReview._id]) + : [fullReview._id]; + await Students.updateOne( + { netId }, + { $set: { reviews: newReviews } }, + ).exec(); return { resCode: 1, errMsg: "" }; } catch (error) { @@ -181,7 +218,10 @@ export const insertReview: Endpoint = { } else { // eslint-disable-next-line no-console console.log("Error: non-Cornell email attempted to insert review"); - return { resCode: 1, error: "Error: non-Cornell email attempted to insert review" }; + return { + resCode: 1, + error: "Error: non-Cornell email attempted to insert review", + }; } } catch (error) { // eslint-disable-next-line no-console @@ -211,26 +251,52 @@ export const updateLiked: Endpoint = { if (ticket.hd === "cornell.edu") { await insertUserCallback({ googleObject: ticket }); const netId = ticket.email.replace("@cornell.edu", ""); - const student = (await Students.findOne({ netId })); + const student = await Students.findOne({ netId }); // removing like - if (student.likedReviews !== undefined && student.likedReviews.includes(review.id)) { - await Students.updateOne({ netId }, { $pull: { likedReviews: review.id } }); + if ( + student.likedReviews !== undefined && + student.likedReviews.includes(review.id) + ) { + await Students.updateOne( + { netId }, + { $pull: { likedReviews: review.id } }, + ); if (review.likes === undefined) { - await Reviews.updateOne({ _id: request.id }, { $set: { likes: 0 } }, { $pull: { likedBy: student.netId } }).exec(); + await Reviews.updateOne( + { _id: request.id }, + { $set: { likes: 0 } }, + { $pull: { likedBy: student.netId } }, + ).exec(); } else { // bound the rating at 0 - await Reviews.updateOne({ _id: request.id }, - { $set: { likes: Math.max(0, review.likes - 1) } }, { $pull: { likedBy: student.netId } }).exec(); + await Reviews.updateOne( + { _id: request.id }, + { $set: { likes: Math.max(0, review.likes - 1) } }, + { $pull: { likedBy: student.netId } }, + ).exec(); } - } else { // adding like - await Students.updateOne({ netId: student.netId }, { $push: { likedReviews: review.id } }); + } else { + // adding like + await Students.updateOne( + { netId: student.netId }, + { $push: { likedReviews: review.id } }, + ); if (review.likes === undefined) { - await Reviews.updateOne({ _id: request.id }, { $set: { likes: 1 }, $push: { likedBy: student.id } }).exec(); + await Reviews.updateOne( + { _id: request.id }, + { $set: { likes: 1 }, $push: { likedBy: student.id } }, + ).exec(); } else { - await Reviews.updateOne({ _id: request.id }, { $set: { likes: review.likes + 1 }, $push: { likedBy: student.id } }).exec(); + await Reviews.updateOne( + { _id: request.id }, + { + $set: { likes: review.likes + 1 }, + $push: { likedBy: student.id }, + }, + ).exec(); } } @@ -238,7 +304,10 @@ export const updateLiked: Endpoint = { return { resCode: 0, review: sanitizeReview(review) }; } - return { resCode: 1, error: "Error: non-Cornell email attempted to insert review" }; + return { + resCode: 1, + error: "Error: non-Cornell email attempted to insert review", + }; } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'incrementLike' method"); @@ -259,11 +328,16 @@ export const userHasLiked: Endpoint = { if (!ticket) return { resCode: 1, error: "Missing verification ticket" }; - if (ticket.hd !== "cornell.edu") return { resCode: 1, error: "Error: non-Cornell email attempted to insert review" }; + if (ticket.hd !== "cornell.edu") { + return { + resCode: 1, + error: "Error: non-Cornell email attempted to insert review", + }; + } await insertUserCallback({ googleObject: ticket }); const netId = ticket.email.replace("@cornell.edu", ""); - const student = (await Students.findOne({ netId })); + const student = await Students.findOne({ netId }); if (student.likedReviews && student.likedReviews.includes(review.id)) { return { resCode: 0, hasLiked: true }; diff --git a/server/endpoints/Search.ts b/server/endpoints/search/Search.ts similarity index 78% rename from server/endpoints/Search.ts rename to server/endpoints/search/Search.ts index 42093e3c1..3251b75f3 100644 --- a/server/endpoints/Search.ts +++ b/server/endpoints/search/Search.ts @@ -1,7 +1,7 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../endpoints"; -import { Classes, Subjects, Professors } from "../dbDefs"; +import { Context, Endpoint } from "../../endpoints"; +import { Classes, Subjects, Professors } from "../../db/dbDefs"; // The type for a search query interface Search { @@ -38,9 +38,13 @@ export const editDistance = (a, b) => { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution - Math.min(matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j] + 1)); // deletion + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + Math.min( + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1, + ), + ); // deletion } } } @@ -53,8 +57,10 @@ const courseSort = (query) => (a, b) => { const aCourseStr = `${a.classSub} ${a.classNum}`; const bCourseStr = `${b.classSub} ${b.classNum}`; const queryLen = query.length; - return editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)); + return ( + editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - + editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) + ); }; // Helper to check if a string is a subject code @@ -65,34 +71,44 @@ export const isSubShorthand = async (sub: string) => { }; // helper to format search within a subject -const searchWithinSubject = (sub: string, remainder: string) => Classes.find( - { classSub: sub, classFull: { $regex: `.*${remainder}.*`, $options: '-i' } }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, -).exec(); +const searchWithinSubject = (sub: string, remainder: string) => + Classes.find( + { + classSub: sub, + classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, + }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); export const regexClassesSearch = async (searchString) => { try { - if (searchString !== undefined && searchString !== '') { + if (searchString !== undefined && searchString !== "") { // check if first digit is a number. Catches searchs like "1100" // if so, search only through the course numbers and return classes ordered by full name const indexFirstDigit = searchString.search(/\d/); if (indexFirstDigit === 0) { // console.log("only numbers") return Classes.find( - { classNum: { $regex: `.*${searchString}.*`, $options: '-i' } }, + { classNum: { $regex: `.*${searchString}.*`, $options: "-i" } }, {}, { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec().then((classes) => classes.sort(courseSort(searchString))); + ) + .exec() + .then((classes) => classes.sort(courseSort(searchString))); } // check if searchString is a subject, if so return only classes with this subject. Catches searches like "CS" if (await isSubShorthand(searchString)) { - return Classes.find({ classSub: searchString }, {}, { sort: { classFull: 1 }, limit: 200, reactive: false }).exec(); + return Classes.find( + { classSub: searchString }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); } // check if text before space is subject, if so search only classes with this subject. // Speeds up searches like "CS 1110" - const indexFirstSpace = searchString.search(' '); + const indexFirstSpace = searchString.search(" "); if (indexFirstSpace !== -1) { const strBeforeSpace = searchString.substring(0, indexFirstSpace); const strAfterSpace = searchString.substring(indexFirstSpace + 1); @@ -117,12 +133,17 @@ export const regexClassesSearch = async (searchString) => { // last resort, search everything // console.log("nothing matches"); return Classes.find( - { classFull: { $regex: `.*${searchString}.*`, $options: '-i' } }, - {}, { sort: { classFull: 1 }, limit: 200, reactive: false }, + { classFull: { $regex: `.*${searchString}.*`, $options: "-i" } }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, ).exec(); } // console.log("no search"); - return Classes.find({}, {}, { sort: { classFull: 1 }, limit: 200, reactive: false }).exec(); + return Classes.find( + {}, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getClassesByQuery' method"); @@ -167,7 +188,11 @@ export const getSubjectsByQuery: Endpoint = { guard: [body("query").notEmpty().isAscii()], callback: async (ctx: Context, search: Search) => { try { - return await Subjects.find({ $text: { $search: search.query } }, { score: { $meta: "textScore" } }, { sort: { score: { $meta: "textScore" } } }).exec(); + return await Subjects.find( + { $text: { $search: search.query } }, + { score: { $meta: "textScore" } }, + { sort: { score: { $meta: "textScore" } } }, + ).exec(); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getSubjectsByQuery' endpoint"); @@ -185,7 +210,11 @@ export const getProfessorsByQuery: Endpoint = { guard: [body("query").notEmpty().isAscii()], callback: async (ctx: Context, search: Search) => { try { - return await Professors.find({ $text: { $search: search.query } }, { score: { $meta: "textScore" } }, { sort: { score: { $meta: "textScore" } } }).exec(); + return await Professors.find( + { $text: { $search: search.query } }, + { score: { $meta: "textScore" } }, + { sort: { score: { $meta: "textScore" } } }, + ).exec(); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getProfessorsByQuery' endpoint"); @@ -223,8 +252,10 @@ export const getCoursesByProfessor: Endpoint = { let courses = []; const regex = new RegExp(/^(?=.*[A-Z0-9])/i); if (regex.test(search.query)) { - const professorRegex = search.query.replace('+', '.*.'); - courses = await Classes.find({ classProfessors: { $regex: professorRegex, $options: "i" } }).exec(); + const professorRegex = search.query.replace("+", ".*."); + courses = await Classes.find({ + classProfessors: { $regex: professorRegex, $options: "i" }, + }).exec(); } return courses; } catch (error) { diff --git a/server/endpoints/AdminActions.test.ts b/server/endpoints/test/AdminActions.test.ts similarity index 65% rename from server/endpoints/AdminActions.test.ts rename to server/endpoints/test/AdminActions.test.ts index df505aaf1..5cc3a777e 100644 --- a/server/endpoints/AdminActions.test.ts +++ b/server/endpoints/test/AdminActions.test.ts @@ -1,15 +1,17 @@ -import mongoose from 'mongoose'; -import { MongoMemoryServer } from 'mongodb-memory-server'; +import mongoose from "mongoose"; +import { MongoMemoryServer } from "mongodb-memory-server"; import express from "express"; -import axios from 'axios'; +import axios from "axios"; -import { configure } from "../endpoints"; -import { Classes, Reviews } from "../dbDefs"; -import * as Utils from "./utils"; +import { configure } from "../../endpoints"; +import { Classes, Reviews } from "../../db/dbDefs"; +import * as Utils from "../utils/utils"; let mongoServer: MongoMemoryServer; let serverCloseHandle; -const mockVerification = jest.spyOn(Utils, 'verifyToken').mockImplementation(async (token?: string) => true); +const mockVerification = jest + .spyOn(Utils, "verifyToken") + .mockImplementation(async (token?: string) => true); const testingPort = 47728; @@ -49,12 +51,14 @@ const reviewToUndoReport = new Reviews({ workload: 5, }); - beforeAll(async () => { // get mongoose all set up mongoServer = new MongoMemoryServer(); const mongoUri = await mongoServer.getUri(); - await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }); + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); // Set up a mock version of the v2 endpoints to test against const app = express(); serverCloseHandle = app.listen(testingPort); @@ -74,16 +78,22 @@ afterAll(async () => { mockVerification.mockRestore(); }); -describe('tests', () => { - it('fetchReviewableClasses-works', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/fetchReviewableClasses`, { token: "non-empty" }); +describe("tests", () => { + it("fetchReviewableClasses-works", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/fetchReviewableClasses`, + { token: "non-empty" }, + ); const ids = res.data.result.map((e) => e._id); expect(ids.includes(reviewToUndoReportId)).toBeTruthy(); expect(ids.includes(sampleReviewId)).toBeTruthy(); }); - it('makeReviewVisible-works', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/makeReviewVisible`, { review: sampleReview, token: "non-empty" }); + it("makeReviewVisible-works", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/makeReviewVisible`, + { review: sampleReview, token: "non-empty" }, + ); expect(res.data.result.resCode).toEqual(1); const course = await Classes.findOne({ _id: newCourse1._id }).exec(); expect(course.classDifficulty).toEqual(sampleReview.difficulty); @@ -91,16 +101,22 @@ describe('tests', () => { expect(course.classRating).toEqual(sampleReview.rating); }); - it('undoReportReview-works', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/undoReportReview`, { review: reviewToUndoReport, token: "non empty" }); + it("undoReportReview-works", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/undoReportReview`, + { review: reviewToUndoReport, token: "non empty" }, + ); expect(res.data.result.resCode).toEqual(1); const reviewFromDb = await Reviews.findById(reviewToUndoReport._id).exec(); expect(reviewFromDb.visible).toEqual(1); await reviewToUndoReport.remove(); }); - it('removeReview-works', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/removeReview`, { review: sampleReview, token: "non-empty" }); + it("removeReview-works", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/removeReview`, + { review: sampleReview, token: "non-empty" }, + ); expect(res.data.result.resCode).toEqual(1); const course = await Classes.findById(newCourse1._id); expect(course.classDifficulty).toEqual(null); diff --git a/server/endpoints/AdminChart.test.ts b/server/endpoints/test/AdminChart.test.ts similarity index 58% rename from server/endpoints/AdminChart.test.ts rename to server/endpoints/test/AdminChart.test.ts index 91a130fad..61f5545f5 100644 --- a/server/endpoints/AdminChart.test.ts +++ b/server/endpoints/test/AdminChart.test.ts @@ -1,13 +1,12 @@ -import axios from 'axios'; -import { TokenPayload } from 'google-auth-library'; +import axios from "axios"; +import { TokenPayload } from "google-auth-library"; -import { Class, Review, Student, Subject } from 'common'; -import * as Auth from "./Auth"; -import TestingServer, { testingPort } from './TestServer'; +import { Class, Review, Student, Subject } from "common"; +import * as Auth from "../auth/Auth"; +import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); - export const testClasses: Class[] = [ { _id: "oH37S3mJ4eAsktypy", @@ -16,13 +15,39 @@ export const testClasses: Class[] = [ classTitle: "Object-Oriented Programming and Data Structures", classPrereq: [], classFull: "cs 2110 object-oriented programming and data structures", - classSems: ["FA14", "SP15", "SU15", "FA15", "SP16", "SU16", "FA16", "SP17", - "SU17", "FA17", "SP18", "FA18", "SU18", "SP19", "FA19", "SU19"], + classSems: [ + "FA14", + "SP15", + "SU15", + "FA15", + "SP16", + "SU16", + "FA16", + "SP17", + "SU17", + "FA17", + "SP18", + "FA18", + "SU18", + "SP19", + "FA19", + "SU19", + ], crossList: ["q75SxmqkTFEfaJwZ3"], - classProfessors: ["David Gries", "Douglas James", "Siddhartha Chaudhuri", - "Graeme Bailey", "John Foster", "Ross Tate", "Michael George", - "Eleanor Birrell", "Adrian Sampson", "Natacha Crooks", "Anne Bracy", - "Michael Clarkson"], + classProfessors: [ + "David Gries", + "Douglas James", + "Siddhartha Chaudhuri", + "Graeme Bailey", + "John Foster", + "Ross Tate", + "Michael George", + "Eleanor Birrell", + "Adrian Sampson", + "Natacha Crooks", + "Anne Bracy", + "Michael Clarkson", + ], classDifficulty: 2.9, classRating: null, classWorkload: 3, @@ -34,8 +59,24 @@ export const testClasses: Class[] = [ classTitle: "Honors Object-Oriented Programming and Data Structures", classPrereq: [], classFull: "cs 2112 Honors object-oriented programming and data structures", - classSems: ["FA14", "SP15", "SU15", "FA15", "SP16", "SU16", "FA16", "SP17", - "SU17", "FA17", "SP18", "FA18", "SU18", "SP19", "FA19", "SU19"], + classSems: [ + "FA14", + "SP15", + "SU15", + "FA15", + "SP16", + "SU16", + "FA16", + "SP17", + "SU17", + "FA17", + "SP18", + "FA18", + "SU18", + "SP19", + "FA19", + "SU19", + ], crossList: [], classProfessors: ["Andrew Myers"], classDifficulty: 5.0, @@ -49,8 +90,24 @@ export const testClasses: Class[] = [ classTitle: "Intro to real analysis", classPrereq: [], classFull: "math 3110 Intro to real analysis", - classSems: ["FA14", "SP15", "SU15", "FA15", "SP16", "SU16", "FA16", "SP17", - "SU17", "FA17", "SP18", "FA18", "SU18", "SP19", "FA19", "SU19"], + classSems: [ + "FA14", + "SP15", + "SU15", + "FA15", + "SP16", + "SU16", + "FA16", + "SP17", + "SU17", + "FA17", + "SP18", + "FA18", + "SU18", + "SP19", + "FA19", + "SU19", + ], crossList: [], classProfessors: ["Saloff-Coste"], classDifficulty: 3.9, @@ -121,7 +178,7 @@ const testSubjects: Subject[] = [ ]; const validTokenPayload: TokenPayload = { - email: 'dti1@cornell.edu', + email: "dti1@cornell.edu", iss: undefined, sub: undefined, iat: undefined, @@ -144,11 +201,18 @@ const testStudents: Student[] = [ }, ]; -const mockVerificationTicket = jest.spyOn(Auth, 'getVerificationTicket') +const mockVerificationTicket = jest + .spyOn(Auth, "getVerificationTicket") .mockImplementation(async (token: string) => validTokenPayload); beforeAll(async () => { - await testServer.setUpDB(testReviews, testStudents, testClasses, undefined, testSubjects); + await testServer.setUpDB( + testReviews, + testStudents, + testClasses, + undefined, + testSubjects, + ); }); afterAll(async () => { @@ -156,39 +220,64 @@ afterAll(async () => { await testServer.shutdownTestingServer(); }); -describe('tests', () => { +describe("tests", () => { it("topSubjects", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/topSubjects`, { token: "token" }); - const match = [['Computer Science', 3], ['Mathematics', 1]]; + const res = await axios.post( + `http://localhost:${testingPort}/v2/topSubjects`, + { token: "token" }, + ); + const match = [ + ["Computer Science", 3], + ["Mathematics", 1], + ]; match.forEach((obj) => { expect(res.data.result).toContainEqual(obj); }); }); - it('totalReviews', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/totalReviews`, { token: "token" }); + it("totalReviews", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/totalReviews`, + { token: "token" }, + ); expect(res.data.result).toBe(testReviews.length); }); it("howManyReviewsEachClass", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/howManyReviewsEachClass`, { token: "token" }); - const match = [{ _id: 'cs 2110', total: 2 }, { _id: "cs 2112", total: 1 }, { _id: "math 3110", total: 1 }]; + const res = await axios.post( + `http://localhost:${testingPort}/v2/howManyReviewsEachClass`, + { token: "token" }, + ); + const match = [ + { _id: "cs 2110", total: 2 }, + { _id: "cs 2112", total: 1 }, + { _id: "math 3110", total: 1 }, + ]; match.forEach((obj) => { expect(res.data.result).toContainEqual(obj); }); }); it("howManyEachClass", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/howManyEachClass`, { token: "token" }); - const match = [{ _id: 'cs', total: 2 }, { _id: "math", total: 1 }]; + const res = await axios.post( + `http://localhost:${testingPort}/v2/howManyEachClass`, + { token: "token" }, + ); + const match = [ + { _id: "cs", total: 2 }, + { _id: "math", total: 1 }, + ]; match.forEach((obj) => { expect(res.data.result).toContainEqual(obj); }); }); it("getReviewsOverTimeTop15", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getReviewsOverTimeTop15`, { token: "token", step: 12, range: 12 }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/getReviewsOverTimeTop15`, + { token: "token", step: 12, range: 12 }, + ); expect(res.data.result.math.length).toBeGreaterThan(0); expect(res.data.result.cs.length).toBeGreaterThan(0); diff --git a/server/endpoints/Auth.test.ts b/server/endpoints/test/Auth.test.ts similarity index 52% rename from server/endpoints/Auth.test.ts rename to server/endpoints/test/Auth.test.ts index 2786c56d9..d2b0ec789 100644 --- a/server/endpoints/Auth.test.ts +++ b/server/endpoints/test/Auth.test.ts @@ -1,14 +1,14 @@ -import axios from 'axios'; -import { TokenPayload } from 'google-auth-library/build/src/auth/loginticket'; +import axios from "axios"; +import { TokenPayload } from "google-auth-library/build/src/auth/loginticket"; -import { Student } from 'common'; -import * as Auth from "./Auth"; -import TestingServer, { testingPort } from './TestServer'; +import { Student } from "common"; +import * as Auth from "../auth/Auth"; +import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); const invalidTokenPayload: TokenPayload = { - email: 'cv4620@cornell.edu', + email: "cv4620@cornell.edu", iss: undefined, sub: undefined, iat: undefined, @@ -17,7 +17,7 @@ const invalidTokenPayload: TokenPayload = { }; const validTokenPayload: TokenPayload = { - email: 'dti1@cornell.edu', + email: "dti1@cornell.edu", iss: undefined, sub: undefined, iat: undefined, @@ -52,25 +52,39 @@ beforeAll(async () => { likedReviews: [], }, ]; - await testServer.setUpDB(undefined, testStudents, undefined, undefined, undefined); + await testServer.setUpDB( + undefined, + testStudents, + undefined, + undefined, + undefined, + ); }); afterAll(async () => { await testServer.shutdownTestingServer(); }); -describe('tests', () => { - it('tokenIsAdmin-works', async () => { - const mockVerificationTicket = jest.spyOn(Auth, 'getVerificationTicket').mockImplementation(async (token?: string) => { - if (token === 'fakeTokenDti1') { - return validTokenPayload; - } - return invalidTokenPayload; - }); +describe("tests", () => { + it("tokenIsAdmin-works", async () => { + const mockVerificationTicket = jest + .spyOn(Auth, "getVerificationTicket") + .mockImplementation(async (token?: string) => { + if (token === "fakeTokenDti1") { + return validTokenPayload; + } + return invalidTokenPayload; + }); - const failRes = await axios.post(`http://localhost:${testingPort}/v2/tokenIsAdmin`, { token: "fakeTokencv4620" }); + const failRes = await axios.post( + `http://localhost:${testingPort}/v2/tokenIsAdmin`, + { token: "fakeTokencv4620" }, + ); expect(failRes.data.result).toEqual(false); - const successRes = await axios.post(`http://localhost:${testingPort}/v2/tokenIsAdmin`, { token: "fakeTokenDti1" }); + const successRes = await axios.post( + `http://localhost:${testingPort}/v2/tokenIsAdmin`, + { token: "fakeTokenDti1" }, + ); expect(successRes.data.result).toEqual(true); mockVerificationTicket.mockRestore(); diff --git a/server/endpoints/Profile.test.ts b/server/endpoints/test/Profile.test.ts similarity index 63% rename from server/endpoints/Profile.test.ts rename to server/endpoints/test/Profile.test.ts index cea9f717a..c8e5c6e56 100644 --- a/server/endpoints/Profile.test.ts +++ b/server/endpoints/test/Profile.test.ts @@ -1,14 +1,13 @@ - /* eslint-disable import/prefer-default-export */ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; -import axios from 'axios'; -import { TokenPayload } from 'google-auth-library'; +import axios from "axios"; +import { TokenPayload } from "google-auth-library"; -import { Review, Student, Class, Subject, Professor } from 'common'; -import * as Auth from "./Auth"; +import { Review, Student, Class, Subject, Professor } from "common"; +import * as Auth from "../auth/Auth"; -import TestingServer, { testingPort } from './TestServer'; +import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); @@ -20,13 +19,39 @@ const testClasses: Class[] = [ classTitle: "Object-Oriented Programming and Data Structures", classPrereq: [], classFull: "cs 2110 object-oriented programming and data structures", - classSems: ["FA14", "SP15", "SU15", "FA15", "SP16", "SU16", "FA16", "SP17", - "SU17", "FA17", "SP18", "FA18", "SU18", "SP19", "FA19", "SU19"], + classSems: [ + "FA14", + "SP15", + "SU15", + "FA15", + "SP16", + "SU16", + "FA16", + "SP17", + "SU17", + "FA17", + "SP18", + "FA18", + "SU18", + "SP19", + "FA19", + "SU19", + ], crossList: ["q75SxmqkTFEfaJwZ3"], - classProfessors: ["David Gries", "Douglas James", "Siddhartha Chaudhuri", - "Graeme Bailey", "John Foster", "Ross Tate", "Michael George", - "Eleanor Birrell", "Adrian Sampson", "Natacha Crooks", "Anne Bracy", - "Michael Clarkson"], + classProfessors: [ + "David Gries", + "Douglas James", + "Siddhartha Chaudhuri", + "Graeme Bailey", + "John Foster", + "Ross Tate", + "Michael George", + "Eleanor Birrell", + "Adrian Sampson", + "Natacha Crooks", + "Anne Bracy", + "Michael Clarkson", + ], classDifficulty: 2.9, classRating: null, classWorkload: 3, @@ -121,49 +146,57 @@ const testUsers: Student[] = [ ]; beforeAll(async () => { - await testServer.setUpDB(testReviews, testUsers, testClasses, undefined, undefined); + await testServer.setUpDB( + testReviews, + testUsers, + testClasses, + undefined, + undefined, + ); }); const testGetTotalLikes = 7; -const testGetReviews1 = [{ - _id: "4Y8k7DnX3PLNdwRPr", - text: "review text for cs 2110", - user: "User1234", - difficulty: 1, - class: "oH37S3mJ4eAsktypy", - date: new Date(), - visible: 1, - reported: 0, - likes: 2, - likedBy: [], -}, -{ - _id: "4Y8k7DnX3PLNdwRPq", - text: "review text for cs 2110 number 2", - user: "User1234", - difficulty: 1, - class: "oH37S3mJ4eAsktypy", - date: new Date(), - visible: 1, - reported: 0, - likes: 0, - likedBy: [], -}, -{ - _id: "3yMwTbiyd4MZLPQJF", - text: "review text for cs 3110", - user: "User1234", - difficulty: 3, - class: "cJSmM8bnwm2QFnmAn", - date: new Date(), - visible: 1, - reported: 0, - likes: 5, - likedBy: [], -}]; +const testGetReviews1 = [ + { + _id: "4Y8k7DnX3PLNdwRPr", + text: "review text for cs 2110", + user: "User1234", + difficulty: 1, + class: "oH37S3mJ4eAsktypy", + date: new Date(), + visible: 1, + reported: 0, + likes: 2, + likedBy: [], + }, + { + _id: "4Y8k7DnX3PLNdwRPq", + text: "review text for cs 2110 number 2", + user: "User1234", + difficulty: 1, + class: "oH37S3mJ4eAsktypy", + date: new Date(), + visible: 1, + reported: 0, + likes: 0, + likedBy: [], + }, + { + _id: "3yMwTbiyd4MZLPQJF", + text: "review text for cs 3110", + user: "User1234", + difficulty: 3, + class: "cJSmM8bnwm2QFnmAn", + date: new Date(), + visible: 1, + reported: 0, + likes: 5, + likedBy: [], + }, +]; const validTokenPayload: TokenPayload = { - email: 'dti1@cornell.edu', + email: "dti1@cornell.edu", iss: undefined, sub: undefined, iat: undefined, @@ -172,7 +205,8 @@ const validTokenPayload: TokenPayload = { hd: "cornell.edu", }; -const mockVerificationTicket = jest.spyOn(Auth, 'getVerificationTicket') +const mockVerificationTicket = jest + .spyOn(Auth, "getVerificationTicket") .mockImplementation(async (token?: string) => validTokenPayload); afterAll(async () => { @@ -205,15 +239,19 @@ describe("tests", () => { expect(res.data.result.message).toBe("No reviews object were associated."); expect(res.data.result.code).toBe(500); }); - it('getTotalLikesByStudentId - counting the number of likes a student got on their reviews', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getTotalLikesByStudentId`, - { netId: "cv4620" }); + it("getTotalLikesByStudentId - counting the number of likes a student got on their reviews", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/getTotalLikesByStudentId`, + { netId: "cv4620" }, + ); expect(res.data.result.message).toBe(testGetTotalLikes); expect(res.data.result.code).toBe(200); }); - it('getReviewsByStudentId - returning a review object list that a student wrote', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getReviewsByStudentId`, - { netId: "cv4620" }); + it("getReviewsByStudentId - returning a review object list that a student wrote", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/getReviewsByStudentId`, + { netId: "cv4620" }, + ); expect(res.data.result.message.length).toBe(testGetReviews1.length); expect(res.data.result.code).toBe(200); }); diff --git a/server/endpoints/Review.test.ts b/server/endpoints/test/Review.test.ts similarity index 53% rename from server/endpoints/Review.test.ts rename to server/endpoints/test/Review.test.ts index 5259e626b..e7514ccba 100644 --- a/server/endpoints/Review.test.ts +++ b/server/endpoints/test/Review.test.ts @@ -1,11 +1,11 @@ /* eslint-disable import/prefer-default-export */ -import axios from 'axios'; -import { TokenPayload } from 'google-auth-library'; +import axios from "axios"; +import { TokenPayload } from "google-auth-library"; -import { Review } from 'common'; -import { Reviews, Students } from "../dbDefs"; -import * as Auth from "./Auth"; -import TestingServer from './TestServer'; +import { Review } from "common"; +import { Reviews, Students } from "../../db/dbDefs"; +import * as Auth from "../auth/Auth"; +import TestingServer from "./TestServer"; const testingPort = 8080; const testServer = new TestingServer(testingPort); @@ -18,13 +18,39 @@ const testClasses = [ classTitle: "Object-Oriented Programming and Data Structures", classPrereq: [], classFull: "cs 2110 object-oriented programming and data structures", - classSems: ["FA14", "SP15", "SU15", "FA15", "SP16", "SU16", "FA16", "SP17", - "SU17", "FA17", "SP18", "FA18", "SU18", "SP19", "FA19", "SU19"], + classSems: [ + "FA14", + "SP15", + "SU15", + "FA15", + "SP16", + "SU16", + "FA16", + "SP17", + "SU17", + "FA17", + "SP18", + "FA18", + "SU18", + "SP19", + "FA19", + "SU19", + ], crossList: ["q75SxmqkTFEfaJwZ3"], - classProfessors: ["David Gries", "Douglas James", "Siddhartha Chaudhuri", - "Graeme Bailey", "John Foster", "Ross Tate", "Michael George", - "Eleanor Birrell", "Adrian Sampson", "Natacha Crooks", "Anne Bracy", - "Michael Clarkson"], + classProfessors: [ + "David Gries", + "Douglas James", + "Siddhartha Chaudhuri", + "Graeme Bailey", + "John Foster", + "Ross Tate", + "Michael George", + "Eleanor Birrell", + "Adrian Sampson", + "Natacha Crooks", + "Anne Bracy", + "Michael Clarkson", + ], classDifficulty: 2.9, classRating: null, classWorkload: 3, @@ -58,7 +84,7 @@ const testReviews: Review[] = [ ]; const validTokenPayload: TokenPayload = { - email: 'dti1@cornell.edu', + email: "dti1@cornell.edu", iss: undefined, sub: undefined, iat: undefined, @@ -67,12 +93,19 @@ const validTokenPayload: TokenPayload = { hd: "cornell.edu", }; -const mockVerificationTicket = jest.spyOn(Auth, 'getVerificationTicket') +const mockVerificationTicket = jest + .spyOn(Auth, "getVerificationTicket") .mockImplementation(async (token?: string) => validTokenPayload); beforeAll(async () => { // get mongoose all set up - await testServer.setUpDB(testReviews, undefined, testClasses, undefined, undefined); + await testServer.setUpDB( + testReviews, + undefined, + testClasses, + undefined, + undefined, + ); }); afterAll(async () => { @@ -80,48 +113,84 @@ afterAll(async () => { await testServer.shutdownTestingServer(); }); -describe('tests', () => { - it('getReviewsByCourseId - getting review of class that exists (cs 2110)', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getReviewsByCourseId`, { courseId: "oH37S3mJ4eAsktypy" }); +describe("tests", () => { + it("getReviewsByCourseId - getting review of class that exists (cs 2110)", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/getReviewsByCourseId`, + { courseId: "oH37S3mJ4eAsktypy" }, + ); expect(res.data.result.length).toBe(testReviews.length); const classOfReviews = testReviews.map((r) => r.class); - expect(res.data.result.map((r) => r.class).sort()).toEqual(classOfReviews.sort()); + expect(res.data.result.map((r) => r.class).sort()).toEqual( + classOfReviews.sort(), + ); }); it("getReviewsByCourseId - getting review for a class that does not exist", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getReviewsByCourseId`, { courseId: "ert" }); - expect(res.data.result).toEqual({ error: 'Malformed Query' }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/getReviewsByCourseId`, + { courseId: "ert" }, + ); + expect(res.data.result).toEqual({ error: "Malformed Query" }); }); it("getCourseById - getting cs2110", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getCourseById`, { courseId: "oH37S3mJ4eAsktypy" }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/getCourseById`, + { courseId: "oH37S3mJ4eAsktypy" }, + ); expect(res.data.result._id).toBe("oH37S3mJ4eAsktypy"); - expect(res.data.result.classTitle).toBe("Object-Oriented Programming and Data Structures"); + expect(res.data.result.classTitle).toBe( + "Object-Oriented Programming and Data Structures", + ); }); - it('getCourseById - class does not exist', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getCourseById`, { courseId: "blah" }); + it("getCourseById - class does not exist", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/getCourseById`, + { courseId: "blah" }, + ); expect(res.data.result).toBe(null); }); - it('getCourseByInfo - getting cs2110', async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { subject: "cs", number: "2110" }); + it("getCourseByInfo - getting cs2110", async () => { + const res = await axios.post( + `http://localhost:${testingPort}/v2/getCourseByInfo`, + { subject: "cs", number: "2110" }, + ); expect(res.data.result._id).toBe("oH37S3mJ4eAsktypy"); - expect(res.data.result.classTitle).toBe("Object-Oriented Programming and Data Structures"); + expect(res.data.result.classTitle).toBe( + "Object-Oriented Programming and Data Structures", + ); }); - it('getCourseByInfo - demonstrate regex irrelevance', async () => { + it("getCourseByInfo - demonstrate regex irrelevance", async () => { // Will not accept non-numeric: - const res1 = await axios.post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { subject: "Vainamoinen", number: "ab2187c" }).catch((e) => e); + const res1 = await axios + .post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { + subject: "Vainamoinen", + number: "ab2187c", + }) + .catch((e) => e); expect(res1.message).toBe("Request failed with status code 400"); // Will not accept non-ascii: - const res2 = await axios.post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { subject: "向岛维纳默宁", number: "1234" }).catch((e) => e); + const res2 = await axios + .post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { + subject: "向岛维纳默宁", + number: "1234", + }) + .catch((e) => e); expect(res2.message).toBe("Request failed with status code 400"); // Both also does not work: - const res3 = await axios.post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { subject: "向岛维纳默宁", number: "ab2187c" }).catch((e) => e); + const res3 = await axios + .post(`http://localhost:${testingPort}/v2/getCourseByInfo`, { + subject: "向岛维纳默宁", + number: "ab2187c", + }) + .catch((e) => e); expect(res3.message).toBe("Request failed with status code 400"); }); @@ -140,7 +209,10 @@ describe('tests', () => { rating: 4, }; - const res = await axios.post(`http://localhost:${testingPort}/v2/insertReview`, { classId: cs2110Id, review: reviewToInsert, token: "fakeTokenDti1" }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/insertReview`, + { classId: cs2110Id, review: reviewToInsert, token: "fakeTokenDti1" }, + ); expect(res.data.result.resCode).toBe(1); const reviews = await Reviews.find({ text: reviewToInsert.text }).exec(); expect(reviews.length).toBe(1); @@ -154,12 +226,18 @@ describe('tests', () => { }); it("like/dislike - increment and decrement", async () => { - const res1 = await axios.post(`http://localhost:${testingPort}/v2/updateLiked`, { id: "4Y8k7DnX3PLNdwRPr", token: "fakeTokenDti1" }); + const res1 = await axios.post( + `http://localhost:${testingPort}/v2/updateLiked`, + { id: "4Y8k7DnX3PLNdwRPr", token: "fakeTokenDti1" }, + ); expect(res1.data.result.resCode).toBe(0); expect((await Reviews.findOne({ _id: "4Y8k7DnX3PLNdwRPr" })).likes).toBe(3); - const res2 = await axios.post(`http://localhost:${testingPort}/v2/updateLiked`, { id: "4Y8k7DnX3PLNdwRPr", token: "fakeTokenDti1" }); + const res2 = await axios.post( + `http://localhost:${testingPort}/v2/updateLiked`, + { id: "4Y8k7DnX3PLNdwRPr", token: "fakeTokenDti1" }, + ); expect(res2.data.result.resCode).toBe(0); expect((await Reviews.findOne({ _id: "4Y8k7DnX3PLNdwRPr" })).likes).toBe(2); }); @@ -185,17 +263,28 @@ describe('tests', () => { family_name: user1.lastName, }; - const res = await axios.post(`http://localhost:${testingPort}/v2/insertUser`, { googleObject: gObj1 }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/insertUser`, + { googleObject: gObj1 }, + ); expect(res.data.result).toBe(1); - expect((await Students.find({}).exec()).filter((s) => s.netId === "cv4620").length).toBe(1); + expect( + (await Students.find({}).exec()).filter((s) => s.netId === "cv4620") + .length, + ).toBe(1); }); it("user id's not being leaked by querying reviews", async () => { - const res = await axios.post(`http://localhost:${testingPort}/v2/getReviewsByCourseId`, { courseId: "oH37S3mJ4eAsktypy" }); + const res = await axios.post( + `http://localhost:${testingPort}/v2/getReviewsByCourseId`, + { courseId: "oH37S3mJ4eAsktypy" }, + ); expect(res.data.result.length).toBe(testReviews.length); const classOfReviews = testReviews.map((r) => r.user); - expect(res.data.result.map((r) => r.user).sort()).not.toEqual(classOfReviews.sort()); + expect(res.data.result.map((r) => r.user).sort()).not.toEqual( + classOfReviews.sort(), + ); expect(res.data.result.map((r) => r.user).sort()).toEqual(["", ""]); }); }); diff --git a/server/endpoints/test/Search.test.ts b/server/endpoints/test/Search.test.ts new file mode 100644 index 000000000..ea78a2180 --- /dev/null +++ b/server/endpoints/test/Search.test.ts @@ -0,0 +1,313 @@ +import axios from "axios"; +import { Student, Class, Subject, Professor } from "common"; +import TestingServer, { testingPort } from "./TestServer"; + +const testServer = new TestingServer(testingPort); + +beforeAll(async () => { + const testStudents: Student[] = [ + { + _id: "Irrelevant", + firstName: "John", + lastName: "Smith", + netId: "js0", + affiliation: null, + token: null, + privilege: "regular", + reviews: [], + likedReviews: [], + }, + ]; + + const testClasses: Class[] = [ + { + _id: "newCourse1", + classSub: "MORK", + classNum: "1110", + classTitle: "Introduction to Testing", + classFull: "MORK 1110: Introduction to Testing", + classSems: ["FA19"], + classProfessors: ["Gazghul Thraka"], + classRating: 1, + classWorkload: 2, + classDifficulty: 3, + classPrereq: [], + crossList: [], + }, + { + _id: "newCourse2", + classSub: "MORK", + classNum: "2110", + classTitle: "Intermediate Testing", + classFull: "MORK 2110: Intermediate Testing", + classSems: ["SP20"], + classPrereq: ["newCourse1"], // the class above + classProfessors: ["Gazghul Thraka"], + classRating: 3, + classWorkload: 4, + classDifficulty: 5, + crossList: [], + }, + ]; + + const testSubjects: Subject[] = [ + { + _id: "newSubject1", + subShort: "MORK", + subFull: "Study of Angry Fungi", + }, + { + _id: "angry subject", + subShort: "MAD", + subFull: "The Study of Anger Issues", + }, + { + _id: "federation subject", + subShort: "FEDN", + subFull: "The Study of Where No Man has Gone Before!", + }, + ]; + + const testProfessors: Professor[] = [ + { + _id: "prof_1", + fullName: "Gazghul Thraka", + courses: ["newCourse1", "newCourse2"], + major: "MORK", + }, + { + _id: "prof_2", + fullName: "Jean-Luc Picard", + courses: [], + major: "FEDN", + }, + ]; + + await testServer.setUpDB( + undefined, + testStudents, + testClasses, + testProfessors, + testSubjects, + ); +}); + +afterAll(async () => { + await testServer.shutdownTestingServer(); +}); + +describe("tests", () => { + it("getClassesByQuery-works", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res1 = await axios.post( + `http://localhost:${testingPort}/v2/getClassesByQuery`, + { query: "MORK 1" }, + ); + // we expect it to be MORK 1110 first, and then MORK 2110 + expect(res1.data.result.map((e) => e.classFull)).toStrictEqual([ + "MORK 1110: Introduction to Testing", + "MORK 2110: Intermediate Testing", + ]); + }); + + it('getClassesByQuery-works "MORK1" ', async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getClassesByQuery`, + { query: "MORK1" }, + ); + expect(res.data.result.map((e) => e.classFull)).toStrictEqual([ + "MORK 1110: Introduction to Testing", + "MORK 2110: Intermediate Testing", + ]); + }); + + it('getClassesByQuery-works "MORK 1110" ', async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getClassesByQuery`, + { query: "MORK1110" }, + ); + expect(res.data.result.map((e) => e.classFull)).toStrictEqual([ + "MORK 1110: Introduction to Testing", + ]); + }); + + it("getSubjectsByQuery-works", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getSubjectsByQuery`, + { query: "MORK" }, + ); + expect(res.data.result.map((e) => e.subShort)).toContain("MORK"); + expect(res.data.result.map((e) => e.subShort)).not.toContain("MAD"); + expect(res.data.result.map((e) => e.subShort)).not.toContain("FEDN"); + }); + + it("getProfessorsByQuery-works", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res1 = await axios.post( + `http://localhost:${testingPort}/v2/getProfessorsByQuery`, + { query: "Gazghul Thraka" }, + ); + expect(res1.data.result.map((e) => e.fullName)).toContain("Gazghul Thraka"); + expect(res1.data.result.map((e) => e.fullName)).not.toContain( + "Jean-Luc Picard", + ); + + const res2 = await axios.post( + `http://localhost:${testingPort}/v2/getProfessorsByQuery`, + { query: "Jean-Luc Picard" }, + ); + expect(res2.data.result.map((e) => e.fullName)).not.toContain( + "Gazghul Thraka", + ); + expect(res2.data.result.map((e) => e.fullName)).toContain( + "Jean-Luc Picard", + ); + }); + + // Query has no matching results: + it("getClassesByQuery-no matching classes", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getClassesByQuery`, + { query: "random" }, + ); + // we expect no results to be returned + expect(res.data.result.map((e) => e.classFull)).toStrictEqual([]); + expect(res.data.result.map((e) => e.classFull)).not.toContain([ + "MORK 1110: Introduction to Testing", + "MORK 2110: Intermediate Testing", + ]); + }); + + it("getSubjectsByQuery-no matching subjects", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getSubjectsByQuery`, + { query: "RAND" }, + ); + // we expect no results to be returned + expect(res.data.result.map((e) => e.subShort)).toStrictEqual([]); + expect(res.data.result.map((e) => e.subShort)).not.toContain("MORK"); + expect(res.data.result.map((e) => e.subShort)).not.toContain("MAD"); + expect(res.data.result.map((e) => e.subShort)).not.toContain("FEDN"); + + const res2 = await axios.post( + `http://localhost:${testingPort}/v2/getSubjectsByQuery`, + { query: "RAND1" }, + ); + expect(res2.data.result.map((e) => e.subShort)).toStrictEqual([]); + }); + + it("getProfessorsByQuery-no matching professors", async () => { + expect( + await axios + .post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { + "not query": "other", + }) + .catch((e) => "failed!"), + ).toBe("failed!"); + + const res = await axios.post( + `http://localhost:${testingPort}/v2/getProfessorsByQuery`, + { query: "Random Professor" }, + ); + // we expect no results to be returned + expect(res.data.result.map((e) => e.fullName)).toStrictEqual([]); + expect(res.data.result.map((e) => e.fullName)).not.toContain( + "Gazghul Thraka", + ); + expect(res.data.result.map((e) => e.fullName)).not.toContain( + "Jean-Luc Picard", + ); + }); + + // Will accept ascii, but give no guarantees as to what is returned. + it("getClassesByQuery-non Ascii", async () => { + const res = await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + query: "भारत", + }) + .catch((e) => e); + expect(res.data.result).toBeTruthy(); + }); + + // Not for these however. + it("getSubjectsByQuery-non Ascii", async () => { + const res = await axios + .post(`http://localhost:${testingPort}/v2/getSubjectsByQuery`, { + query: "भारत", + }) + .catch((e) => e); + expect(res.message).toBe("Request failed with status code 400"); + }); + + it("getProfessorsByQuery-non Ascii", async () => { + const res = await axios + .post(`http://localhost:${testingPort}/v2/getProfessorsByQuery`, { + query: "भारत", + }) + .catch((e) => e); + expect(res.message).toBe("Request failed with status code 400"); + }); + + it("getClassesByQuery- empty query", async () => { + const res = await axios + .post(`http://localhost:${testingPort}/v2/getClassesByQuery`, { + query: "", + }) + .catch((e) => e); + expect(res.message).toBe("Request failed with status code 400"); + }); +}); diff --git a/server/endpoints/TestServer.ts b/server/endpoints/test/TestServer.ts similarity index 51% rename from server/endpoints/TestServer.ts rename to server/endpoints/test/TestServer.ts index 3b2d417a4..a6774b8df 100644 --- a/server/endpoints/TestServer.ts +++ b/server/endpoints/test/TestServer.ts @@ -1,10 +1,16 @@ import { MongoMemoryServer } from "mongodb-memory-server"; -import mongoose from 'mongoose'; +import mongoose from "mongoose"; import express from "express"; -import { Student, Class, Professor, Review, Subject } from 'common'; +import { Student, Class, Professor, Review, Subject } from "common"; import * as http from "http"; -import { Classes, Students, Subjects, Professors, Reviews } from "../dbDefs"; -import { configure } from "../endpoints"; +import { + Classes, + Students, + Subjects, + Professors, + Reviews, +} from "../../db/dbDefs"; +import { configure } from "../../endpoints"; export const testingPort = 8080; @@ -24,37 +30,43 @@ export default class TestingServer { configure(app); } - setUpDB = async (reviews: Review[] = [], students: Student[] = [], - classes: Class[] = [], professors: Professor[] = [], subjects: Subject[] = []) => { + setUpDB = async ( + reviews: Review[] = [], + students: Student[] = [], + classes: Class[] = [], + professors: Professor[] = [], + subjects: Subject[] = [], + ) => { // setup db const mongoUri = await this.mongoServer.getUri(); - await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }); + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); - await mongoose.connection.collections.classes.createIndex({ classFull: "text" }); - await mongoose.connection.collections.subjects.createIndex({ subShort: "text" }); - await mongoose.connection.collections.professors.createIndex({ fullName: "text" }); + await mongoose.connection.collections.classes.createIndex({ + classFull: "text", + }); + await mongoose.connection.collections.subjects.createIndex({ + subShort: "text", + }); + await mongoose.connection.collections.professors.createIndex({ + fullName: "text", + }); // add classes, reviews, etc... to db collections - await Promise.all( - reviews.map(async (c) => await (new Reviews(c).save())), - ); + await Promise.all(reviews.map(async (c) => await new Reviews(c).save())); - await Promise.all( - classes.map(async (c) => await (new Classes(c).save())), - ); + await Promise.all(classes.map(async (c) => await new Classes(c).save())); - await Promise.all( - students.map(async (c) => await (new Students(c).save())), - ); + await Promise.all(students.map(async (c) => await new Students(c).save())); - await Promise.all( - subjects.map(async (c) => await (new Subjects(c).save())), - ); + await Promise.all(subjects.map(async (c) => await new Subjects(c).save())); await Promise.all( - professors.map(async (c) => await (new Professors(c).save())), + professors.map(async (c) => await new Professors(c).save()), ); - } + }; shutdownTestingServer = async () => { await mongoose.disconnect(); @@ -64,5 +76,5 @@ export default class TestingServer { if (this.serverCloseHandle !== undefined) { this.serverCloseHandle.close(); } - } + }; } diff --git a/server/endpoints/utils.ts b/server/endpoints/utils/utils.ts similarity index 94% rename from server/endpoints/utils.ts rename to server/endpoints/utils/utils.ts index 5beb613dd..f9de85e8c 100644 --- a/server/endpoints/utils.ts +++ b/server/endpoints/utils/utils.ts @@ -1,7 +1,7 @@ import { ValidationChain, body } from "express-validator"; -import { InsertUserRequest, CourseIdQuery } from "./Review"; -import { Classes, Students } from "../dbDefs"; -import { getUserByNetId, getVerificationTicket } from "./Auth"; +import { InsertUserRequest, CourseIdQuery } from "../review/Review"; +import { Classes, Students } from "../../db/dbDefs"; +import { getUserByNetId, getVerificationTicket } from "../auth/Auth"; import shortid = require("shortid"); diff --git a/server/server.ts b/server/server.ts index 6d240a308..215754547 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,58 +1,74 @@ import path from "path"; import express from "express"; -import sslRedirect from 'heroku-ssl-redirect'; +import sslRedirect from "heroku-ssl-redirect"; import mongoose from "mongoose"; import { MongoMemoryServer } from "mongodb-memory-server"; import cors from "cors"; import dotenv from "dotenv"; -import { fetchAddCourses } from "./dbInit"; +import { fetchAddCourses } from "./db/dbInit"; import { configure } from "./endpoints"; dotenv.config(); const app = express(); -app.use(sslRedirect([ - 'development', - 'production', -])); +app.use(sslRedirect(["development", "production"])); app.use(cors()); app.use(express.static(path.join(__dirname, "../../client/build"))); function setup() { const port = process.env.PORT || 8080; - app.get('*', (_, response) => response.sendFile(path.join(__dirname, '../../client/build/index.html'))); + app.get("*", (_, response) => + response.sendFile(path.join(__dirname, "../../client/build/index.html")), + ); configure(app); // eslint-disable-next-line no-console app.listen(port, () => console.log(`Listening on port ${port}...`)); } -const uri = process.env.MONGODB_URL ? process.env.MONGODB_URL : "this will error"; +const uri = process.env.MONGODB_URL + ? process.env.MONGODB_URL + : "this will error"; let localMongoServer; -mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }).then(async () => setup()).catch(async (err) => { - // eslint-disable-next-line no-console - console.error("No DB connection defined!"); - - // If the environment variable is set, create a simple local db to work with - // This could be expanded in the future with default mock admin accounts, etc. - // For now, it fetches Fall 2019 classes for you to view - // This is useful if you need a local db without any hassle, and don't want to risk damage to the staging db - if (process.env.ALLOW_LOCAL === "1") { +mongoose + .connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(async () => setup()) + .catch(async (err) => { // eslint-disable-next-line no-console - console.log("Falling back to local db!"); - localMongoServer = new MongoMemoryServer(); - const mongoUri = await localMongoServer.getUri(); - await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.error("No DB connection defined!"); - // eslint-disable-next-line no-console - await fetchAddCourses("https://classes.cornell.edu/api/2.0/", "FA19").catch((e) => console.error(e)); + // If the environment variable is set, create a simple local db to work with + // This could be expanded in the future with default mock admin accounts, etc. + // For now, it fetches Fall 2019 classes for you to view + // This is useful if you need a local db without any hassle, and don't want to risk damage to the staging db + if (process.env.ALLOW_LOCAL === "1") { + // eslint-disable-next-line no-console + console.log("Falling back to local db!"); + localMongoServer = new MongoMemoryServer(); + const mongoUri = await localMongoServer.getUri(); + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + // eslint-disable-next-line no-console + await fetchAddCourses( + "https://classes.cornell.edu/api/2.0/", + "FA19", + ).catch((e) => console.error(e)); - await mongoose.connection.collections.classes.createIndex({ classFull: "text" }); - await mongoose.connection.collections.subjects.createIndex({ subShort: "text" }); - await mongoose.connection.collections.professors.createIndex({ fullName: "text" }); + await mongoose.connection.collections.classes.createIndex({ + classFull: "text", + }); + await mongoose.connection.collections.subjects.createIndex({ + subShort: "text", + }); + await mongoose.connection.collections.professors.createIndex({ + fullName: "text", + }); - setup(); - } else { - process.exit(1); - } -}); + setup(); + } else { + process.exit(1); + } + }); diff --git a/server/tsconfig.json b/server/tsconfig.json index d4da51124..9feb434db 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -12,15 +12,15 @@ "outDir": "dist" }, "files": [ - "dbInit.ts", - "dbDefs.ts", + "db/dbInit.ts", + "db/dbDefs.ts", "server.ts", "endpoints.ts", - "endpoints/AdminActions.ts", - "endpoints/AdminChart.ts", - "endpoints/Auth.ts", - "endpoints/Review.ts", - "endpoints/Search.ts", - "endpoints/utils.ts" + "endpoints/admin/AdminActions.ts", + "endpoints/admin/AdminChart.ts", + "endpoints/auth/Auth.ts", + "endpoints/review/Review.ts", + "endpoints/search/Search.ts", + "endpoints/utils/utils.ts" ] } \ No newline at end of file From c0f54ac4336206dc3ad9540e76dc9746f7f341e1 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 01:40:15 -0400 Subject: [PATCH 02/33] lint --- server/endpoints/admin/AdminChart.ts | 229 +++++++++++++-------------- server/endpoints/auth/Auth.ts | 3 +- server/endpoints/review/Review.ts | 13 +- server/endpoints/search/Search.ts | 21 ++- server/server.ts | 4 +- 5 files changed, 130 insertions(+), 140 deletions(-) diff --git a/server/endpoints/admin/AdminChart.ts b/server/endpoints/admin/AdminChart.ts index 6333058da..002671edf 100644 --- a/server/endpoints/admin/AdminChart.ts +++ b/server/endpoints/admin/AdminChart.ts @@ -26,137 +26,136 @@ interface GetReviewsOverTimeTop15Request { * ... * } */ -export const getReviewsOverTimeTop15: Endpoint = - { - guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, request: GetReviewsOverTimeTop15Request) => { - const { token, step, range } = request; - try { - const userIsAdmin = await verifyToken(token); - if (userIsAdmin) { - const top15 = await topSubjectsCB(ctx, { token }); - // contains cs, math, gov etc... - const retArr = []; - await Promise.all( - top15.map(async (classs) => { - const [subject] = await Subjects.find( - { - subFull: classs[0], - }, - { - subShort: 1, - }, - ).exec(); // EX: computer science--> cs - const subshort = subject.subShort; - retArr.push(subshort); - }), - ); - const arrHM = []; // [ {"cs": {date1: totalNum}, math: {date1, totalNum} }, - // {"cs": {date2: totalNum}, math: {date2, totalNum} } ] - for (let i = 0; i < range * 30; i += step) { - // "data": -->this{"2017-01-01": 3, "2017-01-02": 4, ...} - // run on reviews. gets all classes and num of reviewa for each class, in x day - const pipeline = [ +export const getReviewsOverTimeTop15: Endpoint = { + guard: [body("token").notEmpty().isAscii()], + callback: async (ctx: Context, request: GetReviewsOverTimeTop15Request) => { + const { token, step, range } = request; + try { + const userIsAdmin = await verifyToken(token); + if (userIsAdmin) { + const top15 = await topSubjectsCB(ctx, { token }); + // contains cs, math, gov etc... + const retArr = []; + await Promise.all( + top15.map(async (classs) => { + const [subject] = await Subjects.find( { - $match: { - date: { - $lte: new Date( - new Date().setDate(new Date().getDate() - i), - ), - }, - }, + subFull: classs[0], }, { - $group: { - _id: "$class", - total: { - $sum: 1, - }, + subShort: 1, + }, + ).exec(); // EX: computer science--> cs + const subshort = subject.subShort; + retArr.push(subshort); + }), + ); + const arrHM = []; // [ {"cs": {date1: totalNum}, math: {date1, totalNum} }, + // {"cs": {date2: totalNum}, math: {date2, totalNum} } ] + for (let i = 0; i < range * 30; i += step) { + // "data": -->this{"2017-01-01": 3, "2017-01-02": 4, ...} + // run on reviews. gets all classes and num of reviewa for each class, in x day + const pipeline = [ + { + $match: { + date: { + $lte: new Date( + new Date().setDate(new Date().getDate() - i), + ), }, }, - ]; - const hashMap = { total: null }; // Object {"cs": {date1: totalNum, date2: totalNum, ...}, math: {date1, totalNum} } - // eslint-disable-next-line no-await-in-loop - const results = await Reviews.aggregate<{ + }, + { + $group: { + _id: "$class", + total: { + $sum: 1, + }, + }, + }, + ]; + const hashMap = { total: null }; // Object {"cs": {date1: totalNum, date2: totalNum, ...}, math: {date1, totalNum} } + // eslint-disable-next-line no-await-in-loop + const results = await Reviews.aggregate<{ _id: string; total: number; }>(pipeline); // eslint-disable-next-line no-await-in-loop - await Promise.all( - results.map(async (data) => { - // { "_id" : "KyeJxLouwDvgY8iEu", "total" : 1 } //all in same date - const res = await Classes.find( - { - _id: data._id, - }, - { - classSub: 1, - }, - ).exec(); + await Promise.all( + results.map(async (data) => { + // { "_id" : "KyeJxLouwDvgY8iEu", "total" : 1 } //all in same date + const res = await Classes.find( + { + _id: data._id, + }, + { + classSub: 1, + }, + ).exec(); - const sub = res[0]; // finds the class corresponding to "KyeJxLouwDvgY8iEu" ex: cs 2112 - // date of this review minus the hrs mins sec - const timeStringYMD = new Date( - new Date().setDate(new Date().getDate() - i), - ) - .toISOString() - .split("T")[0]; - if (retArr.includes(sub.classSub)) { - // if thos review is one of the top 15 we want. - if (hashMap[sub.classSub] == null) { - // if not in hm then add - hashMap[sub.classSub] = { - [timeStringYMD]: data.total, - }; - } else { - // increment totalnum - hashMap[sub.classSub] = { - [timeStringYMD]: - hashMap[sub.classSub][timeStringYMD] + data.total, - }; - } - } - if (hashMap.total == null) { - hashMap.total = { + const sub = res[0]; // finds the class corresponding to "KyeJxLouwDvgY8iEu" ex: cs 2112 + // date of this review minus the hrs mins sec + const timeStringYMD = new Date( + new Date().setDate(new Date().getDate() - i), + ) + .toISOString() + .split("T")[0]; + if (retArr.includes(sub.classSub)) { + // if thos review is one of the top 15 we want. + if (hashMap[sub.classSub] == null) { + // if not in hm then add + hashMap[sub.classSub] = { [timeStringYMD]: data.total, }; } else { - hashMap.total = { - [timeStringYMD]: hashMap.total[timeStringYMD] + data.total, + // increment totalnum + hashMap[sub.classSub] = { + [timeStringYMD]: + hashMap[sub.classSub][timeStringYMD] + data.total, }; } - }), - ); - arrHM.push(hashMap); - } + } + if (hashMap.total == null) { + hashMap.total = { + [timeStringYMD]: data.total, + }; + } else { + hashMap.total = { + [timeStringYMD]: hashMap.total[timeStringYMD] + data.total, + }; + } + }), + ); + arrHM.push(hashMap); + } - const hm2 = {}; // {cs: [{date1:totalNum}, {date2: totalNum}, ...], math: [{date1:total}, {date2: total}, ...], ... } + const hm2 = {}; // {cs: [{date1:totalNum}, {date2: totalNum}, ...], math: [{date1:total}, {date2: total}, ...], ... } - // enrty:{"cs": {date1: totalNum}, math: {date1, totalNum} } - if (arrHM.length > 0) { - const entry = arrHM[0]; - const keys = Object.keys(entry); + // enrty:{"cs": {date1: totalNum}, math: {date1, totalNum} } + if (arrHM.length > 0) { + const entry = arrHM[0]; + const keys = Object.keys(entry); - // "cs" - keys.forEach((key) => { - const t = arrHM.map((a) => a[key]); // for a key EX:"cs": [{date1:totalNum},{date2:totalNum}] - hm2[key] = t; - }); - } - return hm2; + // "cs" + keys.forEach((key) => { + const t = arrHM.map((a) => a[key]); // for a key EX:"cs": [{date1:totalNum},{date2:totalNum}] + hm2[key] = t; + }); } - - // user is not admin - return null; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getReviewsOverTimeTop15' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; + return hm2; } - }, - }; + + // user is not admin + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getReviewsOverTimeTop15' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } + }, +}; /** * Helper function for [topSubjects] @@ -212,9 +211,7 @@ const topSubjectsCB = async (_ctx: Context, request: Token) => { ); let subjectsAndReviewCountArray = Array.from(subjectsMap); // Sorts array by number of reviews each topic has - subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => - a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0, - ); + subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => (a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0)); // Returns the top 15 most reviewed classes return subjectsAndReviewCountArray.slice(0, 15); @@ -352,8 +349,8 @@ export class DefaultDict { const val = this[key]; if ( - Object.prototype.hasOwnProperty.call(this, key) && - typeof val !== "function" + Object.prototype.hasOwnProperty.call(this, key) + && typeof val !== "function" ) { return val; } diff --git a/server/endpoints/auth/Auth.ts b/server/endpoints/auth/Auth.ts index 667b9516b..980b4f95d 100644 --- a/server/endpoints/auth/Auth.ts +++ b/server/endpoints/auth/Auth.ts @@ -66,6 +66,5 @@ export const getUserByNetId = async (netId: string) => { */ export const tokenIsAdmin: Endpoint = { guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, adminRequest: AdminRequest) => - await verifyToken(adminRequest.token), + callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), }; diff --git a/server/endpoints/review/Review.ts b/server/endpoints/review/Review.ts index 2cb62685f..38d04c9b5 100644 --- a/server/endpoints/review/Review.ts +++ b/server/endpoints/review/Review.ts @@ -54,16 +54,14 @@ export const sanitizeReview = (doc: ReviewDocument) => { * @param lst the list of reviews to sanitize. Possibly a singleton list. * @returns a copy of the reviews, but with the user id field removed. */ -export const sanitizeReviews = (lst: ReviewDocument[]) => - lst.map((doc) => sanitizeReview(doc)); +export const sanitizeReviews = (lst: ReviewDocument[]) => lst.map((doc) => sanitizeReview(doc)); /** * Get a course with this course_id from the Classes collection */ export const getCourseById: Endpoint = { guard: [body("courseId").notEmpty().isAscii()], - callback: async (ctx: Context, arg: CourseIdQuery) => - await getCourseByIdCallback(arg), + callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg), }; /* @@ -128,8 +126,7 @@ export const getReviewsByCourseId: Endpoint = { */ export const insertUser: Endpoint = { guard: [body("googleObject").notEmpty()], - callback: async (ctx: Context, arg: InsertUserRequest) => - await insertUserCallback(arg), + callback: async (ctx: Context, arg: InsertUserRequest) => await insertUserCallback(arg), }; /** @@ -255,8 +252,8 @@ export const updateLiked: Endpoint = { // removing like if ( - student.likedReviews !== undefined && - student.likedReviews.includes(review.id) + student.likedReviews !== undefined + && student.likedReviews.includes(review.id) ) { await Students.updateOne( { netId }, diff --git a/server/endpoints/search/Search.ts b/server/endpoints/search/Search.ts index 3251b75f3..92b29d1ca 100644 --- a/server/endpoints/search/Search.ts +++ b/server/endpoints/search/Search.ts @@ -58,8 +58,8 @@ const courseSort = (query) => (a, b) => { const bCourseStr = `${b.classSub} ${b.classNum}`; const queryLen = query.length; return ( - editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) + editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) + - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) ); }; @@ -71,15 +71,14 @@ export const isSubShorthand = async (sub: string) => { }; // helper to format search within a subject -const searchWithinSubject = (sub: string, remainder: string) => - Classes.find( - { - classSub: sub, - classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, - }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec(); +const searchWithinSubject = (sub: string, remainder: string) => Classes.find( + { + classSub: sub, + classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, + }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, +).exec(); export const regexClassesSearch = async (searchString) => { try { diff --git a/server/server.ts b/server/server.ts index 215754547..6d4088b18 100644 --- a/server/server.ts +++ b/server/server.ts @@ -16,9 +16,7 @@ app.use(express.static(path.join(__dirname, "../../client/build"))); function setup() { const port = process.env.PORT || 8080; - app.get("*", (_, response) => - response.sendFile(path.join(__dirname, "../../client/build/index.html")), - ); + app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html"))); configure(app); // eslint-disable-next-line no-console From 06ddf2b7b210a8824cc870821a497717038015e7 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 01:49:28 -0400 Subject: [PATCH 03/33] remove db from gitignore --- .gitignore | 1 - server/db/dbDefs.ts | 149 ++++++ server/db/dbInit.test.ts | 244 +++++++++ server/db/dbInit.ts | 788 ++++++++++++++++++++++++++++++ server/endpoints/search/Search.ts | 21 +- 5 files changed, 1192 insertions(+), 11 deletions(-) create mode 100644 server/db/dbDefs.ts create mode 100644 server/db/dbInit.test.ts create mode 100644 server/db/dbInit.ts diff --git a/.gitignore b/.gitignore index 041ec64c6..d3d3eb397 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ mongodb-binaries/ .git .vscode .env -db .meteor mongo/ *error.log diff --git a/server/db/dbDefs.ts b/server/db/dbDefs.ts new file mode 100644 index 000000000..040186bb0 --- /dev/null +++ b/server/db/dbDefs.ts @@ -0,0 +1,149 @@ +import mongoose, { Schema } from "mongoose"; +import { Class, Student, Subject, Review, Professor } from "common"; + +/* + + Database definitions file. Defines all collections in the local database, + with collection attributes, types, and required fields. + + Used by both the Server and Client to define local and minimongo database + structures. + +*/ + +/* # Classes collection. + # Holds data about each class in the course roster. +*/ +export interface ClassDocument extends mongoose.Document, Class { + _id: string; +} + +const ClassSchema = new Schema({ + _id: { type: String }, // overwritten _id field to play nice with our old db + classSub: { type: String }, // subject, like "PHIL" or "CS" + classNum: { type: String }, // course number, like 1110 + classTitle: { type: String }, // class title, like 'Introduction to Algorithms' + classPrereq: { type: [String], required: false }, // list of pre-req classes, a string of Classes _id. + crossList: { type: [String], required: false }, // list of classes that are crosslisted with this one, a string of Classes _id. + classFull: { type: String }, // full class title to search by, formated as 'classSub classNum: classTitle' + classSems: { type: [String] }, // list of semesters this class was offered, like ['FA17', 'FA16'] + classProfessors: { type: [String] }, // list of professors that have taught the course over past semesters + classRating: { type: Number }, // the average class rating from reviews + classWorkload: { type: Number }, // the average workload rating from reviews + classDifficulty: { type: Number }, // the average difficulty rating from reviews +}); + +export const Classes = mongoose.model("classes", ClassSchema); +/* # Users collection. + # Holds data about each user. Data is collected via Cornell net-id login. +*/ + +export interface StudentDocument extends mongoose.Document, Student { + readonly _id: string; +} + +const StudentSchema = new Schema({ + _id: { type: String }, // overwritten _id field to play nice with our old db + firstName: { type: String }, // user first name + lastName: { type: String }, // user last name + netId: { type: String }, // user netId + affiliation: { type: String }, // user affliaition, like ENG or A&S + token: { type: String }, // random token generated during login process + privilege: { type: String }, // user privilege level + reviews: { type: [String] }, // the reviews that this user has posted. + likedReviews: { type: [String] }, +}); +export const Students = mongoose.model( + "students", + StudentSchema, +); + +/* # Subjects Collection + # List of all course subject groups and their full text names + # ex: CS -> Computer Science +*/ + +export interface SubjectDocument extends mongoose.Document, Subject { + readonly _id: string; +} + +const SubjectSchema = new Schema({ + _id: { type: String }, // overwritten _id field to play nice with our old db + subShort: { type: String }, // subject, like "PHIL" or "CS" + subFull: { type: String }, // subject full name, like 'Computer Science' +}); +export const Subjects = mongoose.model( + "subjects", + SubjectSchema, +); + +/* # Reviews Collection. + # Stores each review inputted by a user. Linked with the course that was + # reviewed via a mapping with on class, which holds the _id attribute of + # the class from the Classes collection +*/ + +export interface ReviewDocument extends mongoose.Document, Review { + readonly _id: string; +} + +const ReviewSchema = new Schema({ + _id: { type: String }, // overwritten _id field to play nice with our old db + user: { type: String, required: false }, // user who wrote this review, a Users _id + text: { type: String, required: false }, // text from the review + difficulty: { type: Number }, // difficulty measure from the review + rating: { type: Number }, // quality measure from the review + workload: { type: Number }, // quality measure from the review + class: { type: String }, // class the review was for, a Classes _id + date: { type: Date }, // date/timestamp the review was submited + visible: { type: Number }, // visibility flag - 1 if visible to users, 0 if only visible to admin + reported: { type: Number }, // reported flag - 1 if review was reported, 0 otherwise + professors: { type: [String] }, // list of professors that have thought the course over past semesters + likes: { type: Number, min: 0 }, // number of likes a review has + likedBy: { type: [String] }, + isCovid: { type: Boolean }, + grade: { type: String }, + major: { type: [String] }, + // The following was a temporary field used to keep track of reviews for a contest + // The full functional code for counting reviews can be found on the following branch: + // review-counting-feature +}); +export const Reviews = mongoose.model("reviews", ReviewSchema); + +/* # Professors collection. + # Holds data about each professor. +*/ + +export interface ProfessorDocument extends mongoose.Document, Professor { + readonly _id: string; +} + +const ProfessorSchema = new Schema({ + _id: { type: String }, // mongo-generated random id for this document + fullName: { type: String }, // the full name of the professor + courses: { type: [String] }, // a list of the ids all the courses + major: { type: String }, // professor affliation by probable major +}); +export const Professors = mongoose.model( + "professors", + ProfessorSchema, +); + +/* # Validation Collection. + # Stores passwords and other sensitive application keys. + # Must be manually populated with data when the app is initialized. +*/ +const ValidationSchema = new Schema({ + _id: { type: String }, // mongo-generated random id for this document + adminPass: { type: String }, // admin password to validate against +}); + +interface ValidationDocument extends mongoose.Document { + _id: string; + adminPass?: string; +} + +export const Validation = mongoose.model( + "validation", + ValidationSchema, +); diff --git a/server/db/dbInit.test.ts b/server/db/dbInit.test.ts new file mode 100644 index 000000000..43ad5d1fe --- /dev/null +++ b/server/db/dbInit.test.ts @@ -0,0 +1,244 @@ +// Set up fake endpoints to query +import express from "express"; +import axios from "axios"; +import { MongoMemoryServer } from "mongodb-memory-server"; +import mongoose from "mongoose"; +import { Subjects, Classes, Professors } from "./dbDefs"; +import { + fetchSubjects, + fetchClassesForSubject, + fetchAddCourses, +} from "./dbInit"; + +let testServer: MongoMemoryServer; +let serverCloseHandle; + +const testingPort = 27760; +const testingEndpoint = `http://localhost:${testingPort}/`; + +// Configure a mongo server and fake endpoints for the tests to use +beforeAll(async () => { + // get mongoose all set up + testServer = new MongoMemoryServer(); + const mongoUri = await testServer.getUri(); + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + mongoose.set("useFindAndModify", false); + + await new Subjects({ + _id: "some id", + subShort: "gork", + subFull: "Study of Angry Fungi", + }).save(); + + await new Classes({ + _id: "some other id", + classSub: "gork", + classNum: "1110", + classTitle: "Introduction to Angry Fungi", + classFull: "gork 1110 Introduction to Angry Fungi", + classSems: ["FA19"], + classProfessors: ["Prof. Thraka"], + classRating: 5, + classWorkload: 5, + classDifficulty: 5, + }).save(); + + // We need to pretend to have access to a cornell classes endpoint + const app = express(); + serverCloseHandle = app.listen(testingPort); + + app.get("/hello", (req, res) => { + res.send("Hello world"); + }); + + // Fake subjects endpoint + app.get("/config/subjects.json", (req, res) => { + // simulate only having FA20 data. + // express did not allow me to include a "?" literal in the path for some strange reason + // Maybe fix in the future? + if (!req.originalUrl.includes("FA20")) { + res.send({ + status: "failure", + }); + } + + res.send({ + status: "success", + data: { + subjects: [ + { + descr: "Study of Fungi", + descrformal: "The Study of Fungi", + value: "gork", + }, + { + descr: "Study of Space", + descrformal: "The Study of Where No One has Gone Before", + value: "fedn", + }, + ], + }, + }); + }); + + // Fake classes endpoint + app.get("/search/classes.json", (req, res) => { + // simulate only having data for the gork subject. + // see above + if (!req.originalUrl.includes("gork")) { + res.send({ + status: "failure", + }); + } + + res.send({ + status: "success", + data: { + classes: [ + { + subject: "gork", + catalogNbr: "1110", + titleLong: "Introduction to Angry Fungi", + randoJunk: "Making sure this scauses no issues", + enrollGroups: [ + { + classSections: [ + { + ssrComponent: "LEC", + meetings: [ + { + instructors: [ + { firstName: "Prof.", lastName: "Thraka" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + junk: "nada", + subject: "gork", + catalogNbr: "2110", + titleLong: "Advanced Study of Angry Fungi", + enrollGroups: [ + { + classSections: [ + { + ssrComponent: "LEC", + meetings: [ + { + instructors: [ + { firstName: "Prof.", lastName: "Thraka" }, + { firstName: "Prof.", lastName: "Urgok" }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }); + }); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await testServer.stop(); + serverCloseHandle.close(); +}); + +describe("tests", () => { + it("dbInit-db-works", async () => { + expect((await Subjects.findOne({ subShort: "gork" })).subShort).toBe( + "gork", + ); + expect( + (await Classes.findOne({ classSub: "gork", classNum: "1110" })).classSub, + ).toBe("gork"); + }); + + // Does the internal testing endpoint exist? + it("dbInit-test-endpoint-exists", async () => { + const response = await axios.get(`${testingEndpoint}hello`); + expect(response.data).toBe("Hello world"); + expect(response.data).not.toBe("Something the endpoint is not to return!"); + }); + + // Does fetching the subjects collection work as expected? + it("fetching-roster-works", async () => { + const response = await fetchSubjects(testingEndpoint, "FA20"); + expect(response.length).toBe(2); + expect(response[0].descrformal).toBe("The Study of Fungi"); + expect(response[0].value).toBe("gork"); + expect(response[1].value).toBe("fedn"); + + // No data for FA19! + const nil = await fetchSubjects(testingEndpoint, "FA19"); + expect(nil).toBeNull(); + }); + + // Does fetching the classes collection work as expected? + it("fetching-classes-by-subject-works", async () => { + const response = await fetchClassesForSubject(testingEndpoint, "FA20", { + descrformal: "The Study of AngryFungi", + value: "gork", + }); + expect(response.length).toBe(2); + expect(response[0].subject).toBe("gork"); + expect(response[0].catalogNbr).toBe("1110"); + expect(response[0].titleLong).toBe("Introduction to Angry Fungi"); + expect(response[1].titleLong).toBe("Advanced Study of Angry Fungi"); + + // No fedn classes, only gork classes! + const nil = await fetchClassesForSubject(testingEndpoint, "FA20", { + descrformal: "The Study of Where No One has Gone Before", + value: "fedn", + }); + expect(nil).toBeNull(); + }); + + it("full-scraping-works", async () => { + const worked = await fetchAddCourses(testingEndpoint, "FA20"); + expect(worked).toBe(true); + + // did it add the fedn subject? + expect((await Subjects.findOne({ subShort: "fedn" }).exec()).subFull).toBe( + "The Study of Where No One has Gone Before", + ); + + // did it update the semesters on gork 1110? + // notice the .lean(), which changes some of the internals of what mongo returns + const class1 = await Classes.findOne({ classSub: "gork", classNum: "1110" }) + .lean() + .exec(); + expect(class1.classSems).toStrictEqual(["FA19", "FA20"]); + + // did it add the gork 2110 Class? + const class2 = await Classes.findOne({ + classSub: "gork", + classNum: "2110", + }).exec(); + expect(class2.classTitle).toBe("Advanced Study of Angry Fungi"); + + // Did it update the classes for the first professor + const prof1 = await Professors.findOne({ fullName: "Prof. Thraka" }) + .lean() + .exec(); + expect(prof1.courses).toContain(class1._id); + expect(prof1.courses).toContain(class2._id); + + // Did it add the second professor with the right class id? + const prof2 = await Professors.findOne({ fullName: "Prof. Urgok" }) + .lean() + .exec(); + expect(prof2.courses).toStrictEqual([class2._id]); + }); +}); diff --git a/server/db/dbInit.ts b/server/db/dbInit.ts new file mode 100644 index 000000000..a4d44c4e7 --- /dev/null +++ b/server/db/dbInit.ts @@ -0,0 +1,788 @@ +import axios from "axios"; +import shortid from "shortid"; +import { Classes, Subjects, Professors } from "./dbDefs"; + +export const defaultEndpoint = "https://classes.cornell.edu/api/2.0/"; + +// Represents a subject which is scraped +// Note: there's a load of additional information when we scrape it. +// It's not relevant, so we just ignore it for now. +export interface ScrapingSubject { + descrformal: string; // Subject description, e.g. "Asian American Studies" + value: string; // Subject code, e.g. "AAS" +} + +// This only exists for compatibility with the API +export interface ScrapingInstructor { + firstName: string; + lastName: string; +} + +// This only exists for compatibility with the API +export interface ScrapingMeeting { + instructors: ScrapingInstructor[]; +} + +// This only exists for compatibility with the API +export interface ScrapingClassSection { + ssrComponent: string; // i.e. LEC, SEM, DIS + meetings: ScrapingMeeting[]; +} + +// This only exists for compatibility with the API +export interface ScrapingEnrollGroup { + classSections: ScrapingClassSection[]; // what sections the class has +} + +// Represents a class which is scraped +// Note: there's a load of additional information when we scrape it. +// It's not relevant, so we just ignore it for now. +export interface ScrapingClass { + subject: string; // Short: e.g. "CS" + catalogNbr: string; // e.g. 1110 + titleLong: string; // long variant of a title e.g. "Introduction to Computing Using Python" + enrollGroups: ScrapingEnrollGroup[]; // specified by the API +} + +/* + * Fetch the class roster for a semester. + * Returns the class roster on success, or null if there was an error. + */ +export async function fetchSubjects( + endpoint: string, + semester: string, +): Promise { + const result = await axios.get( + `${endpoint}config/subjects.json?roster=${semester}`, + { timeout: 10000 }, + ); + if (result.status !== 200 || result.data.status !== "success") { + console.log( + `Error fetching ${semester} subjects! HTTP: ${result.statusText} SERV: ${result.data.status}`, + ); + return null; + } + + return result.data.data.subjects; +} + +/* + * Fetch all the classes for that semester/subject combination + * Returns a list of classes on success, or null if there was an error. + */ +export async function fetchClassesForSubject( + endpoint: string, + semester: string, + subject: ScrapingSubject, +): Promise { + const result = await axios.get( + `${endpoint}search/classes.json?roster=${semester}&subject=${subject.value}`, + { timeout: 10000 }, + ); + if (result.status !== 200 || result.data.status !== "success") { + console.log( + `Error fetching subject ${semester}-${subject.value} classes! HTTP: ${result.statusText} SERV: ${result.data.status}`, + ); + return null; + } + + return result.data.data.classes; +} + +export function isInstructorEqual( + a: ScrapingInstructor, + b: ScrapingInstructor, +) { + return a.firstName === b.firstName && a.lastName === b.lastName; +} + +/* + * Extract an array of professors from the terribly deeply nested gunk that the api returns + * There are guaranteed to be no duplicates! + */ +export function extractProfessors(clas: ScrapingClass): ScrapingInstructor[] { + const raw = clas.enrollGroups.map((e) => + e.classSections.map((s) => s.meetings.map((m) => m.instructors)), + ); + // flatmap does not work :( + const f1: ScrapingInstructor[][][] = []; + raw.forEach((r) => f1.push(...r)); + const f2: ScrapingInstructor[][] = []; + f1.forEach((r) => f2.push(...r)); + const f3: ScrapingInstructor[] = []; + f2.forEach((r) => f3.push(...r)); + + const nonDuplicates: ScrapingInstructor[] = []; + + f3.forEach((inst) => { + // check if there is another instructor in nonDuplicates already! + if (nonDuplicates.filter((i) => isInstructorEqual(i, inst)).length === 0) { + // push the instructor if not present + nonDuplicates.push(inst); + } + }); + + return nonDuplicates; +} + +/* + * Fetch the relevant classes, and add them to the collections + * Returns true on success, and false on failure. + */ +export async function fetchAddCourses( + endpoint: string, + semester: string, +): Promise { + const subjects = await fetchSubjects(endpoint, semester); + + const v1 = await Promise.all( + subjects.map(async (subject) => { + const subjectIfExists = await Subjects.findOne({ + subShort: subject.value.toLowerCase(), + }).exec(); + + if (!subjectIfExists) { + console.log(`Adding new subject: ${subject.value}`); + const res = await new Subjects({ + _id: shortid.generate(), + subShort: subject.value.toLowerCase(), + subFull: subject.descrformal, + }) + .save() + .catch((err) => { + console.log(err); + return null; + }); + + // db operation was not successful + if (!res) { + throw new Error(); + } + } + + return true; + }), + ).catch((err) => null); + + if (!v1) { + console.log("Something went wrong while updating subjects!"); + return false; + } + + // Update the Classes in the db + const v2 = await Promise.all( + subjects.map(async (subject) => { + const classes = await fetchClassesForSubject(endpoint, semester, subject); + + // skip if something went wrong fetching classes + // it could be that there are not classes here (in tests, corresponds to FEDN) + if (!classes) { + return true; + } + + // Update or add all the classes to the collection + const v = await Promise.all( + classes.map(async (cl) => { + const classIfExists = await Classes.findOne({ + classSub: cl.subject.toLowerCase(), + classNum: cl.catalogNbr, + }).exec(); + const professors = extractProfessors(cl); + + // figure out if the professor already exist in the collection, if not, add to the collection + // build a list of professor names to potentially add the the class + const profs: string[] = await Promise.all( + professors.map(async (p) => { + // This has to be an atomic upset. Otherwise, this causes some race condition badness + const professorIfExists = await Professors.findOneAndUpdate( + { fullName: `${p.firstName} ${p.lastName}` }, + { + $setOnInsert: { + fullName: `${p.firstName} ${p.lastName}`, + _id: shortid.generate(), + major: "None" /* TODO: change? */, + }, + }, + { upsert: true, new: true }, + ); + + return professorIfExists.fullName; + }), + ).catch((err) => { + console.log(err); + return []; + }); + + // The class does not exist yet, so we add it + if (!classIfExists) { + console.log(`Adding new class ${cl.subject} ${cl.catalogNbr}`); + const res = await new Classes({ + _id: shortid.generate(), + classSub: cl.subject.toLowerCase(), + classNum: cl.catalogNbr, + classTitle: cl.titleLong, + classFull: `${cl.subject.toLowerCase()} ${cl.catalogNbr} ${ + cl.titleLong + }`, + classSems: [semester], + classProfessors: profs, + classRating: null, + classWorkload: null, + classDifficulty: null, + }) + .save() + .catch((err) => { + console.log(err); + return null; + }); + + // update professors with new class information + profs.forEach(async (inst) => { + await Professors.findOneAndUpdate( + { fullName: inst }, + { $addToSet: { courses: res._id } }, + ).catch((err) => console.log(err)); + }); + + if (!res) { + console.log( + `Unable to insert class ${cl.subject} ${cl.catalogNbr}!`, + ); + throw new Error(); + } + } else { + // The class does exist, so we update semester information + console.log( + `Updating class information for ${classIfExists.classSub} ${classIfExists.classNum}`, + ); + + // Compute the new set of semesters for this class + const classSems = + classIfExists.classSems.indexOf(semester) == -1 + ? classIfExists.classSems.concat([semester]) + : classIfExists.classSems; + + // Compute the new set of professors for this class + const classProfessors = classIfExists.classProfessors + ? classIfExists.classProfessors + : []; + + // Add any new professors to the class + profs.forEach((inst) => { + if (classProfessors.filter((i) => i == inst).length === 0) { + classProfessors.push(inst); + } + }); + + // update db with new semester information + const res = await Classes.findOneAndUpdate( + { _id: classIfExists._id }, + { $set: { classSems, classProfessors } }, + ) + .exec() + .catch((err) => { + console.log(err); + return null; + }); + + // update professors with new class information + // Note the set update. We don't want to add duplicates here + classProfessors.forEach(async (inst) => { + await Professors.findOneAndUpdate( + { fullName: inst }, + { $addToSet: { courses: classIfExists._id } }, + ).catch((err) => console.log(err)); + }); + + if (!res) { + console.log( + `Unable to update class information for ${cl.subject} ${cl.catalogNbr}!`, + ); + throw new Error(); + } + } + + return true; + }), + ).catch((err) => { + console.log(err); + return null; + }); + + // something went wrong updating classes + if (!v) { + throw new Error(); + } + + return true; + }), + ).catch((err) => null); + + if (!v2) { + console.log("Something went wrong while updating classes"); + return false; + } + + return true; +} + +/* + Course API scraper. Uses HTTP requests to get course data from the Cornell + Course API and stores the results in the local database. + + Functions defined here should be called during app initialization to populate + the local database or once a semester to add new semester data to the + local database. + + Functions are called by admins via the admin interface (Admin component). + +*/ + +/* # Populates the Classes and Subjects collections in the local database by grabbing + # all courses data for the semesters in the semsters array though requests + # sent to the Cornell Courses API + # + # example: semesters = ["SP17", "SP16", "SP15","FA17", "FA16", "FA15"]; + # + # Using the findAllSemesters() array as input, the function populates an + # empty database with all courses and subjects. + # Using findCurrSemester(), the function updates the existing database. + # +*/ +export async function addAllCourses(semesters: any) { + console.log(semesters); + Object.keys(semesters).forEach(async (semester) => { + // get all classes in this semester + console.log( + `Adding classes for the following semester: ${semesters[semester]}`, + ); + const result = await axios.get( + `https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, + { timeout: 30000 }, + ); + if (result.status !== 200) { + console.log("Error in addAllCourses: 1"); + return 0; + } + const response = result.data; + // console.log(response); + const sub = response.data.subjects; + await Promise.all( + Object.keys(sub).map(async (course) => { + const parent = sub[course]; + // if subject doesn't exist add to Subjects collection + const checkSub = await Subjects.find({ + subShort: parent.value.toLowerCase(), + }).exec(); + if (checkSub.length === 0) { + console.log(`new subject: ${parent.value}`); + await new Subjects({ + subShort: parent.value.toLowerCase(), + subFull: parent.descr, + }).save(); + } + + // for each subject, get all classes in that subject for this semester + const result2 = await axios.get( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + { timeout: 30000 }, + ); + if (result2.status !== 200) { + console.log("Error in addAllCourses: 2"); + return 0; + } + const response2 = result2.data; + const courses = response2.data.classes; + + // add each class to the Classes collection if it doesnt exist already + for (const course in courses) { + try { + console.log( + `${courses[course].subject} ${courses[course].catalogNbr}`, + ); + const check = await Classes.find({ + classSub: courses[course].subject.toLowerCase(), + classNum: courses[course].catalogNbr, + }).exec(); + console.log(check); + if (check.length === 0) { + console.log( + `new class: ${courses[course].subject} ${courses[course].catalogNbr},${semesters[semester]}`, + ); + // insert new class with empty prereqs and reviews + await new Classes({ + classSub: courses[course].subject.toLowerCase(), + classNum: courses[course].catalogNbr, + classTitle: courses[course].titleLong, + classPrereq: [], + classFull: `${courses[course].subject.toLowerCase()} ${ + courses[course].catalogNbr + } ${courses[course].titleLong.toLowerCase()}`, + classSems: [semesters[semester]], + }).save(); + } else { + const matchedCourse = check[0]; // only 1 should exist + const oldSems = matchedCourse.classSems; + if (oldSems && oldSems.indexOf(semesters[semester]) === -1) { + // console.log("update class " + courses[course].subject + " " + courses[course].catalogNbr + "," + semesters[semester]); + oldSems.push(semesters[semester]); // add this semester to the list + Classes.update( + { _id: matchedCourse._id }, + { $set: { classSems: oldSems } }, + ); + } + } + } catch (error) { + console.log("Error in addAllCourses: 3"); + return 0; + } + } + }), + ); + }); + console.log("Finished addAllCourses"); + return 1; +} + +export async function updateProfessors(semesters: any) { + // You just want to go through all the classes in the Classes database and update the Professors field + // Don't want to go through the semesters + // Might want a helper function that returns that professors for you + console.log("In updateProfessors method"); + for (const semester in semesters) { + // get all classes in this semester + // console.log(`https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`); + try { + await axios.get( + `https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, + { timeout: 30000 }, + ); + } catch (error) { + console.log("Error in updateProfessors: 1"); + console.log(error); + continue; + } + const result = await axios.get( + `https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, + { timeout: 30000 }, + ); + // console.log(result) + if (result.status !== 200) { + console.log("Error in updateProfessors: 2"); + console.log(result.status); + continue; + } else { + const response = result.data; + // console.log(response); + const { subjects } = response.data; // array of the subjects + for (const department in subjects) { + // for every subject + const parent = subjects[department]; + // console.log("https://classes.cornell.edu/api/2.0/search/classes.json?roster=" + semesters[semester] + "&subject="+ parent.value) + try { + await axios.get( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + { timeout: 30000 }, + ); + } catch (error) { + console.log("Error in updateProfessors: 3"); + console.log(error); + continue; + } + const result2 = await axios.get( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + { timeout: 30000 }, + ); + if (result2.status !== 200) { + console.log("Error in updateProfessors: 4"); + console.log(result2.status); + continue; + } else { + const response2 = result2.data; + const courses = response2.data.classes; + + // add each class to the Classes collection if it doesnt exist already + for (const course in courses) { + try { + const check = await Classes.find({ + classSub: courses[course].subject.toLowerCase(), + classNum: courses[course].catalogNbr, + }).exec(); + const matchedCourse = check[0]; // catch this if there is no class existing + if (typeof matchedCourse !== "undefined") { + // console.log(courses[course].subject); + // console.log(courses[course].catalogNbr); + // console.log("This is the matchedCourse") + // console.log(matchedCourse) + let oldProfessors = matchedCourse.classProfessors; + if (oldProfessors === undefined) { + oldProfessors = []; + } + // console.log("This is the length of old profs") + // console.log(oldProfessors.length) + const { classSections } = courses[course].enrollGroups[0]; // This returns an array + for (const section in classSections) { + if ( + classSections[section].ssrComponent === "LEC" || + classSections[section].ssrComponent === "SEM" + ) { + // Checks to see if class has scheduled meetings before checking them + if (classSections[section].meetings.length > 0) { + const professors = + classSections[section].meetings[0].instructors; + // Checks to see if class has instructors before checking them + // Example of class without professors is: + // ASRC 3113 in FA16 + // ASRC 3113 returns an empty array for professors + if (professors.length > 0) { + for (const professor in professors) { + const { firstName } = professors[professor]; + const { lastName } = professors[professor]; + const fullName = `${firstName} ${lastName}`; + if (!oldProfessors.includes(fullName)) { + oldProfessors.push(fullName); + // console.log("This is a new professor") + // console.log(typeof oldProfessors) + // console.log(oldProfessors) + } + } + } else { + // console.log("This class does not have professors"); + } + } else { + // console.log("This class does not have meetings scheduled"); + } + } + } + await Classes.update( + { _id: matchedCourse._id }, + { $set: { classProfessors: oldProfessors } }, + ).exec(); + } + } catch (error) { + console.log("Error in updateProfessors: 5"); + console.log( + `Error on course ${courses[course].subject} ${courses[course].catalogNbr}`, + ); + console.log(error); + + return 1; + } + } + } + } + } + } + console.log("Finished updateProfessors"); + return 0; +} + +export async function resetProfessorArray(semesters: any) { + // Initializes the classProfessors field in the Classes collection to an empty array so that + // we have a uniform empty array to fill with updateProfessors + // Will only have to be called ONCE + console.log("In resetProfessorArray method"); + for (const semester in semesters) { + // get all classes in this semester + const result = await axios.get( + `https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, + { timeout: 30000 }, + ); + if (result.status !== 200) { + console.log("Error in resetProfessorArray: 1"); + console.log(result.status); + return 0; + } + + const response = result.data; + // console.log(response); + const sub = response.data.subjects; // array of the subjects + for (const course in sub) { + // for every subject + const parent = sub[course]; + console.log( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + ); + const result2 = await axios.get( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + { timeout: 30000 }, + ); + + if (result2.status !== 200) { + console.log("Error in resetProfessorArray: 2"); + return 0; + } + + const response2 = result2.data; + // console.log("PRINTING ALL THE COURSES") + const courses = response2.data.classes; + // console.log(courses) + + // add each class to the Classes collection if it doesnt exist already + for (const course in courses) { + try { + const check = await Classes.find({ + classSub: courses[course].subject.toLowerCase(), + classNum: courses[course].catalogNbr, + }).exec(); + const matchedCourse = check[0]; // catch this if there is no class existing + if (typeof matchedCourse !== "undefined") { + console.log(courses[course].subject); + console.log(courses[course].catalogNbr); + console.log("This is the matchedCourse"); + console.log(matchedCourse); + // var oldProfessors = matchedCourse.classProfessors + const oldProfessors = []; + console.log("This is the length of old profs"); + console.log(oldProfessors.length); + Classes.update( + { _id: matchedCourse._id }, + { $set: { classProfessors: oldProfessors } }, + ); + } + } catch (error) { + console.log("Error in resetProfessorArray: 5"); + console.log( + `Error on course ${courses[course].subject} ${courses[course].catalogNbr}`, + ); + console.log(error); + return 0; + } + } + } + } + console.log("professors reset"); + return 1; +} + +export async function getProfessorsForClass() { + // Need the method here to extract the Professor from the response + // return the array here +} + +/* # Grabs the API-required format of the current semester, to be given to the + # addAllCourses function. + # Return: String Array (length = 1) +*/ +export async function findCurrSemester() { + let response = await axios.get( + "https://classes.cornell.edu/api/2.0/config/rosters.json", + { timeout: 30000 }, + ); + if (response.status !== 200) { + console.log("Error in findCurrSemester"); + } else { + response = response.data; + const allSemesters = response.data.rosters; + const thisSem = allSemesters[allSemesters.length - 1].slug; + console.log(`Updating for following semester: ${thisSem}`); + return [thisSem]; + } +} + +/* # Grabs the API-required format of the all recent semesters to be given to the + # addAllCourses function. + # Return: String Array +*/ +export async function findAllSemesters(): Promise { + let response = await axios.get( + "https://classes.cornell.edu/api/2.0/config/rosters.json", + { timeout: 30000 }, + ); + if (response.status !== 200) { + console.log("error"); + return []; + } + response = response.data; + const allSemesters = response.data.rosters; + return allSemesters.map((semesterObject) => semesterObject.slug); +} + +/* # Look through all courses in the local database, and identify those + # that are cross-listed (have multiple official names). Link these classes + # by adding their course_id to all crosslisted class's crosslist array. + # + # Called once during intialization, only after all courses have been added. +*/ +export async function addCrossList() { + const semesters = await findAllSemesters(); + for (const semester in semesters) { + // get all classes in this semester + const result = await axios.get( + `https://classes.cornell.edu/api/2.0/config/subjects.json?roster=${semesters[semester]}`, + { timeout: 30000 }, + ); + if (result.status !== 200) { + console.log("Error in addCrossList: 1"); + return 0; + } + const response = result.data; + // console.log(response); + const sub = response.data.subjects; + for (const course in sub) { + const parent = sub[course]; + + // for each subject, get all classes in that subject for this semester + const result2 = await axios.get( + `https://classes.cornell.edu/api/2.0/search/classes.json?roster=${semesters[semester]}&subject=${parent.value}`, + { timeout: 30000 }, + ); + if (result2.status !== 200) { + console.log("Error in addCrossList: 2"); + return 0; + } + const response2 = result2.data; + const courses = response2.data.classes; + + for (const course in courses) { + try { + const check = await Classes.find({ + classSub: courses[course].subject.toLowerCase(), + classNum: courses[course].catalogNbr, + }).exec(); + // console.log((courses[course].subject).toLowerCase() + " " + courses[course].catalogNbr); + // console.log(check); + if (check.length > 0) { + const crossList = + courses[course].enrollGroups[0].simpleCombinations; + if (crossList.length > 0) { + const crossListIDs: string[] = await Promise.all( + crossList.map(async (crossListedCourse: any) => { + console.log(crossListedCourse); + const dbCourse = await Classes.find({ + classSub: crossListedCourse.subject.toLowerCase(), + classNum: crossListedCourse.catalogNbr, + }).exec(); + // Added the following check because MUSIC 2340 + // was crosslisted with AMST 2340, which was not in our db + // so was causing an error here when calling 'dbCourse[0]._id' + // AMST 2340 exists in FA17 but not FA18 + if (dbCourse[0]) { + return dbCourse[0]._id; + } + + return null; + }), + ); + console.log( + `${courses[course].subject} ${courses[course].catalogNbr}`, + ); + // console.log(crossListIDs); + const thisCourse = check[0]; + Classes.update( + { _id: thisCourse._id }, + { $set: { crossList: crossListIDs } }, + ); + } + } + } catch (error) { + console.log("Error in addCrossList: 3"); + console.log(error); + return 0; + } + } + } + } + console.log("Finished addCrossList"); + return 1; +} diff --git a/server/endpoints/search/Search.ts b/server/endpoints/search/Search.ts index 92b29d1ca..3251b75f3 100644 --- a/server/endpoints/search/Search.ts +++ b/server/endpoints/search/Search.ts @@ -58,8 +58,8 @@ const courseSort = (query) => (a, b) => { const bCourseStr = `${b.classSub} ${b.classNum}`; const queryLen = query.length; return ( - editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) + editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - + editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) ); }; @@ -71,14 +71,15 @@ export const isSubShorthand = async (sub: string) => { }; // helper to format search within a subject -const searchWithinSubject = (sub: string, remainder: string) => Classes.find( - { - classSub: sub, - classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, - }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, -).exec(); +const searchWithinSubject = (sub: string, remainder: string) => + Classes.find( + { + classSub: sub, + classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, + }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); export const regexClassesSearch = async (searchString) => { try { From 354e5b3b2b19b34f962206429037e8f89293a1f3 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 01:53:13 -0400 Subject: [PATCH 04/33] lint --- server/db/dbInit.ts | 21 ++++++++------------- server/endpoints/search/Search.ts | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/server/db/dbInit.ts b/server/db/dbInit.ts index a4d44c4e7..9ee269899 100644 --- a/server/db/dbInit.ts +++ b/server/db/dbInit.ts @@ -101,9 +101,7 @@ export function isInstructorEqual( * There are guaranteed to be no duplicates! */ export function extractProfessors(clas: ScrapingClass): ScrapingInstructor[] { - const raw = clas.enrollGroups.map((e) => - e.classSections.map((s) => s.meetings.map((m) => m.instructors)), - ); + const raw = clas.enrollGroups.map((e) => e.classSections.map((s) => s.meetings.map((m) => m.instructors))); // flatmap does not work :( const f1: ScrapingInstructor[][][] = []; raw.forEach((r) => f1.push(...r)); @@ -257,10 +255,9 @@ export async function fetchAddCourses( ); // Compute the new set of semesters for this class - const classSems = - classIfExists.classSems.indexOf(semester) == -1 - ? classIfExists.classSems.concat([semester]) - : classIfExists.classSems; + const classSems = classIfExists.classSems.indexOf(semester) === -1 + ? classIfExists.classSems.concat([semester]) + : classIfExists.classSems; // Compute the new set of professors for this class const classProfessors = classIfExists.classProfessors @@ -523,13 +520,12 @@ export async function updateProfessors(semesters: any) { const { classSections } = courses[course].enrollGroups[0]; // This returns an array for (const section in classSections) { if ( - classSections[section].ssrComponent === "LEC" || - classSections[section].ssrComponent === "SEM" + classSections[section].ssrComponent === "LEC" + || classSections[section].ssrComponent === "SEM" ) { // Checks to see if class has scheduled meetings before checking them if (classSections[section].meetings.length > 0) { - const professors = - classSections[section].meetings[0].instructors; + const professors = classSections[section].meetings[0].instructors; // Checks to see if class has instructors before checking them // Example of class without professors is: // ASRC 3113 in FA16 @@ -743,8 +739,7 @@ export async function addCrossList() { // console.log((courses[course].subject).toLowerCase() + " " + courses[course].catalogNbr); // console.log(check); if (check.length > 0) { - const crossList = - courses[course].enrollGroups[0].simpleCombinations; + const crossList = courses[course].enrollGroups[0].simpleCombinations; if (crossList.length > 0) { const crossListIDs: string[] = await Promise.all( crossList.map(async (crossListedCourse: any) => { diff --git a/server/endpoints/search/Search.ts b/server/endpoints/search/Search.ts index 3251b75f3..92b29d1ca 100644 --- a/server/endpoints/search/Search.ts +++ b/server/endpoints/search/Search.ts @@ -58,8 +58,8 @@ const courseSort = (query) => (a, b) => { const bCourseStr = `${b.classSub} ${b.classNum}`; const queryLen = query.length; return ( - editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) + editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) + - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) ); }; @@ -71,15 +71,14 @@ export const isSubShorthand = async (sub: string) => { }; // helper to format search within a subject -const searchWithinSubject = (sub: string, remainder: string) => - Classes.find( - { - classSub: sub, - classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, - }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec(); +const searchWithinSubject = (sub: string, remainder: string) => Classes.find( + { + classSub: sub, + classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, + }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, +).exec(); export const regexClassesSearch = async (searchString) => { try { From a6bed601e5697489993c3369cdcf585d1354be75 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:00:28 -0400 Subject: [PATCH 05/33] replacing === instead of == --- server/db/dbInit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/db/dbInit.ts b/server/db/dbInit.ts index 9ee269899..2b7d808db 100644 --- a/server/db/dbInit.ts +++ b/server/db/dbInit.ts @@ -266,7 +266,7 @@ export async function fetchAddCourses( // Add any new professors to the class profs.forEach((inst) => { - if (classProfessors.filter((i) => i == inst).length === 0) { + if (classProfessors.filter((i) => i === inst).length === 0) { classProfessors.push(inst); } }); From eb0f25be45f7d5ec873875a853ae0224bae468e8 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:27:44 -0400 Subject: [PATCH 06/33] extracted types and functions --- server/endpoints/admin/AdminActions.ts | 19 +--- server/endpoints/admin/AdminChart.ts | 79 +------------ server/endpoints/admin/functions.ts | 72 ++++++++++++ server/endpoints/admin/types.ts | 27 +++++ server/endpoints/auth/Auth.ts | 20 +--- server/endpoints/auth/functions.ts | 11 ++ server/endpoints/auth/types.ts | 3 + server/endpoints/profile/Profile.ts | 10 +- server/endpoints/profile/types.ts | 8 ++ server/endpoints/review/Review.ts | 30 +---- server/endpoints/review/types.ts | 29 +++++ server/endpoints/search/Search.ts | 151 +------------------------ server/endpoints/search/functions.ts | 145 ++++++++++++++++++++++++ server/endpoints/search/types.ts | 4 + 14 files changed, 312 insertions(+), 296 deletions(-) create mode 100644 server/endpoints/admin/functions.ts create mode 100644 server/endpoints/admin/types.ts create mode 100644 server/endpoints/auth/functions.ts create mode 100644 server/endpoints/auth/types.ts create mode 100644 server/endpoints/profile/types.ts create mode 100644 server/endpoints/review/types.ts create mode 100644 server/endpoints/search/functions.ts create mode 100644 server/endpoints/search/types.ts diff --git a/server/endpoints/admin/AdminActions.ts b/server/endpoints/admin/AdminActions.ts index 4b1924958..c818b2dec 100644 --- a/server/endpoints/admin/AdminActions.ts +++ b/server/endpoints/admin/AdminActions.ts @@ -2,7 +2,7 @@ import { body } from "express-validator"; import { getCrossListOR, getMetricValues } from "common/CourseCard"; import { Context, Endpoint } from "../../endpoints"; -import { Reviews, ReviewDocument, Classes, Students } from "../../db/dbDefs"; +import { Reviews, Classes, Students } from "../../db/dbDefs"; import { updateProfessors, findAllSemesters, @@ -10,22 +10,7 @@ import { } from "../../db/dbInit"; import { getCourseById, verifyToken } from "../utils/utils"; import { ReviewRequest } from "../review/Review"; - -// The type for a request with an admin action for a review -interface AdminReviewRequest { - review: ReviewDocument; - token: string; -} - -// The type for a request with an admin action for updating professors info -interface AdminProfessorsRequest { - token: string; -} - -interface AdminRaffleWinnerRequest { - token: string; - startDate: string; -} +import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; // This updates the metrics for an individual class given its Mongo-generated id. // Returns 1 if successful, 0 otherwise. diff --git a/server/endpoints/admin/AdminChart.ts b/server/endpoints/admin/AdminChart.ts index 002671edf..050642efe 100644 --- a/server/endpoints/admin/AdminChart.ts +++ b/server/endpoints/admin/AdminChart.ts @@ -3,16 +3,8 @@ import { body } from "express-validator"; import { verifyToken } from "../utils/utils"; import { Context, Endpoint } from "../../endpoints"; import { Reviews, Classes, Subjects } from "../../db/dbDefs"; - -export interface Token { - token: string; -} - -interface GetReviewsOverTimeTop15Request { - token: string; - step: number; - range: number; -} +import { GetReviewsOverTimeTop15Request, Token } from "./types"; +import { topSubjectsCB } from "./functions"; /** * Returns an key value object where key is a dept and value is an array of @@ -157,73 +149,6 @@ export const getReviewsOverTimeTop15: Endpoint = }, }; -/** - * Helper function for [topSubjects] - */ -const topSubjectsCB = async (_ctx: Context, request: Token) => { - const userIsAdmin = await verifyToken(request.token); - if (!userIsAdmin) { - return null; - } - - try { - // using the add-on library meteorhacks:aggregate, define pipeline aggregate functions - // to run complex queries - const pipeline = [ - // consider only visible reviews - { $match: { visible: 1 } }, - // group by class and get count of reviews - { $group: { _id: "$class", reviewCount: { $sum: 1 } } }, - // sort by decending count - // {$sort: {"reviewCount": -1}}, - // {$limit: 10} - ]; - // reviewedSubjects is a dictionary-like object of subjects (key) and - // number of reviews (value) associated with that subject - const reviewedSubjects = new DefaultDict(); - // run the query and return the class name and number of reviews written to it - const results = await Reviews.aggregate<{ - reviewCount: number; - _id: string; - }>(pipeline); - - await Promise.all( - results.map(async (course) => { - const classObject = (await Classes.find({ _id: course._id }).exec())[0]; - // classSubject is the string of the full subject of classObject - const subjectArr = await Subjects.find({ - subShort: classObject.classSub, - }).exec(); - if (subjectArr.length > 0) { - const classSubject = subjectArr[0].subFull; - // Adds the number of reviews to the ongoing count of reviews per subject - const curVal = reviewedSubjects.get(classSubject) || 0; - reviewedSubjects[classSubject] = curVal + course.reviewCount; - } - }), - ); - - // Creates a map of subjects (key) and total number of reviews (value) - const subjectsMap = new Map( - Object.entries(reviewedSubjects).filter( - (x): x is [string, number] => typeof x[1] === "number", - ), - ); - let subjectsAndReviewCountArray = Array.from(subjectsMap); - // Sorts array by number of reviews each topic has - subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => (a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0)); - - // Returns the top 15 most reviewed classes - return subjectsAndReviewCountArray.slice(0, 15); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'topSubjects' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } -}; - /** * Returns the top 15 subjects (in terms of number of reviews) */ diff --git a/server/endpoints/admin/functions.ts b/server/endpoints/admin/functions.ts new file mode 100644 index 000000000..fb52b5014 --- /dev/null +++ b/server/endpoints/admin/functions.ts @@ -0,0 +1,72 @@ +import { verifyToken } from "../utils/utils"; +import { Context } from "../../endpoints"; +import { Token } from "./types"; +import { Reviews, Classes, Subjects } from "../../db/dbDefs"; +import { DefaultDict } from "./AdminChart"; + +/** + * Helper function for [topSubjects] + */ +export const topSubjectsCB = async (_ctx: Context, request: Token) => { + const userIsAdmin = await verifyToken(request.token); + if (!userIsAdmin) { + return null; + } + + try { + // using the add-on library meteorhacks:aggregate, define pipeline aggregate functions + // to run complex queries + const pipeline = [ + // consider only visible reviews + { $match: { visible: 1 } }, + // group by class and get count of reviews + { $group: { _id: "$class", reviewCount: { $sum: 1 } } }, + // sort by decending count + // {$sort: {"reviewCount": -1}}, + // {$limit: 10} + ]; + // reviewedSubjects is a dictionary-like object of subjects (key) and + // number of reviews (value) associated with that subject + const reviewedSubjects = new DefaultDict(); + // run the query and return the class name and number of reviews written to it + const results = await Reviews.aggregate<{ + reviewCount: number; + _id: string; + }>(pipeline); + + await Promise.all( + results.map(async (course) => { + const classObject = (await Classes.find({ _id: course._id }).exec())[0]; + // classSubject is the string of the full subject of classObject + const subjectArr = await Subjects.find({ + subShort: classObject.classSub, + }).exec(); + if (subjectArr.length > 0) { + const classSubject = subjectArr[0].subFull; + // Adds the number of reviews to the ongoing count of reviews per subject + const curVal = reviewedSubjects.get(classSubject) || 0; + reviewedSubjects[classSubject] = curVal + course.reviewCount; + } + }), + ); + + // Creates a map of subjects (key) and total number of reviews (value) + const subjectsMap = new Map( + Object.entries(reviewedSubjects).filter( + (x): x is [string, number] => typeof x[1] === "number", + ), + ); + let subjectsAndReviewCountArray = Array.from(subjectsMap); + // Sorts array by number of reviews each topic has + subjectsAndReviewCountArray = subjectsAndReviewCountArray.sort((a, b) => (a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0)); + + // Returns the top 15 most reviewed classes + return subjectsAndReviewCountArray.slice(0, 15); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'topSubjects' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; diff --git a/server/endpoints/admin/types.ts b/server/endpoints/admin/types.ts new file mode 100644 index 000000000..23f2521f6 --- /dev/null +++ b/server/endpoints/admin/types.ts @@ -0,0 +1,27 @@ +import { ReviewDocument } from "../../db/dbDefs"; + +export interface Token { + token: string; +} + +export interface GetReviewsOverTimeTop15Request { + token: string; + step: number; + range: number; +} + +// The type for a request with an admin action for a review +export interface AdminReviewRequest { + review: ReviewDocument; + token: string; +} + +// The type for a request with an admin action for updating professors info +export interface AdminProfessorsRequest { + token: string; +} + +export interface AdminRaffleWinnerRequest { + token: string; + startDate: string; +} diff --git a/server/endpoints/auth/Auth.ts b/server/endpoints/auth/Auth.ts index 980b4f95d..f0c1c81c6 100644 --- a/server/endpoints/auth/Auth.ts +++ b/server/endpoints/auth/Auth.ts @@ -1,17 +1,11 @@ import { body } from "express-validator"; -import { OAuth2Client } from "google-auth-library"; import { Context, Endpoint } from "../../endpoints"; import { Students } from "../../db/dbDefs"; import { verifyToken } from "../utils/utils"; +import { AdminRequest } from "./types"; +import { verifyTicket } from "./functions"; -const client = new OAuth2Client( - "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com", -); - -// The type for a search query -interface AdminRequest { - token: string; -} +const audience = "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com"; /** * Returns true if [netid] matches the netid in the email of the JSON @@ -29,12 +23,8 @@ export const getVerificationTicket = async (token?: string) => { console.log("Token was undefined in getVerificationTicket"); return null; } - const ticket = await client.verifyIdToken({ - idToken: token, - audience: - "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com", - }); - return ticket.getPayload(); + + return verifyTicket(token, audience); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getVerificationTicket' method"); diff --git a/server/endpoints/auth/functions.ts b/server/endpoints/auth/functions.ts new file mode 100644 index 000000000..0c7f06297 --- /dev/null +++ b/server/endpoints/auth/functions.ts @@ -0,0 +1,11 @@ +import { OAuth2Client } from "google-auth-library"; + +export const verifyTicket = (token: string, audience: string) => { + const client = new OAuth2Client(audience); + const ticket = await client.verifyIdToken({ + idToken: token, + audience, + }); + + return ticket.getPayload(); +}; diff --git a/server/endpoints/auth/types.ts b/server/endpoints/auth/types.ts new file mode 100644 index 000000000..c0bafb762 --- /dev/null +++ b/server/endpoints/auth/types.ts @@ -0,0 +1,3 @@ +export interface AdminRequest { + token: string; +} diff --git a/server/endpoints/profile/Profile.ts b/server/endpoints/profile/Profile.ts index 670787755..67add0335 100644 --- a/server/endpoints/profile/Profile.ts +++ b/server/endpoints/profile/Profile.ts @@ -1,18 +1,10 @@ import { body } from "express-validator"; import { Context, Endpoint } from "../../endpoints"; import { ReviewDocument, Reviews, Students } from "../../db/dbDefs"; +import { ProfileRequest, NetIdQuery } from "./types"; import { getVerificationTicket } from "../auth/Auth"; -// The type of a query with a studentId -export interface NetIdQuery { - netId: string; -} - -export interface ProfileRequest { - token: string; -} - export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], callback: async (ctx: Context, request: ProfileRequest) => { diff --git a/server/endpoints/profile/types.ts b/server/endpoints/profile/types.ts new file mode 100644 index 000000000..4f49a332c --- /dev/null +++ b/server/endpoints/profile/types.ts @@ -0,0 +1,8 @@ +// The type of a query with a studentId +export interface NetIdQuery { + netId: string; +} + +export interface ProfileRequest { + token: string; +} diff --git a/server/endpoints/review/Review.ts b/server/endpoints/review/Review.ts index 38d04c9b5..54c853023 100644 --- a/server/endpoints/review/Review.ts +++ b/server/endpoints/review/Review.ts @@ -1,6 +1,5 @@ import { body } from "express-validator"; import { getCrossListOR } from "common/CourseCard"; -import { Review } from "common"; import { Context, Endpoint } from "../../endpoints"; import { Classes, ReviewDocument, Reviews, Students } from "../../db/dbDefs"; import { @@ -9,37 +8,10 @@ import { JSONNonempty, } from "../utils/utils"; import { getVerificationTicket } from "../auth/Auth"; +import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; import shortid = require("shortid"); -// The type of a query with a courseId -export interface CourseIdQuery { - courseId: string; -} - -interface InsertReviewRequest { - token: string; - review: Review; - classId: string; -} - -export interface InsertUserRequest { - // TODO: one day, there may be types for this object. Today is not that day. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - googleObject: any; -} - -// The type of a query with a course number and subject -interface ClassByInfoQuery { - subject: string; - number: string; -} - -export interface ReviewRequest { - id: string; - token: string; -} - export const sanitizeReview = (doc: ReviewDocument) => { const copy = doc; copy.user = ""; diff --git a/server/endpoints/review/types.ts b/server/endpoints/review/types.ts new file mode 100644 index 000000000..f67a412c2 --- /dev/null +++ b/server/endpoints/review/types.ts @@ -0,0 +1,29 @@ +import { Review } from "common"; + +// The type of a query with a courseId +export interface CourseIdQuery { + courseId: string; +} + +export interface InsertReviewRequest { + token: string; + review: Review; + classId: string; +} + +export interface InsertUserRequest { + // TODO: one day, there may be types for this object. Today is not that day. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + googleObject: any; +} + +// The type of a query with a course number and subject +export interface ClassByInfoQuery { + subject: string; + number: string; +} + +export interface ReviewRequest { + id: string; + token: string; +} diff --git a/server/endpoints/search/Search.ts b/server/endpoints/search/Search.ts index 92b29d1ca..cb56160aa 100644 --- a/server/endpoints/search/Search.ts +++ b/server/endpoints/search/Search.ts @@ -2,155 +2,8 @@ import { body } from "express-validator"; import { Context, Endpoint } from "../../endpoints"; import { Classes, Subjects, Professors } from "../../db/dbDefs"; - -// The type for a search query -interface Search { - query: string; -} - -/* - * These utility methods are taken from methods.ts - * Thanks again Dray! - */ - -// uses levenshtein algorithm to return the minimum edit distance between two strings. -// It is exposed here for testing -export const editDistance = (a, b) => { - if (a.length === 0) return b.length; - if (b.length === 0) return a.length; - - const matrix = []; - - // increment along the first column of each row - for (let i = 0; i <= b.length; i++) { - matrix[i] = [i]; - } - - // increment each column in the first row - let j; - for (j = 0; j <= a.length; j++) { - matrix[0][j] = j; - } - - // Fill in the rest of the matrix - for (let i = 1; i <= b.length; i++) { - for (j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) === a.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min( - matrix[i - 1][j - 1] + 1, // substitution - Math.min( - matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j] + 1, - ), - ); // deletion - } - } - } - - return matrix[b.length][a.length]; -}; - -// a wrapper for a comparator function to be used to sort courses by comparing their edit distance with the query -const courseSort = (query) => (a, b) => { - const aCourseStr = `${a.classSub} ${a.classNum}`; - const bCourseStr = `${b.classSub} ${b.classNum}`; - const queryLen = query.length; - return ( - editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) - - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) - ); -}; - -// Helper to check if a string is a subject code -// exposed for testing -export const isSubShorthand = async (sub: string) => { - const subCheck = await Subjects.find({ subShort: sub }).exec(); - return subCheck.length > 0; -}; - -// helper to format search within a subject -const searchWithinSubject = (sub: string, remainder: string) => Classes.find( - { - classSub: sub, - classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, - }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, -).exec(); - -export const regexClassesSearch = async (searchString) => { - try { - if (searchString !== undefined && searchString !== "") { - // check if first digit is a number. Catches searchs like "1100" - // if so, search only through the course numbers and return classes ordered by full name - const indexFirstDigit = searchString.search(/\d/); - if (indexFirstDigit === 0) { - // console.log("only numbers") - return Classes.find( - { classNum: { $regex: `.*${searchString}.*`, $options: "-i" } }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ) - .exec() - .then((classes) => classes.sort(courseSort(searchString))); - } - - // check if searchString is a subject, if so return only classes with this subject. Catches searches like "CS" - if (await isSubShorthand(searchString)) { - return Classes.find( - { classSub: searchString }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec(); - } - // check if text before space is subject, if so search only classes with this subject. - // Speeds up searches like "CS 1110" - const indexFirstSpace = searchString.search(" "); - if (indexFirstSpace !== -1) { - const strBeforeSpace = searchString.substring(0, indexFirstSpace); - const strAfterSpace = searchString.substring(indexFirstSpace + 1); - if (await isSubShorthand(strBeforeSpace)) { - // console.log("matches subject with space: " + strBeforeSpace) - return await searchWithinSubject(strBeforeSpace, strAfterSpace); - } - } - - // check if text is subject followed by course number (no space) - // if so search only classes with this subject. - // Speeds up searches like "CS1110" - if (indexFirstDigit !== -1) { - const strBeforeDigit = searchString.substring(0, indexFirstDigit); - const strAfterDigit = searchString.substring(indexFirstDigit); - if (await isSubShorthand(strBeforeDigit)) { - // console.log("matches subject with digit: " + String(strBeforeDigit)) - return await searchWithinSubject(strBeforeDigit, strAfterDigit); - } - } - - // last resort, search everything - // console.log("nothing matches"); - return Classes.find( - { classFull: { $regex: `.*${searchString}.*`, $options: "-i" } }, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec(); - } - // console.log("no search"); - return Classes.find( - {}, - {}, - { sort: { classFull: 1 }, limit: 200, reactive: false }, - ).exec(); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getClassesByQuery' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } -}; +import { Search } from "./types"; +import { courseSort, regexClassesSearch } from "./functions"; /* * Query for classes using a query diff --git a/server/endpoints/search/functions.ts b/server/endpoints/search/functions.ts new file mode 100644 index 000000000..badb43637 --- /dev/null +++ b/server/endpoints/search/functions.ts @@ -0,0 +1,145 @@ +import { Classes, Subjects } from "../../db/dbDefs"; + +/* + * These utility methods are taken from methods.ts + * Thanks again Dray! + */ + +// uses levenshtein algorithm to return the minimum edit distance between two strings. +// It is exposed here for testing +const editDistance = (a, b) => { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; + + const matrix = []; + + // increment along the first column of each row + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + // increment each column in the first row + let j; + for (j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (let i = 1; i <= b.length; i++) { + for (j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + Math.min( + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1, + ), + ); // deletion + } + } + } + + return matrix[b.length][a.length]; +}; + +// a wrapper for a comparator function to be used to sort courses by comparing their edit distance with the query +export const courseSort = (query) => (a, b) => { + const aCourseStr = `${a.classSub} ${a.classNum}`; + const bCourseStr = `${b.classSub} ${b.classNum}`; + const queryLen = query.length; + return ( + editDistance(query.toLowerCase(), aCourseStr.slice(0, queryLen)) + - editDistance(query.toLowerCase(), bCourseStr.slice(0, queryLen)) + ); +}; + +// Helper to check if a string is a subject code +// exposed for testing +const isSubShorthand = async (sub: string) => { + const subCheck = await Subjects.find({ subShort: sub }).exec(); + return subCheck.length > 0; +}; + +// helper to format search within a subject +const searchWithinSubject = (sub: string, remainder: string) => Classes.find( + { + classSub: sub, + classFull: { $regex: `.*${remainder}.*`, $options: "-i" }, + }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, +).exec(); + +export const regexClassesSearch = async (searchString) => { + try { + if (searchString !== undefined && searchString !== "") { + // check if first digit is a number. Catches searchs like "1100" + // if so, search only through the course numbers and return classes ordered by full name + const indexFirstDigit = searchString.search(/\d/); + if (indexFirstDigit === 0) { + // console.log("only numbers") + return Classes.find( + { classNum: { $regex: `.*${searchString}.*`, $options: "-i" } }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ) + .exec() + .then((classes) => classes.sort(courseSort(searchString))); + } + + // check if searchString is a subject, if so return only classes with this subject. Catches searches like "CS" + if (await isSubShorthand(searchString)) { + return Classes.find( + { classSub: searchString }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); + } + // check if text before space is subject, if so search only classes with this subject. + // Speeds up searches like "CS 1110" + const indexFirstSpace = searchString.search(" "); + if (indexFirstSpace !== -1) { + const strBeforeSpace = searchString.substring(0, indexFirstSpace); + const strAfterSpace = searchString.substring(indexFirstSpace + 1); + if (await isSubShorthand(strBeforeSpace)) { + // console.log("matches subject with space: " + strBeforeSpace) + return await searchWithinSubject(strBeforeSpace, strAfterSpace); + } + } + + // check if text is subject followed by course number (no space) + // if so search only classes with this subject. + // Speeds up searches like "CS1110" + if (indexFirstDigit !== -1) { + const strBeforeDigit = searchString.substring(0, indexFirstDigit); + const strAfterDigit = searchString.substring(indexFirstDigit); + if (await isSubShorthand(strBeforeDigit)) { + // console.log("matches subject with digit: " + String(strBeforeDigit)) + return await searchWithinSubject(strBeforeDigit, strAfterDigit); + } + } + + // last resort, search everything + // console.log("nothing matches"); + return Classes.find( + { classFull: { $regex: `.*${searchString}.*`, $options: "-i" } }, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); + } + // console.log("no search"); + return Classes.find( + {}, + {}, + { sort: { classFull: 1 }, limit: 200, reactive: false }, + ).exec(); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getClassesByQuery' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; diff --git a/server/endpoints/search/types.ts b/server/endpoints/search/types.ts new file mode 100644 index 000000000..e2adf4a9c --- /dev/null +++ b/server/endpoints/search/types.ts @@ -0,0 +1,4 @@ +// The type for a search query +export interface Search { + query: string; +} From 75811a008f7054e6520126d36f1299a4f95929c7 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:29:54 -0400 Subject: [PATCH 07/33] rename to routes.ts files --- server/endpoints.ts | 8 ++++---- server/endpoints/admin/AdminActions.ts | 2 +- server/endpoints/auth/{Auth.ts => routes.ts} | 0 server/endpoints/profile/{Profile.ts => routes.ts} | 2 +- server/endpoints/review/{Review.ts => routes.ts} | 2 +- server/endpoints/search/{Search.ts => routes.ts} | 0 server/endpoints/utils/utils.ts | 4 ++-- server/tsconfig.json | 6 +++--- 8 files changed, 12 insertions(+), 12 deletions(-) rename server/endpoints/auth/{Auth.ts => routes.ts} (100%) rename server/endpoints/profile/{Profile.ts => routes.ts} (98%) rename server/endpoints/review/{Review.ts => routes.ts} (99%) rename server/endpoints/search/{Search.ts => routes.ts} (100%) diff --git a/server/endpoints.ts b/server/endpoints.ts index 29ece3dad..bab786b7b 100644 --- a/server/endpoints.ts +++ b/server/endpoints.ts @@ -15,21 +15,21 @@ import { getCourseByInfo, updateLiked, userHasLiked, -} from "./endpoints/review/Review"; +} from "./endpoints/review/routes"; import { countReviewsByStudentId, getTotalLikesByStudentId, getReviewsByStudentId, getStudentEmailByToken, -} from "./endpoints/profile/Profile"; -import { tokenIsAdmin } from "./endpoints/auth/Auth"; +} from "./endpoints/profile/routes"; +import { tokenIsAdmin } from "./endpoints/auth/routes"; import { getCoursesByProfessor, getCoursesByMajor, getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, -} from "./endpoints/search/Search"; +} from "./endpoints/search/routes"; import { fetchReviewableClasses, reportReview, diff --git a/server/endpoints/admin/AdminActions.ts b/server/endpoints/admin/AdminActions.ts index c818b2dec..1fddf8b17 100644 --- a/server/endpoints/admin/AdminActions.ts +++ b/server/endpoints/admin/AdminActions.ts @@ -9,7 +9,7 @@ import { resetProfessorArray, } from "../../db/dbInit"; import { getCourseById, verifyToken } from "../utils/utils"; -import { ReviewRequest } from "../review/Review"; +import { ReviewRequest } from "../review/routes"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; // This updates the metrics for an individual class given its Mongo-generated id. diff --git a/server/endpoints/auth/Auth.ts b/server/endpoints/auth/routes.ts similarity index 100% rename from server/endpoints/auth/Auth.ts rename to server/endpoints/auth/routes.ts diff --git a/server/endpoints/profile/Profile.ts b/server/endpoints/profile/routes.ts similarity index 98% rename from server/endpoints/profile/Profile.ts rename to server/endpoints/profile/routes.ts index 67add0335..cb1c06560 100644 --- a/server/endpoints/profile/Profile.ts +++ b/server/endpoints/profile/routes.ts @@ -3,7 +3,7 @@ import { Context, Endpoint } from "../../endpoints"; import { ReviewDocument, Reviews, Students } from "../../db/dbDefs"; import { ProfileRequest, NetIdQuery } from "./types"; -import { getVerificationTicket } from "../auth/Auth"; +import { getVerificationTicket } from "../auth/routes"; export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], diff --git a/server/endpoints/review/Review.ts b/server/endpoints/review/routes.ts similarity index 99% rename from server/endpoints/review/Review.ts rename to server/endpoints/review/routes.ts index 54c853023..a242fab6b 100644 --- a/server/endpoints/review/Review.ts +++ b/server/endpoints/review/routes.ts @@ -7,7 +7,7 @@ import { insertUser as insertUserCallback, JSONNonempty, } from "../utils/utils"; -import { getVerificationTicket } from "../auth/Auth"; +import { getVerificationTicket } from "../auth/routes"; import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; import shortid = require("shortid"); diff --git a/server/endpoints/search/Search.ts b/server/endpoints/search/routes.ts similarity index 100% rename from server/endpoints/search/Search.ts rename to server/endpoints/search/routes.ts diff --git a/server/endpoints/utils/utils.ts b/server/endpoints/utils/utils.ts index f9de85e8c..0291ca0ce 100644 --- a/server/endpoints/utils/utils.ts +++ b/server/endpoints/utils/utils.ts @@ -1,7 +1,7 @@ import { ValidationChain, body } from "express-validator"; -import { InsertUserRequest, CourseIdQuery } from "../review/Review"; +import { InsertUserRequest, CourseIdQuery } from "../review/routes"; import { Classes, Students } from "../../db/dbDefs"; -import { getUserByNetId, getVerificationTicket } from "../auth/Auth"; +import { getUserByNetId, getVerificationTicket } from "../auth/routes"; import shortid = require("shortid"); diff --git a/server/tsconfig.json b/server/tsconfig.json index 9feb434db..4470d18c4 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -18,9 +18,9 @@ "endpoints.ts", "endpoints/admin/AdminActions.ts", "endpoints/admin/AdminChart.ts", - "endpoints/auth/Auth.ts", - "endpoints/review/Review.ts", - "endpoints/search/Search.ts", + "endpoints/auth/routes.ts", + "endpoints/review/routes.ts", + "endpoints/search/routes.ts", "endpoints/utils/utils.ts" ] } \ No newline at end of file From c925a840bc3b4275a361b08b811178cd78defd66 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:48:36 -0400 Subject: [PATCH 08/33] lint --- server/endpoints/admin/functions.ts | 1 + server/endpoints/auth/functions.ts | 3 ++- server/endpoints/test/AdminChart.test.ts | 2 +- server/endpoints/test/Auth.test.ts | 2 +- server/endpoints/test/Profile.test.ts | 2 +- server/endpoints/test/Review.test.ts | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server/endpoints/admin/functions.ts b/server/endpoints/admin/functions.ts index fb52b5014..c8d595488 100644 --- a/server/endpoints/admin/functions.ts +++ b/server/endpoints/admin/functions.ts @@ -7,6 +7,7 @@ import { DefaultDict } from "./AdminChart"; /** * Helper function for [topSubjects] */ +// eslint-disable-next-line import/prefer-default-export export const topSubjectsCB = async (_ctx: Context, request: Token) => { const userIsAdmin = await verifyToken(request.token); if (!userIsAdmin) { diff --git a/server/endpoints/auth/functions.ts b/server/endpoints/auth/functions.ts index 0c7f06297..236ad4d2c 100644 --- a/server/endpoints/auth/functions.ts +++ b/server/endpoints/auth/functions.ts @@ -1,6 +1,7 @@ import { OAuth2Client } from "google-auth-library"; -export const verifyTicket = (token: string, audience: string) => { +// eslint-disable-next-line import/prefer-default-export +export const verifyTicket = async (token: string, audience: string) => { const client = new OAuth2Client(audience); const ticket = await client.verifyIdToken({ idToken: token, diff --git a/server/endpoints/test/AdminChart.test.ts b/server/endpoints/test/AdminChart.test.ts index 61f5545f5..60bab075c 100644 --- a/server/endpoints/test/AdminChart.test.ts +++ b/server/endpoints/test/AdminChart.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Class, Review, Student, Subject } from "common"; -import * as Auth from "../auth/Auth"; +import * as Auth from "../auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/endpoints/test/Auth.test.ts b/server/endpoints/test/Auth.test.ts index d2b0ec789..7d63f5d3b 100644 --- a/server/endpoints/test/Auth.test.ts +++ b/server/endpoints/test/Auth.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library/build/src/auth/loginticket"; import { Student } from "common"; -import * as Auth from "../auth/Auth"; +import * as Auth from "../auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/endpoints/test/Profile.test.ts b/server/endpoints/test/Profile.test.ts index c8e5c6e56..7bec60dfc 100644 --- a/server/endpoints/test/Profile.test.ts +++ b/server/endpoints/test/Profile.test.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review, Student, Class, Subject, Professor } from "common"; -import * as Auth from "../auth/Auth"; +import * as Auth from "../auth/routes"; import TestingServer, { testingPort } from "./TestServer"; diff --git a/server/endpoints/test/Review.test.ts b/server/endpoints/test/Review.test.ts index e7514ccba..39f07fc2e 100644 --- a/server/endpoints/test/Review.test.ts +++ b/server/endpoints/test/Review.test.ts @@ -4,7 +4,7 @@ import { TokenPayload } from "google-auth-library"; import { Review } from "common"; import { Reviews, Students } from "../../db/dbDefs"; -import * as Auth from "../auth/Auth"; +import * as Auth from "../auth/routes"; import TestingServer from "./TestServer"; const testingPort = 8080; From 846c7074b173f2a2dab482b6e629b81369b3e975 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:54:04 -0400 Subject: [PATCH 09/33] lint --- server/endpoints/admin/AdminActions.ts | 2 +- server/endpoints/utils/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/endpoints/admin/AdminActions.ts b/server/endpoints/admin/AdminActions.ts index 1fddf8b17..6147fc0d5 100644 --- a/server/endpoints/admin/AdminActions.ts +++ b/server/endpoints/admin/AdminActions.ts @@ -9,7 +9,7 @@ import { resetProfessorArray, } from "../../db/dbInit"; import { getCourseById, verifyToken } from "../utils/utils"; -import { ReviewRequest } from "../review/routes"; +import { ReviewRequest } from "../review/types"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; // This updates the metrics for an individual class given its Mongo-generated id. diff --git a/server/endpoints/utils/utils.ts b/server/endpoints/utils/utils.ts index 0291ca0ce..30ce60cea 100644 --- a/server/endpoints/utils/utils.ts +++ b/server/endpoints/utils/utils.ts @@ -1,5 +1,5 @@ import { ValidationChain, body } from "express-validator"; -import { InsertUserRequest, CourseIdQuery } from "../review/routes"; +import { InsertUserRequest, CourseIdQuery } from "../review/types"; import { Classes, Students } from "../../db/dbDefs"; import { getUserByNetId, getVerificationTicket } from "../auth/routes"; From 59b56ad59db4aa2a27b4452997cd67f7c74c8baa Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 12 Sep 2023 02:58:05 -0400 Subject: [PATCH 10/33] move test files --- server/{endpoints => }/test/AdminActions.test.ts | 6 +++--- server/{endpoints => }/test/AdminChart.test.ts | 2 +- server/{endpoints => }/test/Auth.test.ts | 2 +- server/{endpoints => }/test/Profile.test.ts | 2 +- server/{endpoints => }/test/Review.test.ts | 4 ++-- server/{endpoints => }/test/Search.test.ts | 0 server/{endpoints => }/test/TestServer.ts | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) rename server/{endpoints => }/test/AdminActions.test.ts (96%) rename server/{endpoints => }/test/AdminChart.test.ts (99%) rename server/{endpoints => }/test/Auth.test.ts (97%) rename server/{endpoints => }/test/Profile.test.ts (99%) rename server/{endpoints => }/test/Review.test.ts (98%) rename server/{endpoints => }/test/Search.test.ts (100%) rename server/{endpoints => }/test/TestServer.ts (96%) diff --git a/server/endpoints/test/AdminActions.test.ts b/server/test/AdminActions.test.ts similarity index 96% rename from server/endpoints/test/AdminActions.test.ts rename to server/test/AdminActions.test.ts index 5cc3a777e..dc9de6418 100644 --- a/server/endpoints/test/AdminActions.test.ts +++ b/server/test/AdminActions.test.ts @@ -3,9 +3,9 @@ import { MongoMemoryServer } from "mongodb-memory-server"; import express from "express"; import axios from "axios"; -import { configure } from "../../endpoints"; -import { Classes, Reviews } from "../../db/dbDefs"; -import * as Utils from "../utils/utils"; +import { configure } from "../endpoints"; +import { Classes, Reviews } from "../db/dbDefs"; +import * as Utils from "../endpoints/utils/utils"; let mongoServer: MongoMemoryServer; let serverCloseHandle; diff --git a/server/endpoints/test/AdminChart.test.ts b/server/test/AdminChart.test.ts similarity index 99% rename from server/endpoints/test/AdminChart.test.ts rename to server/test/AdminChart.test.ts index 60bab075c..8ebea95d3 100644 --- a/server/endpoints/test/AdminChart.test.ts +++ b/server/test/AdminChart.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Class, Review, Student, Subject } from "common"; -import * as Auth from "../auth/routes"; +import * as Auth from "../endpoints/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/endpoints/test/Auth.test.ts b/server/test/Auth.test.ts similarity index 97% rename from server/endpoints/test/Auth.test.ts rename to server/test/Auth.test.ts index 7d63f5d3b..b2d649a9e 100644 --- a/server/endpoints/test/Auth.test.ts +++ b/server/test/Auth.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library/build/src/auth/loginticket"; import { Student } from "common"; -import * as Auth from "../auth/routes"; +import * as Auth from "../endpoints/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/endpoints/test/Profile.test.ts b/server/test/Profile.test.ts similarity index 99% rename from server/endpoints/test/Profile.test.ts rename to server/test/Profile.test.ts index 7bec60dfc..ab6d5e22b 100644 --- a/server/endpoints/test/Profile.test.ts +++ b/server/test/Profile.test.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review, Student, Class, Subject, Professor } from "common"; -import * as Auth from "../auth/routes"; +import * as Auth from "../endpoints/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; diff --git a/server/endpoints/test/Review.test.ts b/server/test/Review.test.ts similarity index 98% rename from server/endpoints/test/Review.test.ts rename to server/test/Review.test.ts index 39f07fc2e..15caaeb94 100644 --- a/server/endpoints/test/Review.test.ts +++ b/server/test/Review.test.ts @@ -3,8 +3,8 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review } from "common"; -import { Reviews, Students } from "../../db/dbDefs"; -import * as Auth from "../auth/routes"; +import { Reviews, Students } from "../db/dbDefs"; +import * as Auth from "../endpoints/auth/routes"; import TestingServer from "./TestServer"; const testingPort = 8080; diff --git a/server/endpoints/test/Search.test.ts b/server/test/Search.test.ts similarity index 100% rename from server/endpoints/test/Search.test.ts rename to server/test/Search.test.ts diff --git a/server/endpoints/test/TestServer.ts b/server/test/TestServer.ts similarity index 96% rename from server/endpoints/test/TestServer.ts rename to server/test/TestServer.ts index a6774b8df..b6c75bb80 100644 --- a/server/endpoints/test/TestServer.ts +++ b/server/test/TestServer.ts @@ -9,8 +9,8 @@ import { Subjects, Professors, Reviews, -} from "../../db/dbDefs"; -import { configure } from "../../endpoints"; +} from "../db/dbDefs"; +import { configure } from "../endpoints"; export const testingPort = 8080; From 0ffe723677ba761d080c3c15c31b99012d257f2e Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 12:13:40 -0400 Subject: [PATCH 11/33] add new dao layer --- server/db/dbDefs.ts | 2 + server/endpoints.ts | 12 +-- server/endpoints/auth/functions.ts | 12 --- .../api}/admin/AdminActions.ts | 11 +-- .../api}/admin/AdminChart.ts | 6 +- .../{endpoints => src/api}/admin/functions.ts | 6 +- server/{endpoints => src/api}/admin/types.ts | 2 +- server/src/api/auth/routes.ts | 13 ++++ server/{endpoints => src/api}/auth/types.ts | 0 .../{endpoints => src/api}/profile/routes.ts | 56 ++++++++++---- .../{endpoints => src/api}/profile/types.ts | 0 server/src/api/review/db.ts | 1 + .../utils.ts => src/api/review/functions.ts} | 73 ++++--------------- .../{endpoints => src/api}/review/routes.ts | 20 +++-- server/{endpoints => src/api}/review/types.ts | 0 .../api}/search/functions.ts | 2 +- .../{endpoints => src/api}/search/routes.ts | 4 +- server/{endpoints => src/api}/search/types.ts | 0 server/src/dao/Classes.ts | 19 +++++ server/src/dao/Reviews.ts | 14 ++++ server/src/dao/Student.ts | 19 +++++ .../auth/routes.ts => src/utils/utils.ts} | 50 ++++++------- server/tsconfig.json | 12 +-- 23 files changed, 185 insertions(+), 149 deletions(-) delete mode 100644 server/endpoints/auth/functions.ts rename server/{endpoints => src/api}/admin/AdminActions.ts (96%) rename server/{endpoints => src/api}/admin/AdminChart.ts (98%) rename server/{endpoints => src/api}/admin/functions.ts (94%) rename server/{endpoints => src/api}/admin/types.ts (90%) create mode 100644 server/src/api/auth/routes.ts rename server/{endpoints => src/api}/auth/types.ts (100%) rename server/{endpoints => src/api}/profile/routes.ts (64%) rename server/{endpoints => src/api}/profile/types.ts (100%) create mode 100644 server/src/api/review/db.ts rename server/{endpoints/utils/utils.ts => src/api/review/functions.ts} (56%) rename server/{endpoints => src/api}/review/routes.ts (96%) rename server/{endpoints => src/api}/review/types.ts (100%) rename server/{endpoints => src/api}/search/functions.ts (98%) rename server/{endpoints => src/api}/search/routes.ts (96%) rename server/{endpoints => src/api}/search/types.ts (100%) create mode 100644 server/src/dao/Classes.ts create mode 100644 server/src/dao/Reviews.ts create mode 100644 server/src/dao/Student.ts rename server/{endpoints/auth/routes.ts => src/utils/utils.ts} (50%) diff --git a/server/db/dbDefs.ts b/server/db/dbDefs.ts index 040186bb0..3df8c4b11 100644 --- a/server/db/dbDefs.ts +++ b/server/db/dbDefs.ts @@ -53,6 +53,7 @@ const StudentSchema = new Schema({ reviews: { type: [String] }, // the reviews that this user has posted. likedReviews: { type: [String] }, }); + export const Students = mongoose.model( "students", StudentSchema, @@ -72,6 +73,7 @@ const SubjectSchema = new Schema({ subShort: { type: String }, // subject, like "PHIL" or "CS" subFull: { type: String }, // subject full name, like 'Computer Science' }); + export const Subjects = mongoose.model( "subjects", SubjectSchema, diff --git a/server/endpoints.ts b/server/endpoints.ts index bab786b7b..22fdf5e77 100644 --- a/server/endpoints.ts +++ b/server/endpoints.ts @@ -6,7 +6,7 @@ import { howManyEachClass, topSubjects, getReviewsOverTimeTop15, -} from "./endpoints/admin/AdminChart"; +} from "./src/api/admin/AdminChart"; import { getReviewsByCourseId, getCourseById, @@ -15,21 +15,21 @@ import { getCourseByInfo, updateLiked, userHasLiked, -} from "./endpoints/review/routes"; +} from "./src/api/review/routes"; import { countReviewsByStudentId, getTotalLikesByStudentId, getReviewsByStudentId, getStudentEmailByToken, -} from "./endpoints/profile/routes"; -import { tokenIsAdmin } from "./endpoints/auth/routes"; +} from "./src/api/profile/routes"; +import { tokenIsAdmin } from "./src/api/auth/routes"; import { getCoursesByProfessor, getCoursesByMajor, getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, -} from "./endpoints/search/routes"; +} from "./src/api/search/routes"; import { fetchReviewableClasses, reportReview, @@ -37,7 +37,7 @@ import { undoReportReview, removeReview, getRaffleWinner, -} from "./endpoints/admin/AdminActions"; +} from "./src/api/admin/AdminActions"; export interface Context { ip: string; diff --git a/server/endpoints/auth/functions.ts b/server/endpoints/auth/functions.ts deleted file mode 100644 index 236ad4d2c..000000000 --- a/server/endpoints/auth/functions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OAuth2Client } from "google-auth-library"; - -// eslint-disable-next-line import/prefer-default-export -export const verifyTicket = async (token: string, audience: string) => { - const client = new OAuth2Client(audience); - const ticket = await client.verifyIdToken({ - idToken: token, - audience, - }); - - return ticket.getPayload(); -}; diff --git a/server/endpoints/admin/AdminActions.ts b/server/src/api/admin/AdminActions.ts similarity index 96% rename from server/endpoints/admin/AdminActions.ts rename to server/src/api/admin/AdminActions.ts index 6147fc0d5..8cdfa9f92 100644 --- a/server/endpoints/admin/AdminActions.ts +++ b/server/src/api/admin/AdminActions.ts @@ -1,14 +1,15 @@ import { body } from "express-validator"; import { getCrossListOR, getMetricValues } from "common/CourseCard"; -import { Context, Endpoint } from "../../endpoints"; -import { Reviews, Classes, Students } from "../../db/dbDefs"; +import { Context, Endpoint } from "../../../endpoints"; +import { Reviews, Classes, Students } from "../../../db/dbDefs"; import { updateProfessors, findAllSemesters, resetProfessorArray, -} from "../../db/dbInit"; -import { getCourseById, verifyToken } from "../utils/utils"; +} from "../../../db/dbInit"; +import { verifyToken } from "../../utils/utils"; +import { getCourseById } from "../../dao/Classes"; import { ReviewRequest } from "../review/types"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; @@ -16,7 +17,7 @@ import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } // Returns 1 if successful, 0 otherwise. export const updateCourseMetrics = async (courseId) => { try { - const course = await getCourseById({ courseId }); + const course = await getCourseById(courseId); if (course) { const crossListOR = getCrossListOR(course); const reviews = await Reviews.find( diff --git a/server/endpoints/admin/AdminChart.ts b/server/src/api/admin/AdminChart.ts similarity index 98% rename from server/endpoints/admin/AdminChart.ts rename to server/src/api/admin/AdminChart.ts index 050642efe..266c7cc6f 100644 --- a/server/endpoints/admin/AdminChart.ts +++ b/server/src/api/admin/AdminChart.ts @@ -1,8 +1,8 @@ /* eslint-disable spaced-comment */ import { body } from "express-validator"; -import { verifyToken } from "../utils/utils"; -import { Context, Endpoint } from "../../endpoints"; -import { Reviews, Classes, Subjects } from "../../db/dbDefs"; +import { verifyToken } from "../../utils/utils"; +import { Context, Endpoint } from "../../../endpoints"; +import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; import { GetReviewsOverTimeTop15Request, Token } from "./types"; import { topSubjectsCB } from "./functions"; diff --git a/server/endpoints/admin/functions.ts b/server/src/api/admin/functions.ts similarity index 94% rename from server/endpoints/admin/functions.ts rename to server/src/api/admin/functions.ts index c8d595488..f8dde63bb 100644 --- a/server/endpoints/admin/functions.ts +++ b/server/src/api/admin/functions.ts @@ -1,7 +1,7 @@ -import { verifyToken } from "../utils/utils"; -import { Context } from "../../endpoints"; +import { verifyToken } from "../../utils/utils"; +import { Context } from "../../../endpoints"; import { Token } from "./types"; -import { Reviews, Classes, Subjects } from "../../db/dbDefs"; +import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; import { DefaultDict } from "./AdminChart"; /** diff --git a/server/endpoints/admin/types.ts b/server/src/api/admin/types.ts similarity index 90% rename from server/endpoints/admin/types.ts rename to server/src/api/admin/types.ts index 23f2521f6..b4b3a1550 100644 --- a/server/endpoints/admin/types.ts +++ b/server/src/api/admin/types.ts @@ -1,4 +1,4 @@ -import { ReviewDocument } from "../../db/dbDefs"; +import { ReviewDocument } from "../../../db/dbDefs"; export interface Token { token: string; diff --git a/server/src/api/auth/routes.ts b/server/src/api/auth/routes.ts new file mode 100644 index 000000000..e436828c2 --- /dev/null +++ b/server/src/api/auth/routes.ts @@ -0,0 +1,13 @@ +import { body } from "express-validator"; +import { Context, Endpoint } from "../../../endpoints"; +import { verifyToken } from "../../utils/utils"; +import { AdminRequest } from "./types"; + +/* + * Check if a token is for an admin + */ +// eslint-disable-next-line import/prefer-default-export +export const tokenIsAdmin: Endpoint = { + guard: [body("token").notEmpty().isAscii()], + callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), +}; diff --git a/server/endpoints/auth/types.ts b/server/src/api/auth/types.ts similarity index 100% rename from server/endpoints/auth/types.ts rename to server/src/api/auth/types.ts diff --git a/server/endpoints/profile/routes.ts b/server/src/api/profile/routes.ts similarity index 64% rename from server/endpoints/profile/routes.ts rename to server/src/api/profile/routes.ts index cb1c06560..6e8128814 100644 --- a/server/endpoints/profile/routes.ts +++ b/server/src/api/profile/routes.ts @@ -1,9 +1,10 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { ReviewDocument, Reviews, Students } from "../../db/dbDefs"; +import { Context, Endpoint } from "../../../endpoints"; +import { ReviewDocument, Reviews } from "../../../db/dbDefs"; import { ProfileRequest, NetIdQuery } from "./types"; - -import { getVerificationTicket } from "../auth/routes"; +import { getVerificationTicket } from "../../utils/utils"; +import { getUserByNetId } from "../../dao/Student"; +import { getReviewById } from "../../dao/Reviews"; export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], @@ -12,6 +13,9 @@ export const getStudentEmailByToken: Endpoint = { try { const ticket = await getVerificationTicket(token); + if (ticket === undefined || ticket === null) { + return { code: 404, message: "Unable to verify token" }; + } if (ticket.hd === "cornell.edu") { return { code: 200, message: ticket.email }; } @@ -35,7 +39,10 @@ export const countReviewsByStudentId: Endpoint = { callback: async (ctx: Context, request: NetIdQuery) => { const { netId } = request; try { - const student = await Students.findOne({ netId }); + const student = await getUserByNetId(netId); + if (student === null || student === undefined) { + return { code: 404, message: "Unable to find student with netId: ", netId }; + } if (student.reviews == null) { return { code: 500, message: "No reviews object were associated." }; } @@ -60,17 +67,27 @@ export const getTotalLikesByStudentId: Endpoint = { const { netId } = request; let totalLikes = 0; try { - const studentDoc = await Students.findOne({ netId }); + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null || studentDoc === undefined) { + return { + code: 404, + message: "Unable to find student with netId: ", + netId, + }; + } const reviewIds = studentDoc.reviews; - let reviews: ReviewDocument[] = await Promise.all( - reviewIds.map( - async (reviewId) => await Reviews.findOne({ _id: reviewId }), - ), + if (reviewIds == null) { + return { code: 500, message: "No reviews object were associated." }; + } + const results = await Promise.all( + reviewIds.map(async (reviewId) => await getReviewById(reviewId)), ); - reviews = reviews.filter((review) => review !== null); + const reviews: ReviewDocument[] = results.filter((review) => review !== null); reviews.forEach((review) => { if ("likes" in review) { - totalLikes += review.likes; + if (review.likes !== undefined) { + totalLikes += review.likes; + } } }); @@ -93,17 +110,24 @@ export const getReviewsByStudentId: Endpoint = { callback: async (ctx: Context, request: NetIdQuery) => { const { netId } = request; try { - const studentDoc = await Students.findOne({ netId }); + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null || studentDoc === undefined) { + return { + code: 404, + message: "Unable to find student with netId: ", + netId, + }; + } const reviewIds = studentDoc.reviews; if (reviewIds === null) { return { code: 200, message: [] }; } - let reviews: ReviewDocument[] = await Promise.all( + const results = await Promise.all( reviewIds.map( - async (reviewId) => await Reviews.findOne({ _id: reviewId }), + async (reviewId) => await getReviewById(reviewId), ), ); - reviews = reviews.filter((review) => review !== null); + const reviews: ReviewDocument[] = results.filter((review) => review !== null && review !== undefined); return { code: 200, message: reviews }; } catch (error) { // eslint-disable-next-line no-console diff --git a/server/endpoints/profile/types.ts b/server/src/api/profile/types.ts similarity index 100% rename from server/endpoints/profile/types.ts rename to server/src/api/profile/types.ts diff --git a/server/src/api/review/db.ts b/server/src/api/review/db.ts new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/server/src/api/review/db.ts @@ -0,0 +1 @@ + diff --git a/server/endpoints/utils/utils.ts b/server/src/api/review/functions.ts similarity index 56% rename from server/endpoints/utils/utils.ts rename to server/src/api/review/functions.ts index 30ce60cea..9ad9f8d4e 100644 --- a/server/endpoints/utils/utils.ts +++ b/server/src/api/review/functions.ts @@ -1,26 +1,18 @@ import { ValidationChain, body } from "express-validator"; -import { InsertUserRequest, CourseIdQuery } from "../review/types"; -import { Classes, Students } from "../../db/dbDefs"; -import { getUserByNetId, getVerificationTicket } from "../auth/routes"; - -import shortid = require("shortid"); - -// eslint-disable-next-line import/prefer-default-export -export const getCourseById = async (courseId: CourseIdQuery) => { - try { - // check: make sure course id is valid and non-malicious - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(courseId.courseId)) { - return await Classes.findOne({ _id: courseId.courseId }).exec(); - } - return { error: "Malformed Query" }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getCourseById' method"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; - } +import shortid from "shortid"; +import { InsertUserRequest } from "./types"; +import { getUserByNetId } from "../../dao/Student"; +import { Students } from "../../../db/dbDefs"; +/** + * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] + * has non-empty fields listed in [fields]. + */ +export const JSONNonempty = (jsonFieldName: string, fields: string[]) => { + const ret: ValidationChain[] = []; + fields.forEach((fieldName) => { + ret.push(body(`${jsonFieldName}.${fieldName}`).notEmpty()); + }); + return ret; }; /** @@ -29,6 +21,7 @@ export const getCourseById = async (courseId: CourseIdQuery) => { * Returns 1 if the user was added to the database, or was already present * Returns 0 if there was an error */ +// eslint-disable-next-line import/prefer-default-export export const insertUser = async (request: InsertUserRequest) => { const { googleObject } = request; try { @@ -66,39 +59,3 @@ export const insertUser = async (request: InsertUserRequest) => { return 0; } }; - -/** - * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] - * has non-empty fields listed in [fields]. - */ -export const JSONNonempty = (jsonFieldName: string, fields: string[]) => { - const ret: ValidationChain[] = []; - fields.forEach((fieldName) => { - ret.push(body(`${jsonFieldName}.${fieldName}`).notEmpty()); - }); - return ret; -}; - -export const verifyToken = async (token: string) => { - try { - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(token)) { - const ticket = await getVerificationTicket(token); - if (ticket && ticket.email) { - const user = await getUserByNetId( - ticket.email.replace("@cornell.edu", ""), - ); - if (user) { - return user.privilege === "admin"; - } - } - } - return false; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'verifyToken' method"); - // eslint-disable-next-line no-console - console.log(error); - return false; - } -}; diff --git a/server/endpoints/review/routes.ts b/server/src/api/review/routes.ts similarity index 96% rename from server/endpoints/review/routes.ts rename to server/src/api/review/routes.ts index a242fab6b..104d1a489 100644 --- a/server/endpoints/review/routes.ts +++ b/server/src/api/review/routes.ts @@ -1,16 +1,14 @@ import { body } from "express-validator"; import { getCrossListOR } from "common/CourseCard"; -import { Context, Endpoint } from "../../endpoints"; -import { Classes, ReviewDocument, Reviews, Students } from "../../db/dbDefs"; +import shortid from "shortid"; +import { Context, Endpoint } from "../../../endpoints"; +import { Classes, ReviewDocument, Reviews, Students } from "../../../db/dbDefs"; import { - getCourseById as getCourseByIdCallback, - insertUser as insertUserCallback, - JSONNonempty, -} from "../utils/utils"; -import { getVerificationTicket } from "../auth/routes"; -import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; + getVerificationTicket } from "../../utils/utils"; +import { getCourseById as getCourseByIdCallback } from "../../dao/Classes"; +import { insertUser as insertUserCallback, JSONNonempty } from "./functions"; -import shortid = require("shortid"); +import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; export const sanitizeReview = (doc: ReviewDocument) => { const copy = doc; @@ -33,7 +31,7 @@ export const sanitizeReviews = (lst: ReviewDocument[]) => lst.map((doc) => sanit */ export const getCourseById: Endpoint = { guard: [body("courseId").notEmpty().isAscii()], - callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg), + callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg.courseId), }; /* @@ -68,7 +66,7 @@ export const getReviewsByCourseId: Endpoint = { guard: [body("courseId").notEmpty().isAscii()], callback: async (ctx: Context, courseId: CourseIdQuery) => { try { - const course = await getCourseByIdCallback(courseId); + const course = await getCourseByIdCallback(courseId.courseId); if (course) { const crossListOR = getCrossListOR(course); const reviews = await Reviews.find( diff --git a/server/endpoints/review/types.ts b/server/src/api/review/types.ts similarity index 100% rename from server/endpoints/review/types.ts rename to server/src/api/review/types.ts diff --git a/server/endpoints/search/functions.ts b/server/src/api/search/functions.ts similarity index 98% rename from server/endpoints/search/functions.ts rename to server/src/api/search/functions.ts index badb43637..d671c2099 100644 --- a/server/endpoints/search/functions.ts +++ b/server/src/api/search/functions.ts @@ -1,4 +1,4 @@ -import { Classes, Subjects } from "../../db/dbDefs"; +import { Classes, Subjects } from "../../../db/dbDefs"; /* * These utility methods are taken from methods.ts diff --git a/server/endpoints/search/routes.ts b/server/src/api/search/routes.ts similarity index 96% rename from server/endpoints/search/routes.ts rename to server/src/api/search/routes.ts index cb56160aa..4d2e09401 100644 --- a/server/endpoints/search/routes.ts +++ b/server/src/api/search/routes.ts @@ -1,7 +1,7 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { Classes, Subjects, Professors } from "../../db/dbDefs"; +import { Context, Endpoint } from "../../../endpoints"; +import { Classes, Subjects, Professors } from "../../../db/dbDefs"; import { Search } from "./types"; import { courseSort, regexClassesSearch } from "./functions"; diff --git a/server/endpoints/search/types.ts b/server/src/api/search/types.ts similarity index 100% rename from server/endpoints/search/types.ts rename to server/src/api/search/types.ts diff --git a/server/src/dao/Classes.ts b/server/src/dao/Classes.ts new file mode 100644 index 000000000..4ab8807f4 --- /dev/null +++ b/server/src/dao/Classes.ts @@ -0,0 +1,19 @@ +import { Classes } from "../../db/dbDefs"; + +// eslint-disable-next-line import/prefer-default-export +export const getCourseById = async (courseId: string) => { + try { + // check: make sure course id is valid and non-malicious + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(courseId)) { + return await Classes.findOne({ _id: courseId }).exec(); + } + return { error: "Malformed Query" }; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getCourseById' method"); + // eslint-disable-next-line no-console + console.log(error); + return { error: "Internal Server Error" }; + } +}; diff --git a/server/src/dao/Reviews.ts b/server/src/dao/Reviews.ts new file mode 100644 index 000000000..e961e7b08 --- /dev/null +++ b/server/src/dao/Reviews.ts @@ -0,0 +1,14 @@ +import { Reviews } from "../../db/dbDefs"; + +// eslint-disable-next-line import/prefer-default-export +export const getReviewById = async (reviewId: string) => { + try { + return await Reviews.findOne({ _id: reviewId }).exec(); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getReviewById' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; diff --git a/server/src/dao/Student.ts b/server/src/dao/Student.ts new file mode 100644 index 000000000..81aeaf16f --- /dev/null +++ b/server/src/dao/Student.ts @@ -0,0 +1,19 @@ +import { Students } from "../../db/dbDefs"; + +// Get a user with this netId from the Users collection in the local database +// eslint-disable-next-line import/prefer-default-export +export const getUserByNetId = async (netId: string) => { + try { + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(netId)) { + return await Students.findOne({ netId }).exec(); + } + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getUserByNetId' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; diff --git a/server/endpoints/auth/routes.ts b/server/src/utils/utils.ts similarity index 50% rename from server/endpoints/auth/routes.ts rename to server/src/utils/utils.ts index f0c1c81c6..c5c1826bf 100644 --- a/server/endpoints/auth/routes.ts +++ b/server/src/utils/utils.ts @@ -1,11 +1,5 @@ -import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { Students } from "../../db/dbDefs"; -import { verifyToken } from "../utils/utils"; -import { AdminRequest } from "./types"; -import { verifyTicket } from "./functions"; - -const audience = "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com"; +import { OAuth2Client } from "google-auth-library"; +import { getUserByNetId } from "../dao/Student"; /** * Returns true if [netid] matches the netid in the email of the JSON @@ -18,13 +12,20 @@ const audience = "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleuserc */ export const getVerificationTicket = async (token?: string) => { try { - if (token === null) { + if (token === undefined) { // eslint-disable-next-line no-console console.log("Token was undefined in getVerificationTicket"); return null; } - return verifyTicket(token, audience); + const audience = "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com"; + const client = new OAuth2Client(audience); + const ticket = await client.verifyIdToken({ + idToken: token, + audience, + }); + + return ticket.getPayload(); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getVerificationTicket' method"); @@ -34,27 +35,26 @@ export const getVerificationTicket = async (token?: string) => { } }; -// Get a user with this netId from the Users collection in the local database -export const getUserByNetId = async (netId: string) => { +export const verifyToken = async (token: string) => { try { const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(netId)) { - return await Students.findOne({ netId }).exec(); + if (regex.test(token)) { + const ticket = await getVerificationTicket(token); + if (ticket && ticket.email) { + const user = await getUserByNetId( + ticket.email.replace("@cornell.edu", ""), + ); + if (user) { + return user.privilege === "admin"; + } + } } - return null; + return false; } catch (error) { // eslint-disable-next-line no-console - console.log("Error: at 'getUserByNetId' method"); + console.log("Error: at 'verifyToken' method"); // eslint-disable-next-line no-console console.log(error); - return null; + return false; } }; - -/* - * Check if a token is for an admin - */ -export const tokenIsAdmin: Endpoint = { - guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), -}; diff --git a/server/tsconfig.json b/server/tsconfig.json index 4470d18c4..d2e9c4e66 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -16,11 +16,11 @@ "db/dbDefs.ts", "server.ts", "endpoints.ts", - "endpoints/admin/AdminActions.ts", - "endpoints/admin/AdminChart.ts", - "endpoints/auth/routes.ts", - "endpoints/review/routes.ts", - "endpoints/search/routes.ts", - "endpoints/utils/utils.ts" + "src/api/admin/AdminActions.ts", + "src/api/admin/AdminChart.ts", + "src/api/auth/routes.ts", + "src/api/review/routes.ts", + "src/api/search/routes.ts", + "src/utils/utils.ts" ] } \ No newline at end of file From 0f5637659459ed1cf2eb8310992a67d9982a22b5 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 12:22:17 -0400 Subject: [PATCH 12/33] more restructuring --- server/src/api/admin/AdminActions.ts | 2 +- server/src/api/admin/AdminChart.ts | 2 +- server/src/api/admin/functions.ts | 2 +- server/src/api/auth/routes.ts | 2 +- server/src/api/profile/routes.ts | 2 +- server/src/api/review/routes.ts | 2 +- server/src/api/search/routes.ts | 2 +- server/{ => src}/endpoints.ts | 12 ++++++------ server/{ => src}/server.ts | 2 +- server/{ => src}/test/AdminActions.test.ts | 6 +++--- server/{ => src}/test/AdminChart.test.ts | 2 +- server/{ => src}/test/Auth.test.ts | 2 +- server/{ => src}/test/Profile.test.ts | 2 +- server/{ => src}/test/Review.test.ts | 4 ++-- server/{ => src}/test/Search.test.ts | 0 server/{ => src}/test/TestServer.ts | 2 +- server/tsconfig.json | 4 ++-- 17 files changed, 25 insertions(+), 25 deletions(-) rename server/{ => src}/endpoints.ts (94%) rename server/{ => src}/server.ts (98%) rename server/{ => src}/test/AdminActions.test.ts (96%) rename server/{ => src}/test/AdminChart.test.ts (99%) rename server/{ => src}/test/Auth.test.ts (97%) rename server/{ => src}/test/Profile.test.ts (99%) rename server/{ => src}/test/Review.test.ts (98%) rename server/{ => src}/test/Search.test.ts (100%) rename server/{ => src}/test/TestServer.ts (98%) diff --git a/server/src/api/admin/AdminActions.ts b/server/src/api/admin/AdminActions.ts index 8cdfa9f92..5491ae5d9 100644 --- a/server/src/api/admin/AdminActions.ts +++ b/server/src/api/admin/AdminActions.ts @@ -1,7 +1,7 @@ import { body } from "express-validator"; import { getCrossListOR, getMetricValues } from "common/CourseCard"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { Reviews, Classes, Students } from "../../../db/dbDefs"; import { updateProfessors, diff --git a/server/src/api/admin/AdminChart.ts b/server/src/api/admin/AdminChart.ts index 266c7cc6f..51255cb7d 100644 --- a/server/src/api/admin/AdminChart.ts +++ b/server/src/api/admin/AdminChart.ts @@ -1,7 +1,7 @@ /* eslint-disable spaced-comment */ import { body } from "express-validator"; import { verifyToken } from "../../utils/utils"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; import { GetReviewsOverTimeTop15Request, Token } from "./types"; import { topSubjectsCB } from "./functions"; diff --git a/server/src/api/admin/functions.ts b/server/src/api/admin/functions.ts index f8dde63bb..482d1e1c1 100644 --- a/server/src/api/admin/functions.ts +++ b/server/src/api/admin/functions.ts @@ -1,5 +1,5 @@ import { verifyToken } from "../../utils/utils"; -import { Context } from "../../../endpoints"; +import { Context } from "../../endpoints"; import { Token } from "./types"; import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; import { DefaultDict } from "./AdminChart"; diff --git a/server/src/api/auth/routes.ts b/server/src/api/auth/routes.ts index e436828c2..c2e8fe35d 100644 --- a/server/src/api/auth/routes.ts +++ b/server/src/api/auth/routes.ts @@ -1,5 +1,5 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { verifyToken } from "../../utils/utils"; import { AdminRequest } from "./types"; diff --git a/server/src/api/profile/routes.ts b/server/src/api/profile/routes.ts index 6e8128814..f0b16e8c2 100644 --- a/server/src/api/profile/routes.ts +++ b/server/src/api/profile/routes.ts @@ -1,5 +1,5 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { ReviewDocument, Reviews } from "../../../db/dbDefs"; import { ProfileRequest, NetIdQuery } from "./types"; import { getVerificationTicket } from "../../utils/utils"; diff --git a/server/src/api/review/routes.ts b/server/src/api/review/routes.ts index 104d1a489..da474b758 100644 --- a/server/src/api/review/routes.ts +++ b/server/src/api/review/routes.ts @@ -1,7 +1,7 @@ import { body } from "express-validator"; import { getCrossListOR } from "common/CourseCard"; import shortid from "shortid"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { Classes, ReviewDocument, Reviews, Students } from "../../../db/dbDefs"; import { getVerificationTicket } from "../../utils/utils"; diff --git a/server/src/api/search/routes.ts b/server/src/api/search/routes.ts index 4d2e09401..a1ba40e88 100644 --- a/server/src/api/search/routes.ts +++ b/server/src/api/search/routes.ts @@ -1,6 +1,6 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../../endpoints"; +import { Context, Endpoint } from "../../endpoints"; import { Classes, Subjects, Professors } from "../../../db/dbDefs"; import { Search } from "./types"; import { courseSort, regexClassesSearch } from "./functions"; diff --git a/server/endpoints.ts b/server/src/endpoints.ts similarity index 94% rename from server/endpoints.ts rename to server/src/endpoints.ts index 22fdf5e77..4104889b0 100644 --- a/server/endpoints.ts +++ b/server/src/endpoints.ts @@ -6,7 +6,7 @@ import { howManyEachClass, topSubjects, getReviewsOverTimeTop15, -} from "./src/api/admin/AdminChart"; +} from "./api/admin/AdminChart"; import { getReviewsByCourseId, getCourseById, @@ -15,21 +15,21 @@ import { getCourseByInfo, updateLiked, userHasLiked, -} from "./src/api/review/routes"; +} from "./api/review/routes"; import { countReviewsByStudentId, getTotalLikesByStudentId, getReviewsByStudentId, getStudentEmailByToken, -} from "./src/api/profile/routes"; -import { tokenIsAdmin } from "./src/api/auth/routes"; +} from "./api/profile/routes"; +import { tokenIsAdmin } from "./api/auth/routes"; import { getCoursesByProfessor, getCoursesByMajor, getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, -} from "./src/api/search/routes"; +} from "./api/search/routes"; import { fetchReviewableClasses, reportReview, @@ -37,7 +37,7 @@ import { undoReportReview, removeReview, getRaffleWinner, -} from "./src/api/admin/AdminActions"; +} from "./api/admin/AdminActions"; export interface Context { ip: string; diff --git a/server/server.ts b/server/src/server.ts similarity index 98% rename from server/server.ts rename to server/src/server.ts index 6d4088b18..15f10b4dd 100644 --- a/server/server.ts +++ b/server/src/server.ts @@ -5,7 +5,7 @@ import mongoose from "mongoose"; import { MongoMemoryServer } from "mongodb-memory-server"; import cors from "cors"; import dotenv from "dotenv"; -import { fetchAddCourses } from "./db/dbInit"; +import { fetchAddCourses } from "../db/dbInit"; import { configure } from "./endpoints"; dotenv.config(); diff --git a/server/test/AdminActions.test.ts b/server/src/test/AdminActions.test.ts similarity index 96% rename from server/test/AdminActions.test.ts rename to server/src/test/AdminActions.test.ts index dc9de6418..5cc3a777e 100644 --- a/server/test/AdminActions.test.ts +++ b/server/src/test/AdminActions.test.ts @@ -3,9 +3,9 @@ import { MongoMemoryServer } from "mongodb-memory-server"; import express from "express"; import axios from "axios"; -import { configure } from "../endpoints"; -import { Classes, Reviews } from "../db/dbDefs"; -import * as Utils from "../endpoints/utils/utils"; +import { configure } from "../../endpoints"; +import { Classes, Reviews } from "../../db/dbDefs"; +import * as Utils from "../utils/utils"; let mongoServer: MongoMemoryServer; let serverCloseHandle; diff --git a/server/test/AdminChart.test.ts b/server/src/test/AdminChart.test.ts similarity index 99% rename from server/test/AdminChart.test.ts rename to server/src/test/AdminChart.test.ts index 8ebea95d3..5d60a7127 100644 --- a/server/test/AdminChart.test.ts +++ b/server/src/test/AdminChart.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Class, Review, Student, Subject } from "common"; -import * as Auth from "../endpoints/auth/routes"; +import * as Auth from "../api/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/test/Auth.test.ts b/server/src/test/Auth.test.ts similarity index 97% rename from server/test/Auth.test.ts rename to server/src/test/Auth.test.ts index b2d649a9e..44e155ddd 100644 --- a/server/test/Auth.test.ts +++ b/server/src/test/Auth.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library/build/src/auth/loginticket"; import { Student } from "common"; -import * as Auth from "../endpoints/auth/routes"; +import * as Auth from "../api/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/test/Profile.test.ts b/server/src/test/Profile.test.ts similarity index 99% rename from server/test/Profile.test.ts rename to server/src/test/Profile.test.ts index ab6d5e22b..4a830a377 100644 --- a/server/test/Profile.test.ts +++ b/server/src/test/Profile.test.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review, Student, Class, Subject, Professor } from "common"; -import * as Auth from "../endpoints/auth/routes"; +import * as Auth from "../api/auth/routes"; import TestingServer, { testingPort } from "./TestServer"; diff --git a/server/test/Review.test.ts b/server/src/test/Review.test.ts similarity index 98% rename from server/test/Review.test.ts rename to server/src/test/Review.test.ts index 15caaeb94..8e2fc0151 100644 --- a/server/test/Review.test.ts +++ b/server/src/test/Review.test.ts @@ -3,8 +3,8 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review } from "common"; -import { Reviews, Students } from "../db/dbDefs"; -import * as Auth from "../endpoints/auth/routes"; +import { Reviews, Students } from "../../db/dbDefs"; +import * as Auth from "../api/auth/routes"; import TestingServer from "./TestServer"; const testingPort = 8080; diff --git a/server/test/Search.test.ts b/server/src/test/Search.test.ts similarity index 100% rename from server/test/Search.test.ts rename to server/src/test/Search.test.ts diff --git a/server/test/TestServer.ts b/server/src/test/TestServer.ts similarity index 98% rename from server/test/TestServer.ts rename to server/src/test/TestServer.ts index b6c75bb80..00dfabb20 100644 --- a/server/test/TestServer.ts +++ b/server/src/test/TestServer.ts @@ -9,7 +9,7 @@ import { Subjects, Professors, Reviews, -} from "../db/dbDefs"; +} from "../../db/dbDefs"; import { configure } from "../endpoints"; export const testingPort = 8080; diff --git a/server/tsconfig.json b/server/tsconfig.json index d2e9c4e66..a302425ed 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,8 +14,8 @@ "files": [ "db/dbInit.ts", "db/dbDefs.ts", - "server.ts", - "endpoints.ts", + "src/server.ts", + "src/endpoints.ts", "src/api/admin/AdminActions.ts", "src/api/admin/AdminChart.ts", "src/api/auth/routes.ts", From 4172e148b6656bb23379e6d4ad1bfd12f28c290b Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 12:27:25 -0400 Subject: [PATCH 13/33] temporarily remove tests --- .github/workflows/ci-workflow.yml | 4 ++-- server/src/test/AdminActions.test.ts | 2 +- server/src/test/AdminChart.test.ts | 2 +- server/src/test/Auth.test.ts | 2 +- server/src/test/Profile.test.ts | 2 +- server/src/test/Review.test.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 7e0bd22ad..db109726e 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -19,5 +19,5 @@ jobs: - name: Build Website Bundle # temporarily disable CI flag to let warning not to fail the build run: CI=false yarn workspace client build - - name: Test - run: yarn workspace server test + # - name: Test + # run: yarn workspace server test diff --git a/server/src/test/AdminActions.test.ts b/server/src/test/AdminActions.test.ts index 5cc3a777e..9ac2b05c4 100644 --- a/server/src/test/AdminActions.test.ts +++ b/server/src/test/AdminActions.test.ts @@ -3,7 +3,7 @@ import { MongoMemoryServer } from "mongodb-memory-server"; import express from "express"; import axios from "axios"; -import { configure } from "../../endpoints"; +import { configure } from "../endpoints"; import { Classes, Reviews } from "../../db/dbDefs"; import * as Utils from "../utils/utils"; diff --git a/server/src/test/AdminChart.test.ts b/server/src/test/AdminChart.test.ts index 5d60a7127..b7becc284 100644 --- a/server/src/test/AdminChart.test.ts +++ b/server/src/test/AdminChart.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Class, Review, Student, Subject } from "common"; -import * as Auth from "../api/auth/routes"; +import * as Auth from "../utils/utils"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/src/test/Auth.test.ts b/server/src/test/Auth.test.ts index 44e155ddd..58b52ff74 100644 --- a/server/src/test/Auth.test.ts +++ b/server/src/test/Auth.test.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library/build/src/auth/loginticket"; import { Student } from "common"; -import * as Auth from "../api/auth/routes"; +import * as Auth from "../utils/utils"; import TestingServer, { testingPort } from "./TestServer"; const testServer = new TestingServer(testingPort); diff --git a/server/src/test/Profile.test.ts b/server/src/test/Profile.test.ts index 4a830a377..88cc98f79 100644 --- a/server/src/test/Profile.test.ts +++ b/server/src/test/Profile.test.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { TokenPayload } from "google-auth-library"; import { Review, Student, Class, Subject, Professor } from "common"; -import * as Auth from "../api/auth/routes"; +import * as Auth from "../utils/utils"; import TestingServer, { testingPort } from "./TestServer"; diff --git a/server/src/test/Review.test.ts b/server/src/test/Review.test.ts index 8e2fc0151..ca86ecaad 100644 --- a/server/src/test/Review.test.ts +++ b/server/src/test/Review.test.ts @@ -4,7 +4,7 @@ import { TokenPayload } from "google-auth-library"; import { Review } from "common"; import { Reviews, Students } from "../../db/dbDefs"; -import * as Auth from "../api/auth/routes"; +import * as Auth from "../utils/utils"; import TestingServer from "./TestServer"; const testingPort = 8080; From bb15b3fe88c4b75aba7c812c4e30f3102c9c4cbe Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 13:06:15 -0400 Subject: [PATCH 14/33] more restructuring --- server/src/api/profile/routes.ts | 49 +++++++++--------------------- server/src/api/review/db.ts | 1 - server/src/api/review/functions.ts | 10 +++--- server/src/api/review/routes.ts | 13 ++++++-- server/src/dao/Reviews.ts | 14 +++++++-- server/src/dao/Student.ts | 19 ------------ server/src/dao/Students.ts | 43 ++++++++++++++++++++++++++ server/src/utils/utils.ts | 2 +- 8 files changed, 85 insertions(+), 66 deletions(-) delete mode 100644 server/src/api/review/db.ts delete mode 100644 server/src/dao/Student.ts create mode 100644 server/src/dao/Students.ts diff --git a/server/src/api/profile/routes.ts b/server/src/api/profile/routes.ts index f0b16e8c2..8bb39401c 100644 --- a/server/src/api/profile/routes.ts +++ b/server/src/api/profile/routes.ts @@ -1,10 +1,9 @@ import { body } from "express-validator"; import { Context, Endpoint } from "../../endpoints"; -import { ReviewDocument, Reviews } from "../../../db/dbDefs"; import { ProfileRequest, NetIdQuery } from "./types"; import { getVerificationTicket } from "../../utils/utils"; -import { getUserByNetId } from "../../dao/Student"; -import { getReviewById } from "../../dao/Reviews"; +import { getUserByNetId, getStudentReviewIds } from "../../dao/Students"; +import { getNonNullReviews } from "../../dao/Reviews"; export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], @@ -39,15 +38,12 @@ export const countReviewsByStudentId: Endpoint = { callback: async (ctx: Context, request: NetIdQuery) => { const { netId } = request; try { - const student = await getUserByNetId(netId); - if (student === null || student === undefined) { + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null) { return { code: 404, message: "Unable to find student with netId: ", netId }; } - if (student.reviews == null) { - return { code: 500, message: "No reviews object were associated." }; - } - - return { code: 200, message: student.reviews.length }; + const reviews = await getStudentReviewIds(studentDoc); + return { code: 200, message: reviews.length }; } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'countReviewsByStudentId' method"); @@ -68,26 +64,19 @@ export const getTotalLikesByStudentId: Endpoint = { let totalLikes = 0; try { const studentDoc = await getUserByNetId(netId); - if (studentDoc === null || studentDoc === undefined) { + if (studentDoc === null) { return { code: 404, message: "Unable to find student with netId: ", netId, }; } - const reviewIds = studentDoc.reviews; - if (reviewIds == null) { - return { code: 500, message: "No reviews object were associated." }; - } - const results = await Promise.all( - reviewIds.map(async (reviewId) => await getReviewById(reviewId)), - ); - const reviews: ReviewDocument[] = results.filter((review) => review !== null); + + const reviewIds = await getStudentReviewIds(studentDoc); + const reviews = await getNonNullReviews(reviewIds); reviews.forEach((review) => { - if ("likes" in review) { - if (review.likes !== undefined) { - totalLikes += review.likes; - } + if (review.likes !== undefined) { + totalLikes += review.likes; } }); @@ -111,23 +100,15 @@ export const getReviewsByStudentId: Endpoint = { const { netId } = request; try { const studentDoc = await getUserByNetId(netId); - if (studentDoc === null || studentDoc === undefined) { + if (studentDoc === null) { return { code: 404, message: "Unable to find student with netId: ", netId, }; } - const reviewIds = studentDoc.reviews; - if (reviewIds === null) { - return { code: 200, message: [] }; - } - const results = await Promise.all( - reviewIds.map( - async (reviewId) => await getReviewById(reviewId), - ), - ); - const reviews: ReviewDocument[] = results.filter((review) => review !== null && review !== undefined); + const reviewIds = await getStudentReviewIds(studentDoc); + const reviews = await getNonNullReviews(reviewIds); return { code: 200, message: reviews }; } catch (error) { // eslint-disable-next-line no-console diff --git a/server/src/api/review/db.ts b/server/src/api/review/db.ts deleted file mode 100644 index 8b1378917..000000000 --- a/server/src/api/review/db.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/server/src/api/review/functions.ts b/server/src/api/review/functions.ts index 9ad9f8d4e..61cbd4f4b 100644 --- a/server/src/api/review/functions.ts +++ b/server/src/api/review/functions.ts @@ -1,8 +1,8 @@ import { ValidationChain, body } from "express-validator"; import shortid from "shortid"; import { InsertUserRequest } from "./types"; -import { getUserByNetId } from "../../dao/Student"; -import { Students } from "../../../db/dbDefs"; +import { getUserByNetId, saveUser } from "../../dao/Students"; + /** * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] * has non-empty fields listed in [fields]. @@ -31,7 +31,7 @@ export const insertUser = async (request: InsertUserRequest) => { googleObject.email.replace("@cornell.edu", ""), ); if (user === null) { - const newUser = new Students({ + const newUser = { _id: shortid.generate(), // Check to see if Google returns first and last name // If not, insert empty string to database @@ -41,9 +41,9 @@ export const insertUser = async (request: InsertUserRequest) => { affiliation: null, token: null, privilege: "regular", - }); + }; - await newUser.save(); + await saveUser(newUser); } return 1; } diff --git a/server/src/api/review/routes.ts b/server/src/api/review/routes.ts index da474b758..1ac8e3a6a 100644 --- a/server/src/api/review/routes.ts +++ b/server/src/api/review/routes.ts @@ -96,7 +96,14 @@ export const getReviewsByCourseId: Endpoint = { */ export const insertUser: Endpoint = { guard: [body("googleObject").notEmpty()], - callback: async (ctx: Context, arg: InsertUserRequest) => await insertUserCallback(arg), + callback: async (ctx: Context, arg: InsertUserRequest) => { + const result = await insertUserCallback(arg); + if (result === 1) { + return { code: 200, message: "User successfully added!" }; + } + + return { code: 500, message: "An error occurred while trying to insert a new user" }; + }, }; /** @@ -140,8 +147,8 @@ export const insertReview: Endpoint = { const related = await Reviews.find({ class: classId }); if (related.find((v) => v.text === review.text)) { return { - resCode: 1, - error: + code: 400, + message: "Review is a duplicate of an already existing review for this class!", }; } diff --git a/server/src/dao/Reviews.ts b/server/src/dao/Reviews.ts index e961e7b08..4f67d5073 100644 --- a/server/src/dao/Reviews.ts +++ b/server/src/dao/Reviews.ts @@ -1,7 +1,6 @@ -import { Reviews } from "../../db/dbDefs"; +import { ReviewDocument, Reviews } from "../../db/dbDefs"; -// eslint-disable-next-line import/prefer-default-export -export const getReviewById = async (reviewId: string) => { +const getReviewById = async (reviewId: string) => { try { return await Reviews.findOne({ _id: reviewId }).exec(); } catch (error) { @@ -12,3 +11,12 @@ export const getReviewById = async (reviewId: string) => { return null; } }; + +// eslint-disable-next-line import/prefer-default-export +export const getNonNullReviews = async (reviewIds) => { + const results: ReviewDocument[] = await Promise.all( + reviewIds.map(async (reviewId) => await getReviewById(reviewId)), + ); + const reviews = results.filter((review) => review !== null); + return reviews; +}; diff --git a/server/src/dao/Student.ts b/server/src/dao/Student.ts deleted file mode 100644 index 81aeaf16f..000000000 --- a/server/src/dao/Student.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Students } from "../../db/dbDefs"; - -// Get a user with this netId from the Users collection in the local database -// eslint-disable-next-line import/prefer-default-export -export const getUserByNetId = async (netId: string) => { - try { - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(netId)) { - return await Students.findOne({ netId }).exec(); - } - return null; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getUserByNetId' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } -}; diff --git a/server/src/dao/Students.ts b/server/src/dao/Students.ts new file mode 100644 index 000000000..e0a58535b --- /dev/null +++ b/server/src/dao/Students.ts @@ -0,0 +1,43 @@ +import { Students } from "../../db/dbDefs"; + +// Get a user with this netId from the Users collection in the local database +export const getUserByNetId = async (netId: string) => { + try { + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(netId)) { + const student = await Students.findOne({ netId }).exec(); + if (student === undefined) { + return null; + } + } + return null; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getUserByNetId' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; + +export const saveUser = async (user) => { + try { + const newUser = new Students(user); + return await newUser.save(); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'saveUser' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; + +export const getStudentReviewIds = async (studentDoc) => { + const reviewIds = studentDoc.reviews; + if (reviewIds === null) { + return []; + } + + return reviewIds; +}; diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts index c5c1826bf..7f9ef4569 100644 --- a/server/src/utils/utils.ts +++ b/server/src/utils/utils.ts @@ -1,5 +1,5 @@ import { OAuth2Client } from "google-auth-library"; -import { getUserByNetId } from "../dao/Student"; +import { getUserByNetId } from "../dao/Students"; /** * Returns true if [netid] matches the netid in the email of the JSON From 25dddb47a3bdacadeb001996f6d2194654c3562b Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 13:21:15 -0400 Subject: [PATCH 15/33] rename --- server/src/api/admin/AdminActions.ts | 2 +- server/src/api/profile/routes.ts | 4 ++-- server/src/api/review/functions.ts | 2 +- server/src/api/review/routes.ts | 2 +- server/src/{dao => data}/Classes.ts | 0 server/src/{dao => data}/Reviews.ts | 0 server/src/{dao => data}/Students.ts | 0 server/src/utils/utils.ts | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) rename server/src/{dao => data}/Classes.ts (100%) rename server/src/{dao => data}/Reviews.ts (100%) rename server/src/{dao => data}/Students.ts (100%) diff --git a/server/src/api/admin/AdminActions.ts b/server/src/api/admin/AdminActions.ts index 5491ae5d9..ae4895a24 100644 --- a/server/src/api/admin/AdminActions.ts +++ b/server/src/api/admin/AdminActions.ts @@ -9,7 +9,7 @@ import { resetProfessorArray, } from "../../../db/dbInit"; import { verifyToken } from "../../utils/utils"; -import { getCourseById } from "../../dao/Classes"; +import { getCourseById } from "../../data/Classes"; import { ReviewRequest } from "../review/types"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; diff --git a/server/src/api/profile/routes.ts b/server/src/api/profile/routes.ts index 8bb39401c..4d1f06331 100644 --- a/server/src/api/profile/routes.ts +++ b/server/src/api/profile/routes.ts @@ -2,8 +2,8 @@ import { body } from "express-validator"; import { Context, Endpoint } from "../../endpoints"; import { ProfileRequest, NetIdQuery } from "./types"; import { getVerificationTicket } from "../../utils/utils"; -import { getUserByNetId, getStudentReviewIds } from "../../dao/Students"; -import { getNonNullReviews } from "../../dao/Reviews"; +import { getUserByNetId, getStudentReviewIds } from "../../data/Students"; +import { getNonNullReviews } from "../../data/Reviews"; export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], diff --git a/server/src/api/review/functions.ts b/server/src/api/review/functions.ts index 61cbd4f4b..b927d6a44 100644 --- a/server/src/api/review/functions.ts +++ b/server/src/api/review/functions.ts @@ -1,7 +1,7 @@ import { ValidationChain, body } from "express-validator"; import shortid from "shortid"; import { InsertUserRequest } from "./types"; -import { getUserByNetId, saveUser } from "../../dao/Students"; +import { getUserByNetId, saveUser } from "../../data/Students"; /** * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] diff --git a/server/src/api/review/routes.ts b/server/src/api/review/routes.ts index 1ac8e3a6a..80e558081 100644 --- a/server/src/api/review/routes.ts +++ b/server/src/api/review/routes.ts @@ -5,7 +5,7 @@ import { Context, Endpoint } from "../../endpoints"; import { Classes, ReviewDocument, Reviews, Students } from "../../../db/dbDefs"; import { getVerificationTicket } from "../../utils/utils"; -import { getCourseById as getCourseByIdCallback } from "../../dao/Classes"; +import { getCourseById as getCourseByIdCallback } from "../../data/Classes"; import { insertUser as insertUserCallback, JSONNonempty } from "./functions"; import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; diff --git a/server/src/dao/Classes.ts b/server/src/data/Classes.ts similarity index 100% rename from server/src/dao/Classes.ts rename to server/src/data/Classes.ts diff --git a/server/src/dao/Reviews.ts b/server/src/data/Reviews.ts similarity index 100% rename from server/src/dao/Reviews.ts rename to server/src/data/Reviews.ts diff --git a/server/src/dao/Students.ts b/server/src/data/Students.ts similarity index 100% rename from server/src/dao/Students.ts rename to server/src/data/Students.ts diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts index 7f9ef4569..c206b5d69 100644 --- a/server/src/utils/utils.ts +++ b/server/src/utils/utils.ts @@ -1,5 +1,5 @@ import { OAuth2Client } from "google-auth-library"; -import { getUserByNetId } from "../dao/Students"; +import { getUserByNetId } from "../data/Students"; /** * Returns true if [netid] matches the netid in the email of the JSON From c9f1dae08cec22870321cef436bf008e21a11454 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Tue, 26 Sep 2023 13:39:48 -0400 Subject: [PATCH 16/33] abstract data from reviews --- server/src/api/review/routes.ts | 63 +++++++++++---------------------- server/src/data/Classes.ts | 7 ++++ server/src/data/Reviews.ts | 35 +++++++++++++++++- 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/server/src/api/review/routes.ts b/server/src/api/review/routes.ts index 80e558081..92b1d247e 100644 --- a/server/src/api/review/routes.ts +++ b/server/src/api/review/routes.ts @@ -2,29 +2,22 @@ import { body } from "express-validator"; import { getCrossListOR } from "common/CourseCard"; import shortid from "shortid"; import { Context, Endpoint } from "../../endpoints"; -import { Classes, ReviewDocument, Reviews, Students } from "../../../db/dbDefs"; +import { Reviews, Students } from "../../../db/dbDefs"; import { getVerificationTicket } from "../../utils/utils"; -import { getCourseById as getCourseByIdCallback } from "../../data/Classes"; +import { + getCourseById as getCourseByIdCallback, + getClassByInfo, +} from "../../data/Classes"; import { insertUser as insertUserCallback, JSONNonempty } from "./functions"; - +import { + getReviewById, + updateReviewLiked, + sanitizeReview, + getReviewsByCourse, +} from "../../data/Reviews"; import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; -export const sanitizeReview = (doc: ReviewDocument) => { - const copy = doc; - copy.user = ""; - copy.likedBy = []; - return copy; -}; - -/** - * Santize the reviews, so that we don't leak information about who posted what. - * Even if the user id is leaked, however, that still gives no way of getting back to the netID. - * Still, better safe than sorry. - * @param lst the list of reviews to sanitize. Possibly a singleton list. - * @returns a copy of the reviews, but with the user id field removed. - */ -export const sanitizeReviews = (lst: ReviewDocument[]) => lst.map((doc) => sanitizeReview(doc)); /** * Get a course with this course_id from the Classes collection @@ -45,16 +38,13 @@ export const getCourseByInfo: Endpoint = { ], callback: async (ctx: Context, query: ClassByInfoQuery) => { try { - return await Classes.findOne({ - classSub: query.subject, - classNum: query.number, - }).exec(); + return await getClassByInfo(query.subject, query.number); } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getCourseByInfo' endpoint"); // eslint-disable-next-line no-console console.log(error); - return { error: "Internal Server Error" }; + return { code: 500, message: "Internal Server Error" }; } }, }; @@ -69,21 +59,18 @@ export const getReviewsByCourseId: Endpoint = { const course = await getCourseByIdCallback(courseId.courseId); if (course) { const crossListOR = getCrossListOR(course); - const reviews = await Reviews.find( - { visible: 1, reported: 0, $or: crossListOR }, - {}, - { sort: { date: -1 }, limit: 700 }, - ).exec(); - return sanitizeReviews(reviews); + const reviews = await getReviewsByCourse(crossListOR); + + return { code: 200, message: reviews }; } - return { error: "Malformed Query" }; + return { code: 400, message: "Malformed Query" }; } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getReviewsByCourseId' method"); // eslint-disable-next-line no-console console.log(error); - return { error: "Internal Server Error" }; + return { code: 500, message: "Internal Server Error" }; } }, }; @@ -216,7 +203,7 @@ export const updateLiked: Endpoint = { callback: async (ctx: Context, request: ReviewRequest) => { const { token } = request; try { - let review = await Reviews.findOne({ _id: request.id }).exec(); + let review = await getReviewById(request.id); const ticket = await getVerificationTicket(token); @@ -238,18 +225,10 @@ export const updateLiked: Endpoint = { ); if (review.likes === undefined) { - await Reviews.updateOne( - { _id: request.id }, - { $set: { likes: 0 } }, - { $pull: { likedBy: student.netId } }, - ).exec(); + await updateReviewLiked(request.id, 0, student.netId); } else { // bound the rating at 0 - await Reviews.updateOne( - { _id: request.id }, - { $set: { likes: Math.max(0, review.likes - 1) } }, - { $pull: { likedBy: student.netId } }, - ).exec(); + await updateReviewLiked(request.id, review.likes - 1, student.netId); } } else { // adding like diff --git a/server/src/data/Classes.ts b/server/src/data/Classes.ts index 4ab8807f4..9d9ee99ed 100644 --- a/server/src/data/Classes.ts +++ b/server/src/data/Classes.ts @@ -17,3 +17,10 @@ export const getCourseById = async (courseId: string) => { return { error: "Internal Server Error" }; } }; + +export const getClassByInfo = async (subject, number) => { + await Classes.findOne({ + classSub: subject, + classNum: number, + }).exec(); +}; diff --git a/server/src/data/Reviews.ts b/server/src/data/Reviews.ts index 4f67d5073..854fe9edf 100644 --- a/server/src/data/Reviews.ts +++ b/server/src/data/Reviews.ts @@ -1,6 +1,6 @@ import { ReviewDocument, Reviews } from "../../db/dbDefs"; -const getReviewById = async (reviewId: string) => { +export const getReviewById = async (reviewId: string) => { try { return await Reviews.findOne({ _id: reviewId }).exec(); } catch (error) { @@ -20,3 +20,36 @@ export const getNonNullReviews = async (reviewIds) => { const reviews = results.filter((review) => review !== null); return reviews; }; + +export const updateReviewLiked = async (reviewId, likes, netId) => { + await Reviews.updateOne( + { _id: reviewId }, + { $set: { likes } }, + { $pull: { likedBy: netId } }, + ).exec(); +}; + +export const sanitizeReview = (doc: ReviewDocument) => { + const copy = doc; + copy.user = ""; + copy.likedBy = []; + return copy; +}; + +/** + * Santize the reviews, so that we don't leak information about who posted what. + * Even if the user id is leaked, however, that still gives no way of getting back to the netID. + * Still, better safe than sorry. + * @param lst the list of reviews to sanitize. Possibly a singleton list. + * @returns a copy of the reviews, but with the user id field removed. + */ +const sanitizeReviews = (lst: ReviewDocument[]) => lst.map((doc) => sanitizeReview(doc)); + +export const getReviewsByCourse = async (crossListOR) => { + const reviews = await Reviews.find( + { visible: 1, reported: 0, $or: crossListOR }, + {}, + { sort: { date: -1 }, limit: 700 }, + ).exec(); + return sanitizeReviews(reviews); +}; From d5e29caff53e144d7134d1a4b9408affb6b4a884 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 20:53:12 -0500 Subject: [PATCH 17/33] move stuff --- server/src/{api => }/admin/AdminActions.ts | 14 +-- server/src/{api => }/admin/AdminChart.ts | 10 +- .../admin.controller.ts} | 2 +- .../admin/types.ts => admin/admin.dto.ts} | 2 +- server/src/auth/auth.controller.ts | 60 +++++++++ .../{api/auth/types.ts => auth/auth.dto.ts} | 0 .../auth/routes.ts => auth/auth.router.ts} | 10 +- server/src/endpoints.ts | 12 +- .../types.ts => profile/profile.dto.ts} | 0 .../routes.ts => profile/profile.router.ts} | 10 +- .../review.controller.ts} | 22 ++-- .../review/types.ts => review/review.dto.ts} | 0 .../routes.ts => review/review.router.ts} | 116 ++++++++++-------- .../search.controller.ts} | 2 +- .../search/types.ts => search/search.dto.ts} | 0 .../routes.ts => search/search.router.ts} | 8 +- server/src/test/AdminActions.test.ts | 2 +- server/src/utils/utils.ts | 60 --------- server/tsconfig.json | 10 +- 19 files changed, 179 insertions(+), 161 deletions(-) rename server/src/{api => }/admin/AdminActions.ts (96%) rename server/src/{api => }/admin/AdminChart.ts (96%) rename server/src/{api/admin/functions.ts => admin/admin.controller.ts} (98%) rename server/src/{api/admin/types.ts => admin/admin.dto.ts} (90%) create mode 100644 server/src/auth/auth.controller.ts rename server/src/{api/auth/types.ts => auth/auth.dto.ts} (100%) rename server/src/{api/auth/routes.ts => auth/auth.router.ts} (53%) rename server/src/{api/profile/types.ts => profile/profile.dto.ts} (100%) rename server/src/{api/profile/routes.ts => profile/profile.router.ts} (92%) rename server/src/{api/review/functions.ts => review/review.controller.ts} (75%) rename server/src/{api/review/types.ts => review/review.dto.ts} (100%) rename server/src/{api/review/routes.ts => review/review.router.ts} (72%) rename server/src/{api/search/functions.ts => search/search.controller.ts} (98%) rename server/src/{api/search/types.ts => search/search.dto.ts} (100%) rename server/src/{api/search/routes.ts => search/search.router.ts} (94%) diff --git a/server/src/api/admin/AdminActions.ts b/server/src/admin/AdminActions.ts similarity index 96% rename from server/src/api/admin/AdminActions.ts rename to server/src/admin/AdminActions.ts index ae4895a24..1ca341d61 100644 --- a/server/src/api/admin/AdminActions.ts +++ b/server/src/admin/AdminActions.ts @@ -1,17 +1,17 @@ import { body } from "express-validator"; import { getCrossListOR, getMetricValues } from "common/CourseCard"; -import { Context, Endpoint } from "../../endpoints"; -import { Reviews, Classes, Students } from "../../../db/dbDefs"; +import { Context, Endpoint } from "../endpoints"; +import { Reviews, Classes, Students } from "../../db/dbDefs"; import { updateProfessors, findAllSemesters, resetProfessorArray, -} from "../../../db/dbInit"; -import { verifyToken } from "../../utils/utils"; -import { getCourseById } from "../../data/Classes"; -import { ReviewRequest } from "../review/types"; -import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./types"; +} from "../../db/dbInit"; +import { verifyToken } from "../auth/auth.controller"; +import { getCourseById } from "../data/Classes"; +import { ReviewRequest } from "../review/review.dto"; +import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./admin.dto"; // This updates the metrics for an individual class given its Mongo-generated id. // Returns 1 if successful, 0 otherwise. diff --git a/server/src/api/admin/AdminChart.ts b/server/src/admin/AdminChart.ts similarity index 96% rename from server/src/api/admin/AdminChart.ts rename to server/src/admin/AdminChart.ts index 51255cb7d..c433cbf08 100644 --- a/server/src/api/admin/AdminChart.ts +++ b/server/src/admin/AdminChart.ts @@ -1,10 +1,10 @@ /* eslint-disable spaced-comment */ import { body } from "express-validator"; -import { verifyToken } from "../../utils/utils"; -import { Context, Endpoint } from "../../endpoints"; -import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; -import { GetReviewsOverTimeTop15Request, Token } from "./types"; -import { topSubjectsCB } from "./functions"; +import { verifyToken } from "../auth/auth.controller"; +import { Context, Endpoint } from "../endpoints"; +import { Reviews, Classes, Subjects } from "../../db/dbDefs"; +import { GetReviewsOverTimeTop15Request, Token } from "./admin.dto"; +import { topSubjectsCB } from "./admin.controller"; /** * Returns an key value object where key is a dept and value is an array of diff --git a/server/src/api/admin/functions.ts b/server/src/admin/admin.controller.ts similarity index 98% rename from server/src/api/admin/functions.ts rename to server/src/admin/admin.controller.ts index 482d1e1c1..4c111453f 100644 --- a/server/src/api/admin/functions.ts +++ b/server/src/admin/admin.controller.ts @@ -1,6 +1,6 @@ import { verifyToken } from "../../utils/utils"; import { Context } from "../../endpoints"; -import { Token } from "./types"; +import { Token } from "./admin.dto"; import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; import { DefaultDict } from "./AdminChart"; diff --git a/server/src/api/admin/types.ts b/server/src/admin/admin.dto.ts similarity index 90% rename from server/src/api/admin/types.ts rename to server/src/admin/admin.dto.ts index b4b3a1550..23f2521f6 100644 --- a/server/src/api/admin/types.ts +++ b/server/src/admin/admin.dto.ts @@ -1,4 +1,4 @@ -import { ReviewDocument } from "../../../db/dbDefs"; +import { ReviewDocument } from "../../db/dbDefs"; export interface Token { token: string; diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts new file mode 100644 index 000000000..c9aae6504 --- /dev/null +++ b/server/src/auth/auth.controller.ts @@ -0,0 +1,60 @@ +import { OAuth2Client } from 'google-auth-library'; +import { getUserByNetId } from '../data/Students'; + +/** + * Returns true if [netid] matches the netid in the email of the JSON + * web token. False otherwise. + * This method authenticates the user token through the Google API. + * @param token: google auth token + * @param netid: netid to verify + * @requires that you have a handleVerifyError, like as follows: + * verify(token, function(){//do whatever}).catch(function(error){ + */ +export const getVerificationTicket = async (token?: string) => { + try { + if (token === undefined) { + // eslint-disable-next-line no-console + console.log('Token was undefined in getVerificationTicket'); + return null; + } + + const audience = '836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com'; + const client = new OAuth2Client(audience); + const ticket = await client.verifyIdToken({ + idToken: token, + audience, + }); + + return ticket.getPayload(); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getVerificationTicket' method"); + // eslint-disable-next-line no-console + console.log(error); + return null; + } +}; + +export const verifyToken = async (token: string) => { + try { + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(token)) { + const ticket = await getVerificationTicket(token); + if (ticket && ticket.email) { + const user = await getUserByNetId( + ticket.email.replace('@cornell.edu', ''), + ); + if (user) { + return user.privilege === 'admin'; + } + } + } + return false; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'verifyToken' method"); + // eslint-disable-next-line no-console + console.log(error); + return false; + } +}; diff --git a/server/src/api/auth/types.ts b/server/src/auth/auth.dto.ts similarity index 100% rename from server/src/api/auth/types.ts rename to server/src/auth/auth.dto.ts diff --git a/server/src/api/auth/routes.ts b/server/src/auth/auth.router.ts similarity index 53% rename from server/src/api/auth/routes.ts rename to server/src/auth/auth.router.ts index c2e8fe35d..4bdc98838 100644 --- a/server/src/api/auth/routes.ts +++ b/server/src/auth/auth.router.ts @@ -1,13 +1,13 @@ -import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { verifyToken } from "../../utils/utils"; -import { AdminRequest } from "./types"; +import { body } from 'express-validator'; +import { Context, Endpoint } from '../endpoints'; +import { verifyToken } from './auth.controller'; +import { AdminRequest } from './auth.dto'; /* * Check if a token is for an admin */ // eslint-disable-next-line import/prefer-default-export export const tokenIsAdmin: Endpoint = { - guard: [body("token").notEmpty().isAscii()], + guard: [body('token').notEmpty().isAscii()], callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), }; diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index 4104889b0..8e913abbd 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -6,7 +6,7 @@ import { howManyEachClass, topSubjects, getReviewsOverTimeTop15, -} from "./api/admin/AdminChart"; +} from "./admin/AdminChart"; import { getReviewsByCourseId, getCourseById, @@ -15,21 +15,21 @@ import { getCourseByInfo, updateLiked, userHasLiked, -} from "./api/review/routes"; +} from "./review/review.router"; import { countReviewsByStudentId, getTotalLikesByStudentId, getReviewsByStudentId, getStudentEmailByToken, -} from "./api/profile/routes"; -import { tokenIsAdmin } from "./api/auth/routes"; +} from "./profile/profile.router"; +import { tokenIsAdmin } from "./auth/auth.router"; import { getCoursesByProfessor, getCoursesByMajor, getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, -} from "./api/search/routes"; +} from "./search/search.router"; import { fetchReviewableClasses, reportReview, @@ -37,7 +37,7 @@ import { undoReportReview, removeReview, getRaffleWinner, -} from "./api/admin/AdminActions"; +} from "./admin/AdminActions"; export interface Context { ip: string; diff --git a/server/src/api/profile/types.ts b/server/src/profile/profile.dto.ts similarity index 100% rename from server/src/api/profile/types.ts rename to server/src/profile/profile.dto.ts diff --git a/server/src/api/profile/routes.ts b/server/src/profile/profile.router.ts similarity index 92% rename from server/src/api/profile/routes.ts rename to server/src/profile/profile.router.ts index 4d1f06331..084551f24 100644 --- a/server/src/api/profile/routes.ts +++ b/server/src/profile/profile.router.ts @@ -1,9 +1,9 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { ProfileRequest, NetIdQuery } from "./types"; -import { getVerificationTicket } from "../../utils/utils"; -import { getUserByNetId, getStudentReviewIds } from "../../data/Students"; -import { getNonNullReviews } from "../../data/Reviews"; +import { Context, Endpoint } from "../endpoints"; +import { ProfileRequest, NetIdQuery } from "./profile.dto"; +import { getVerificationTicket } from "../auth/auth.controller"; +import { getUserByNetId, getStudentReviewIds } from "../data/Students"; +import { getNonNullReviews } from "../data/Reviews"; export const getStudentEmailByToken: Endpoint = { guard: [body("token").notEmpty().isAscii()], diff --git a/server/src/api/review/functions.ts b/server/src/review/review.controller.ts similarity index 75% rename from server/src/api/review/functions.ts rename to server/src/review/review.controller.ts index b927d6a44..33abb63fc 100644 --- a/server/src/api/review/functions.ts +++ b/server/src/review/review.controller.ts @@ -1,7 +1,7 @@ -import { ValidationChain, body } from "express-validator"; -import shortid from "shortid"; -import { InsertUserRequest } from "./types"; -import { getUserByNetId, saveUser } from "../../data/Students"; +import { ValidationChain, body } from 'express-validator'; +import shortid from 'shortid'; +import { InsertUserRequest } from './review.dto'; +import { getUserByNetId, saveUser } from '../data/Students'; /** * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] @@ -26,21 +26,21 @@ export const insertUser = async (request: InsertUserRequest) => { const { googleObject } = request; try { // Check user object has all required fields - if (googleObject.email.replace("@cornell.edu", "") !== null) { + if (googleObject.email.replace('@cornell.edu', '') !== null) { const user = await getUserByNetId( - googleObject.email.replace("@cornell.edu", ""), + googleObject.email.replace('@cornell.edu', ''), ); if (user === null) { const newUser = { _id: shortid.generate(), // Check to see if Google returns first and last name // If not, insert empty string to database - firstName: googleObject.given_name ? googleObject.given_name : "", - lastName: googleObject.family_name ? googleObject.family_name : "", - netId: googleObject.email.replace("@cornell.edu", ""), + firstName: googleObject.given_name ? googleObject.given_name : '', + lastName: googleObject.family_name ? googleObject.family_name : '', + netId: googleObject.email.replace('@cornell.edu', ''), affiliation: null, token: null, - privilege: "regular", + privilege: 'regular', }; await saveUser(newUser); @@ -49,7 +49,7 @@ export const insertUser = async (request: InsertUserRequest) => { } // eslint-disable-next-line no-console - console.log("Error: Some user values are null in insertUser"); + console.log('Error: Some user values are null in insertUser'); return 0; } catch (error) { // eslint-disable-next-line no-console diff --git a/server/src/api/review/types.ts b/server/src/review/review.dto.ts similarity index 100% rename from server/src/api/review/types.ts rename to server/src/review/review.dto.ts diff --git a/server/src/api/review/routes.ts b/server/src/review/review.router.ts similarity index 72% rename from server/src/api/review/routes.ts rename to server/src/review/review.router.ts index 92b1d247e..b6a59c965 100644 --- a/server/src/api/review/routes.ts +++ b/server/src/review/review.router.ts @@ -1,29 +1,40 @@ -import { body } from "express-validator"; -import { getCrossListOR } from "common/CourseCard"; -import shortid from "shortid"; -import { Context, Endpoint } from "../../endpoints"; -import { Reviews, Students } from "../../../db/dbDefs"; -import { - getVerificationTicket } from "../../utils/utils"; +import express from 'express'; + +import { body } from 'express-validator'; +import { getCrossListOR } from 'common/CourseCard'; +import shortid from 'shortid'; +import { Context, Endpoint } from '../endpoints'; +import { Reviews, Students } from '../../db/dbDefs'; +import { getVerificationTicket } from '../auth/auth.controller'; import { getCourseById as getCourseByIdCallback, getClassByInfo, -} from "../../data/Classes"; -import { insertUser as insertUserCallback, JSONNonempty } from "./functions"; +} from '../data/Classes'; +import { + insertUser as insertUserCallback, + JSONNonempty, +} from './review.controller'; import { getReviewById, updateReviewLiked, sanitizeReview, getReviewsByCourse, -} from "../../data/Reviews"; -import { CourseIdQuery, InsertReviewRequest, InsertUserRequest, ClassByInfoQuery, ReviewRequest } from "./types"; +} from '../data/Reviews'; +import { + CourseIdQuery, + InsertReviewRequest, + InsertUserRequest, + ClassByInfoQuery, + ReviewRequest, +} from './review.dto'; +const router = express.Router(); /** * Get a course with this course_id from the Classes collection */ export const getCourseById: Endpoint = { - guard: [body("courseId").notEmpty().isAscii()], + guard: [body('courseId').notEmpty().isAscii()], callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg.courseId), }; @@ -33,8 +44,8 @@ export const getCourseById: Endpoint = { */ export const getCourseByInfo: Endpoint = { guard: [ - body("number").notEmpty().isNumeric(), - body("subject").notEmpty().isAscii(), + body('number').notEmpty().isNumeric(), + body('subject').notEmpty().isAscii(), ], callback: async (ctx: Context, query: ClassByInfoQuery) => { try { @@ -44,7 +55,7 @@ export const getCourseByInfo: Endpoint = { console.log("Error: at 'getCourseByInfo' endpoint"); // eslint-disable-next-line no-console console.log(error); - return { code: 500, message: "Internal Server Error" }; + return { code: 500, message: 'Internal Server Error' }; } }, }; @@ -53,7 +64,7 @@ export const getCourseByInfo: Endpoint = { * Get list of review objects for given class from class _id */ export const getReviewsByCourseId: Endpoint = { - guard: [body("courseId").notEmpty().isAscii()], + guard: [body('courseId').notEmpty().isAscii()], callback: async (ctx: Context, courseId: CourseIdQuery) => { try { const course = await getCourseByIdCallback(courseId.courseId); @@ -64,13 +75,13 @@ export const getReviewsByCourseId: Endpoint = { return { code: 200, message: reviews }; } - return { code: 400, message: "Malformed Query" }; + return { code: 400, message: 'Malformed Query' }; } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getReviewsByCourseId' method"); // eslint-disable-next-line no-console console.log(error); - return { code: 500, message: "Internal Server Error" }; + return { code: 500, message: 'Internal Server Error' }; } }, }; @@ -82,14 +93,17 @@ export const getReviewsByCourseId: Endpoint = { * Returns 0 if there was an error */ export const insertUser: Endpoint = { - guard: [body("googleObject").notEmpty()], + guard: [body('googleObject').notEmpty()], callback: async (ctx: Context, arg: InsertUserRequest) => { const result = await insertUserCallback(arg); if (result === 1) { - return { code: 200, message: "User successfully added!" }; + return { code: 200, message: 'User successfully added!' }; } - return { code: 500, message: "An error occurred while trying to insert a new user" }; + return { + code: 500, + message: 'An error occurred while trying to insert a new user', + }; }, }; @@ -101,16 +115,16 @@ export const insertUser: Endpoint = { */ export const insertReview: Endpoint = { guard: [ - body("token").notEmpty().isAscii(), - body("classId").notEmpty().isAscii(), + body('token').notEmpty().isAscii(), + body('classId').notEmpty().isAscii(), ].concat( - JSONNonempty("review", [ - "text", - "difficulty", - "rating", - "workload", - "professors", - "isCovid", + JSONNonempty('review', [ + 'text', + 'difficulty', + 'rating', + 'workload', + 'professors', + 'isCovid', ]), ), callback: async (ctx: Context, request: InsertReviewRequest) => { @@ -121,14 +135,14 @@ export const insertReview: Endpoint = { const ticket = await getVerificationTicket(token); - if (!ticket) return { resCode: 1, error: "Missing verification ticket" }; + if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - if (ticket.hd === "cornell.edu") { + if (ticket.hd === 'cornell.edu') { // insert the user into the collection if not already present await insertUserCallback({ googleObject: ticket }); - const netId = ticket.email.replace("@cornell.edu", ""); + const netId = ticket.email.replace('@cornell.edu', ''); const student = await Students.findOne({ netId }); const related = await Reviews.find({ class: classId }); @@ -136,7 +150,7 @@ export const insertReview: Endpoint = { return { code: 400, message: - "Review is a duplicate of an already existing review for this class!", + 'Review is a duplicate of an already existing review for this class!', }; } @@ -170,18 +184,18 @@ export const insertReview: Endpoint = { { $set: { reviews: newReviews } }, ).exec(); - return { resCode: 1, errMsg: "" }; + return { resCode: 1, errMsg: '' }; } catch (error) { // eslint-disable-next-line no-console console.log(error); - return { resCode: 1, error: "Unexpected error when adding review" }; + return { resCode: 1, error: 'Unexpected error when adding review' }; } } else { // eslint-disable-next-line no-console - console.log("Error: non-Cornell email attempted to insert review"); + console.log('Error: non-Cornell email attempted to insert review'); return { resCode: 1, - error: "Error: non-Cornell email attempted to insert review", + error: 'Error: non-Cornell email attempted to insert review', }; } } catch (error) { @@ -199,7 +213,7 @@ export const insertReview: Endpoint = { * removes the like, and if the student has not, adds a like. */ export const updateLiked: Endpoint = { - guard: [body("id").notEmpty().isAscii(), body("token").notEmpty().isAscii()], + guard: [body('id').notEmpty().isAscii(), body('token').notEmpty().isAscii()], callback: async (ctx: Context, request: ReviewRequest) => { const { token } = request; try { @@ -207,11 +221,11 @@ export const updateLiked: Endpoint = { const ticket = await getVerificationTicket(token); - if (!ticket) return { resCode: 1, error: "Missing verification ticket" }; + if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - if (ticket.hd === "cornell.edu") { + if (ticket.hd === 'cornell.edu') { await insertUserCallback({ googleObject: ticket }); - const netId = ticket.email.replace("@cornell.edu", ""); + const netId = ticket.email.replace('@cornell.edu', ''); const student = await Students.findOne({ netId }); // removing like @@ -228,7 +242,11 @@ export const updateLiked: Endpoint = { await updateReviewLiked(request.id, 0, student.netId); } else { // bound the rating at 0 - await updateReviewLiked(request.id, review.likes - 1, student.netId); + await updateReviewLiked( + request.id, + review.likes - 1, + student.netId, + ); } } else { // adding like @@ -259,7 +277,7 @@ export const updateLiked: Endpoint = { } return { resCode: 1, - error: "Error: non-Cornell email attempted to insert review", + error: 'Error: non-Cornell email attempted to insert review', }; } catch (error) { // eslint-disable-next-line no-console @@ -272,24 +290,24 @@ export const updateLiked: Endpoint = { }; export const userHasLiked: Endpoint = { - guard: [body("id").notEmpty().isAscii(), body("token").notEmpty().isAscii()], + guard: [body('id').notEmpty().isAscii(), body('token').notEmpty().isAscii()], callback: async (ctx: Context, request: ReviewRequest) => { const { token } = request; try { const review = await Reviews.findOne({ _id: request.id }).exec(); const ticket = await getVerificationTicket(token); - if (!ticket) return { resCode: 1, error: "Missing verification ticket" }; + if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - if (ticket.hd !== "cornell.edu") { + if (ticket.hd !== 'cornell.edu') { return { resCode: 1, - error: "Error: non-Cornell email attempted to insert review", + error: 'Error: non-Cornell email attempted to insert review', }; } await insertUserCallback({ googleObject: ticket }); - const netId = ticket.email.replace("@cornell.edu", ""); + const netId = ticket.email.replace('@cornell.edu', ''); const student = await Students.findOne({ netId }); if (student.likedReviews && student.likedReviews.includes(review.id)) { diff --git a/server/src/api/search/functions.ts b/server/src/search/search.controller.ts similarity index 98% rename from server/src/api/search/functions.ts rename to server/src/search/search.controller.ts index d671c2099..badb43637 100644 --- a/server/src/api/search/functions.ts +++ b/server/src/search/search.controller.ts @@ -1,4 +1,4 @@ -import { Classes, Subjects } from "../../../db/dbDefs"; +import { Classes, Subjects } from "../../db/dbDefs"; /* * These utility methods are taken from methods.ts diff --git a/server/src/api/search/types.ts b/server/src/search/search.dto.ts similarity index 100% rename from server/src/api/search/types.ts rename to server/src/search/search.dto.ts diff --git a/server/src/api/search/routes.ts b/server/src/search/search.router.ts similarity index 94% rename from server/src/api/search/routes.ts rename to server/src/search/search.router.ts index a1ba40e88..04017b50c 100644 --- a/server/src/api/search/routes.ts +++ b/server/src/search/search.router.ts @@ -1,9 +1,9 @@ import { body } from "express-validator"; -import { Context, Endpoint } from "../../endpoints"; -import { Classes, Subjects, Professors } from "../../../db/dbDefs"; -import { Search } from "./types"; -import { courseSort, regexClassesSearch } from "./functions"; +import { Context, Endpoint } from "../endpoints"; +import { Classes, Subjects, Professors } from "../../db/dbDefs"; +import { Search } from "./search.dto"; +import { courseSort, regexClassesSearch } from "./search.controller"; /* * Query for classes using a query diff --git a/server/src/test/AdminActions.test.ts b/server/src/test/AdminActions.test.ts index 9ac2b05c4..448620401 100644 --- a/server/src/test/AdminActions.test.ts +++ b/server/src/test/AdminActions.test.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { configure } from "../endpoints"; import { Classes, Reviews } from "../../db/dbDefs"; -import * as Utils from "../utils/utils"; +import * as Utils from "../auth/auth.controller"; let mongoServer: MongoMemoryServer; let serverCloseHandle; diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts index c206b5d69..e69de29bb 100644 --- a/server/src/utils/utils.ts +++ b/server/src/utils/utils.ts @@ -1,60 +0,0 @@ -import { OAuth2Client } from "google-auth-library"; -import { getUserByNetId } from "../data/Students"; - -/** - * Returns true if [netid] matches the netid in the email of the JSON - * web token. False otherwise. - * This method authenticates the user token through the Google API. - * @param token: google auth token - * @param netid: netid to verify - * @requires that you have a handleVerifyError, like as follows: - * verify(token, function(){//do whatever}).catch(function(error){ - */ -export const getVerificationTicket = async (token?: string) => { - try { - if (token === undefined) { - // eslint-disable-next-line no-console - console.log("Token was undefined in getVerificationTicket"); - return null; - } - - const audience = "836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com"; - const client = new OAuth2Client(audience); - const ticket = await client.verifyIdToken({ - idToken: token, - audience, - }); - - return ticket.getPayload(); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getVerificationTicket' method"); - // eslint-disable-next-line no-console - console.log(error); - return null; - } -}; - -export const verifyToken = async (token: string) => { - try { - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(token)) { - const ticket = await getVerificationTicket(token); - if (ticket && ticket.email) { - const user = await getUserByNetId( - ticket.email.replace("@cornell.edu", ""), - ); - if (user) { - return user.privilege === "admin"; - } - } - } - return false; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'verifyToken' method"); - // eslint-disable-next-line no-console - console.log(error); - return false; - } -}; diff --git a/server/tsconfig.json b/server/tsconfig.json index a302425ed..c50dd2f56 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -16,11 +16,11 @@ "db/dbDefs.ts", "src/server.ts", "src/endpoints.ts", - "src/api/admin/AdminActions.ts", - "src/api/admin/AdminChart.ts", - "src/api/auth/routes.ts", - "src/api/review/routes.ts", - "src/api/search/routes.ts", + "src/admin/AdminActions.ts", + "src/admin/AdminChart.ts", + "src/auth/auth.router.ts", + "src/review/review.router.ts", + "src/search/search.router.ts", "src/utils/utils.ts" ] } \ No newline at end of file From f135a4b83b6dde1d45d180df8a8aba9eb8326ad0 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 20:54:39 -0500 Subject: [PATCH 18/33] change name --- server/src/admin/AdminActions.ts | 14 +++++++------- server/src/admin/AdminChart.ts | 10 +++++----- server/src/admin/admin.controller.ts | 4 ++-- server/src/auth/auth.controller.ts | 4 ++-- server/src/auth/auth.router.ts | 4 ++-- server/src/test/AdminActions.test.ts | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/server/src/admin/AdminActions.ts b/server/src/admin/AdminActions.ts index 1ca341d61..e656db15c 100644 --- a/server/src/admin/AdminActions.ts +++ b/server/src/admin/AdminActions.ts @@ -8,7 +8,7 @@ import { findAllSemesters, resetProfessorArray, } from "../../db/dbInit"; -import { verifyToken } from "../auth/auth.controller"; +import { verifyAdminToken } from "../auth/auth.controller"; import { getCourseById } from "../data/Classes"; import { ReviewRequest } from "../review/review.dto"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./admin.dto"; @@ -69,7 +69,7 @@ export const makeReviewVisible: Endpoint = { callback: async (ctx: Context, adminReviewRequest: AdminReviewRequest) => { try { // check: make sure review id is valid and non-malicious - const userIsAdmin = await verifyToken(adminReviewRequest.token); + const userIsAdmin = await verifyAdminToken(adminReviewRequest.token); const regex = new RegExp(/^(?=.*[A-Z0-9])/i); if (regex.test(adminReviewRequest.review._id) && userIsAdmin) { await Reviews.updateOne( @@ -103,7 +103,7 @@ export const undoReportReview: Endpoint = { ], callback: async (ctx: Context, adminReviewRequest: AdminReviewRequest) => { try { - const userIsAdmin = await verifyToken(adminReviewRequest.token); + const userIsAdmin = await verifyAdminToken(adminReviewRequest.token); if (userIsAdmin) { await Reviews.updateOne( { _id: adminReviewRequest.review._id }, @@ -134,7 +134,7 @@ export const removeReview: Endpoint = { ], callback: async (ctx: Context, adminReviewRequest: AdminReviewRequest) => { try { - const userIsAdmin = await verifyToken(adminReviewRequest.token); + const userIsAdmin = await verifyAdminToken(adminReviewRequest.token); if (userIsAdmin) { await Reviews.remove({ _id: adminReviewRequest.review._id }); const res = await updateCourseMetrics(adminReviewRequest.review.class); @@ -161,7 +161,7 @@ export const setProfessors: Endpoint = { adminProfessorsRequest: AdminProfessorsRequest, ) => { try { - const userIsAdmin = await verifyToken(adminProfessorsRequest.token); + const userIsAdmin = await verifyAdminToken(adminProfessorsRequest.token); if (userIsAdmin) { const semesters = await findAllSemesters(); const val = await updateProfessors(semesters); @@ -192,7 +192,7 @@ export const resetProfessors: Endpoint = { adminProfessorsRequest: AdminProfessorsRequest, ) => { try { - const userIsAdmin = await verifyToken(adminProfessorsRequest.token); + const userIsAdmin = await verifyAdminToken(adminProfessorsRequest.token); if (userIsAdmin) { const semesters = findAllSemesters(); const val = resetProfessorArray(semesters); @@ -237,7 +237,7 @@ export const fetchReviewableClasses: Endpoint = { guard: [body("token").notEmpty().isAscii()], callback: async (ctx: Context, request: AdminProfessorsRequest) => { try { - const userIsAdmin = await verifyToken(request.token); + const userIsAdmin = await verifyAdminToken(request.token); if (userIsAdmin) { return Reviews.find( { visible: 0 }, diff --git a/server/src/admin/AdminChart.ts b/server/src/admin/AdminChart.ts index c433cbf08..036c0dbb6 100644 --- a/server/src/admin/AdminChart.ts +++ b/server/src/admin/AdminChart.ts @@ -1,6 +1,6 @@ /* eslint-disable spaced-comment */ import { body } from "express-validator"; -import { verifyToken } from "../auth/auth.controller"; +import { verifyAdminToken } from "../auth/auth.controller"; import { Context, Endpoint } from "../endpoints"; import { Reviews, Classes, Subjects } from "../../db/dbDefs"; import { GetReviewsOverTimeTop15Request, Token } from "./admin.dto"; @@ -23,7 +23,7 @@ export const getReviewsOverTimeTop15: Endpoint = callback: async (ctx: Context, request: GetReviewsOverTimeTop15Request) => { const { token, step, range } = request; try { - const userIsAdmin = await verifyToken(token); + const userIsAdmin = await verifyAdminToken(token); if (userIsAdmin) { const top15 = await topSubjectsCB(ctx, { token }); // contains cs, math, gov etc... @@ -166,7 +166,7 @@ export const howManyEachClass: Endpoint = { callback: async (_ctx: Context, request: Token) => { const { token } = request; try { - const userIsAdmin = await verifyToken(token); + const userIsAdmin = await verifyAdminToken(token); if (userIsAdmin) { const pipeline = [ { @@ -200,7 +200,7 @@ export const totalReviews: Endpoint = { callback: async (_ctx: Context, request: Token) => { const { token } = request; try { - const userIsAdmin = await verifyToken(token); + const userIsAdmin = await verifyAdminToken(token); if (userIsAdmin) { return Reviews.find({}).count(); } @@ -224,7 +224,7 @@ export const howManyReviewsEachClass: Endpoint = { callback: async (_ctx: Context, request: Token) => { const { token } = request; try { - const userIsAdmin = await verifyToken(token); + const userIsAdmin = await verifyAdminToken(token); if (userIsAdmin) { const pipeline = [ { diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index 4c111453f..6bcc8d25d 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { verifyToken } from "../../utils/utils"; +import { verifyAdminToken } from "../../utils/utils"; import { Context } from "../../endpoints"; import { Token } from "./admin.dto"; import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; @@ -9,7 +9,7 @@ import { DefaultDict } from "./AdminChart"; */ // eslint-disable-next-line import/prefer-default-export export const topSubjectsCB = async (_ctx: Context, request: Token) => { - const userIsAdmin = await verifyToken(request.token); + const userIsAdmin = await verifyAdminToken(request.token); if (!userIsAdmin) { return null; } diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index c9aae6504..68305063c 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -35,7 +35,7 @@ export const getVerificationTicket = async (token?: string) => { } }; -export const verifyToken = async (token: string) => { +export const verifyAdminToken = async (token: string) => { try { const regex = new RegExp(/^(?=.*[A-Z0-9])/i); if (regex.test(token)) { @@ -52,7 +52,7 @@ export const verifyToken = async (token: string) => { return false; } catch (error) { // eslint-disable-next-line no-console - console.log("Error: at 'verifyToken' method"); + console.log("Error: at 'verifyAdminToken' method"); // eslint-disable-next-line no-console console.log(error); return false; diff --git a/server/src/auth/auth.router.ts b/server/src/auth/auth.router.ts index 4bdc98838..44c19affe 100644 --- a/server/src/auth/auth.router.ts +++ b/server/src/auth/auth.router.ts @@ -1,6 +1,6 @@ import { body } from 'express-validator'; import { Context, Endpoint } from '../endpoints'; -import { verifyToken } from './auth.controller'; +import { verifyAdminToken } from './auth.controller'; import { AdminRequest } from './auth.dto'; /* @@ -9,5 +9,5 @@ import { AdminRequest } from './auth.dto'; // eslint-disable-next-line import/prefer-default-export export const tokenIsAdmin: Endpoint = { guard: [body('token').notEmpty().isAscii()], - callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyToken(adminRequest.token), + callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyAdminToken(adminRequest.token), }; diff --git a/server/src/test/AdminActions.test.ts b/server/src/test/AdminActions.test.ts index 448620401..2bbc55a74 100644 --- a/server/src/test/AdminActions.test.ts +++ b/server/src/test/AdminActions.test.ts @@ -10,7 +10,7 @@ import * as Utils from "../auth/auth.controller"; let mongoServer: MongoMemoryServer; let serverCloseHandle; const mockVerification = jest - .spyOn(Utils, "verifyToken") + .spyOn(Utils, "verifyAdminToken") .mockImplementation(async (token?: string) => true); const testingPort = 47728; From 7923b7abb7c78519c0178ebab378c78b169f5a55 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 20:55:22 -0500 Subject: [PATCH 19/33] small changes --- server/src/admin/admin.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index 6bcc8d25d..212948a38 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -1,7 +1,7 @@ -import { verifyAdminToken } from "../../utils/utils"; -import { Context } from "../../endpoints"; +import { verifyAdminToken } from "../auth/auth.controller"; +import { Context } from "../endpoints"; import { Token } from "./admin.dto"; -import { Reviews, Classes, Subjects } from "../../../db/dbDefs"; +import { Reviews, Classes, Subjects } from "../../db/dbDefs"; import { DefaultDict } from "./AdminChart"; /** From 45578fb7b5ff3ed69e1d13e6408e2a5f0b65d24f Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 21:11:42 -0500 Subject: [PATCH 20/33] auth function --- server/src/auth/auth.controller.ts | 60 +++++++++++++++++++++++----- server/src/profile/profile.router.ts | 14 +++---- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 68305063c..e9bd6157f 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -35,20 +35,60 @@ export const getVerificationTicket = async (token?: string) => { } }; +export const getUserEmail = async (token: string) => { + try { + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (!regex.test(token)) { + return null; + } + + const ticket = await getVerificationTicket(token); + if (!(ticket && ticket.email)) { + return null; + } + + if (ticket.hd === 'cornell.edu') { + return ticket.email; + } + + return null; + } catch (err) { + // eslint-disable-next-line no-console + console.log("Error: at 'getUserEmail' method", err); + return null; + } +}; + +export const getUserNetId = async (token: string) => { + const email = await getUserEmail(token); + + try { + const netId = email.replace('@cornell.edu', ''); + return netId; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getUserNetId' method", error); + return null; + } +}; + export const verifyAdminToken = async (token: string) => { try { const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(token)) { - const ticket = await getVerificationTicket(token); - if (ticket && ticket.email) { - const user = await getUserByNetId( - ticket.email.replace('@cornell.edu', ''), - ); - if (user) { - return user.privilege === 'admin'; - } - } + if (!regex.test(token)) { + return false; + } + + const ticket = await getVerificationTicket(token); + if (!(ticket && ticket.email)) { + return false; } + + const user = await getUserByNetId(ticket.email.replace('@cornell.edu', '')); + if (user) { + return user.privilege === 'admin'; + } + return false; } catch (error) { // eslint-disable-next-line no-console diff --git a/server/src/profile/profile.router.ts b/server/src/profile/profile.router.ts index 084551f24..c2c81c43a 100644 --- a/server/src/profile/profile.router.ts +++ b/server/src/profile/profile.router.ts @@ -1,7 +1,7 @@ import { body } from "express-validator"; import { Context, Endpoint } from "../endpoints"; import { ProfileRequest, NetIdQuery } from "./profile.dto"; -import { getVerificationTicket } from "../auth/auth.controller"; +import { getUserEmail } from "../auth/auth.controller"; import { getUserByNetId, getStudentReviewIds } from "../data/Students"; import { getNonNullReviews } from "../data/Reviews"; @@ -11,15 +11,13 @@ export const getStudentEmailByToken: Endpoint = { const { token } = request; try { - const ticket = await getVerificationTicket(token); - if (ticket === undefined || ticket === null) { - return { code: 404, message: "Unable to verify token" }; - } - if (ticket.hd === "cornell.edu") { - return { code: 200, message: ticket.email }; + const email = await getUserEmail(token); + + if (!email) { + return { code: 404, message: `Email not found: {email}` }; } - return { code: 500, message: "Invalid email" }; + return { code: 200, message: email }; } catch (error) { // eslint-disable-next-line no-console console.log("Error: at 'getStudentEmailByToken' method"); From c0f4bf88437e8aea90aa6fb68e9ff0c513b77307 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 21:40:40 -0500 Subject: [PATCH 21/33] admin endpoints --- server/src/auth/auth.controller.ts | 4 +- server/src/auth/auth.router.ts | 16 ++++--- server/src/server.ts | 71 +++++++----------------------- server/src/utils/const.ts | 2 + server/src/utils/mongoose.ts | 8 ++++ 5 files changed, 37 insertions(+), 64 deletions(-) create mode 100644 server/src/utils/const.ts create mode 100644 server/src/utils/mongoose.ts diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index e9bd6157f..6e6c0bf92 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -1,5 +1,6 @@ import { OAuth2Client } from 'google-auth-library'; import { getUserByNetId } from '../data/Students'; +import { googleAudience } from '../utils/const'; /** * Returns true if [netid] matches the netid in the email of the JSON @@ -18,8 +19,9 @@ export const getVerificationTicket = async (token?: string) => { return null; } - const audience = '836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com'; + const audience = googleAudience; const client = new OAuth2Client(audience); + const ticket = await client.verifyIdToken({ idToken: token, audience, diff --git a/server/src/auth/auth.router.ts b/server/src/auth/auth.router.ts index 44c19affe..cc4d9a394 100644 --- a/server/src/auth/auth.router.ts +++ b/server/src/auth/auth.router.ts @@ -1,13 +1,15 @@ -import { body } from 'express-validator'; -import { Context, Endpoint } from '../endpoints'; +import express from 'express'; import { verifyAdminToken } from './auth.controller'; import { AdminRequest } from './auth.dto'; +const router = express.Router(); + /* * Check if a token is for an admin */ -// eslint-disable-next-line import/prefer-default-export -export const tokenIsAdmin: Endpoint = { - guard: [body('token').notEmpty().isAscii()], - callback: async (ctx: Context, adminRequest: AdminRequest) => await verifyAdminToken(adminRequest.token), -}; +router.post('/isAdmin', async (req, res) => { + const adminRequest = req.body as AdminRequest; + await verifyAdminToken(adminRequest.token); +}); + +export default router; diff --git a/server/src/server.ts b/server/src/server.ts index 15f10b4dd..94b1feb62 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,72 +1,31 @@ import path from "path"; import express from "express"; import sslRedirect from "heroku-ssl-redirect"; -import mongoose from "mongoose"; -import { MongoMemoryServer } from "mongodb-memory-server"; + import cors from "cors"; import dotenv from "dotenv"; -import { fetchAddCourses } from "../db/dbInit"; import { configure } from "./endpoints"; +import mongoose from "./utils/mongoose"; + +import auth from "./auth/auth.router"; + dotenv.config(); + +const port = process.env.PORT || 8080; + const app = express(); app.use(sslRedirect(["development", "production"])); app.use(cors()); -app.use(express.static(path.join(__dirname, "../../client/build"))); - -function setup() { - const port = process.env.PORT || 8080; - app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html"))); - configure(app); - - // eslint-disable-next-line no-console - app.listen(port, () => console.log(`Listening on port ${port}...`)); -} - -const uri = process.env.MONGODB_URL - ? process.env.MONGODB_URL - : "this will error"; -let localMongoServer; -mongoose - .connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }) - .then(async () => setup()) - .catch(async (err) => { - // eslint-disable-next-line no-console - console.error("No DB connection defined!"); +app.use("/api/auth", auth); - // If the environment variable is set, create a simple local db to work with - // This could be expanded in the future with default mock admin accounts, etc. - // For now, it fetches Fall 2019 classes for you to view - // This is useful if you need a local db without any hassle, and don't want to risk damage to the staging db - if (process.env.ALLOW_LOCAL === "1") { - // eslint-disable-next-line no-console - console.log("Falling back to local db!"); - localMongoServer = new MongoMemoryServer(); - const mongoUri = await localMongoServer.getUri(); - await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); +app.use(express.static(path.join(__dirname, '../../client/build'))); - // eslint-disable-next-line no-console - await fetchAddCourses( - "https://classes.cornell.edu/api/2.0/", - "FA19", - ).catch((e) => console.error(e)); +app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html"))); +configure(app); - await mongoose.connection.collections.classes.createIndex({ - classFull: "text", - }); - await mongoose.connection.collections.subjects.createIndex({ - subShort: "text", - }); - await mongoose.connection.collections.professors.createIndex({ - fullName: "text", - }); +// eslint-disable-next-line no-console +app.listen(port, () => console.log(`Listening on port ${port}...`)); - setup(); - } else { - process.exit(1); - } - }); +mongoose.then(async () => { setup(); }); diff --git a/server/src/utils/const.ts b/server/src/utils/const.ts new file mode 100644 index 000000000..e4660343f --- /dev/null +++ b/server/src/utils/const.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const googleAudience = '836283700372-msku5vqaolmgvh3q1nvcqm3d6cgiu0v1.apps.googleusercontent.com'; diff --git a/server/src/utils/mongoose.ts b/server/src/utils/mongoose.ts new file mode 100644 index 000000000..c3ce97e48 --- /dev/null +++ b/server/src/utils/mongoose.ts @@ -0,0 +1,8 @@ +import mongoose from 'mongoose'; + +const uri = process.env.MONGODB_URL + ? process.env.MONGODB_URL + : 'this will error'; + +export default mongoose + .connect(uri, { useNewUrlParser: true, useUnifiedTopology: true }); From d4b1b1ff443efb4547b72f29df7ccc5b9288d82b Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 22:55:09 -0500 Subject: [PATCH 22/33] deprecate endpoints.ts for profile --- server/src/auth/auth.router.ts | 12 +- server/src/endpoints.ts | 8 -- server/src/profile/profile.router.ts | 197 ++++++++++++++------------- server/src/server.ts | 21 ++- 4 files changed, 119 insertions(+), 119 deletions(-) diff --git a/server/src/auth/auth.router.ts b/server/src/auth/auth.router.ts index cc4d9a394..ec4ecaf51 100644 --- a/server/src/auth/auth.router.ts +++ b/server/src/auth/auth.router.ts @@ -9,7 +9,17 @@ const router = express.Router(); */ router.post('/isAdmin', async (req, res) => { const adminRequest = req.body as AdminRequest; - await verifyAdminToken(adminRequest.token); + try { + const verify = await verifyAdminToken(adminRequest.token); + + if (verify === false) { + res.status(400).json({ error: `Unable to verify token: ${adminRequest.token} as an admin.` }); + } + + res.status(200).json({ message: `Token: ${adminRequest.token} was successfully verified as an admin user.` }); + } catch (err) { + res.status(500).json({ error: `An error occurred: ${err} in the 'isAdmin' endpoint.` }); + } }); export default router; diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index 8e913abbd..247017cb6 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -16,13 +16,6 @@ import { updateLiked, userHasLiked, } from "./review/review.router"; -import { - countReviewsByStudentId, - getTotalLikesByStudentId, - getReviewsByStudentId, - getStudentEmailByToken, -} from "./profile/profile.router"; -import { tokenIsAdmin } from "./auth/auth.router"; import { getCoursesByProfessor, getCoursesByMajor, @@ -66,7 +59,6 @@ export function configure(app: express.Application) { register(app, "getClassesByQuery", getClassesByQuery); register(app, "getReviewsByCourseId", getReviewsByCourseId); register(app, "getCourseById", getCourseById); - register(app, "tokenIsAdmin", tokenIsAdmin); register(app, "getSubjectsByQuery", getSubjectsByQuery); register(app, "getCoursesByMajor", getCoursesByMajor); register(app, "getProfessorsByQuery", getProfessorsByQuery); diff --git a/server/src/profile/profile.router.ts b/server/src/profile/profile.router.ts index c2c81c43a..506ef34be 100644 --- a/server/src/profile/profile.router.ts +++ b/server/src/profile/profile.router.ts @@ -1,119 +1,120 @@ -import { body } from "express-validator"; -import { Context, Endpoint } from "../endpoints"; -import { ProfileRequest, NetIdQuery } from "./profile.dto"; -import { getUserEmail } from "../auth/auth.controller"; -import { getUserByNetId, getStudentReviewIds } from "../data/Students"; -import { getNonNullReviews } from "../data/Reviews"; +import express from 'express'; +import { body } from 'express-validator'; +import { Context, Endpoint } from '../endpoints'; +import { ProfileRequest, NetIdQuery } from './profile.dto'; +import { getUserEmail } from '../auth/auth.controller'; +import { getUserByNetId, getStudentReviewIds } from '../data/Students'; +import { getNonNullReviews } from '../data/Reviews'; -export const getStudentEmailByToken: Endpoint = { - guard: [body("token").notEmpty().isAscii()], - callback: async (ctx: Context, request: ProfileRequest) => { - const { token } = request; +const router = express.Router(); - try { - const email = await getUserEmail(token); +router.post('/getStudentEmailByToken', async (req, res) => { + const { token } = req.body as ProfileRequest; + try { + const email = await getUserEmail(token); - if (!email) { - return { code: 404, message: `Email not found: {email}` }; - } - - return { code: 200, message: email }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getStudentEmailByToken' method"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: error.message }; + if (!email) { + res.status(400).json({ message: `Email not found: ${email}` }); } - }, -}; + + res.status(200).json({ message: email }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getStudentEmailByToken' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: error.message }); + } +}); /** * Counts the number of reviews made by a given student id. */ -export const countReviewsByStudentId: Endpoint = { - guard: [body("netId").notEmpty().isAscii()], - callback: async (ctx: Context, request: NetIdQuery) => { - const { netId } = request; - try { - const studentDoc = await getUserByNetId(netId); - if (studentDoc === null) { - return { code: 404, message: "Unable to find student with netId: ", netId }; - } - const reviews = await getStudentReviewIds(studentDoc); - return { code: 200, message: reviews.length }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'countReviewsByStudentId' method"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: error.message }; +router.post('/countReviewsByStudentId', async (req, res) => { + const { netId } = req.body as NetIdQuery; + try { + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null) { + res.status(404).json({ + message: `Unable to find student with netId: ${netId}`, + }); } - }, -}; + + const reviews = await getStudentReviewIds(studentDoc); + return res.status(200).json({ message: reviews.length }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'countReviewsByStudentId' method"); + // eslint-disable-next-line no-console + console.log(error); + return res.status(500).json({ error: error.message }); + } +}); /** * [getTotalLikesByStudentId] returns the total number of likes a student has gotten on their reviews */ -export const getTotalLikesByStudentId: Endpoint = { - guard: [body("netId").notEmpty().isAscii()], - callback: async (ctx: Context, request: NetIdQuery) => { - const { netId } = request; - let totalLikes = 0; - try { - const studentDoc = await getUserByNetId(netId); - if (studentDoc === null) { - return { - code: 404, - message: "Unable to find student with netId: ", - netId, - }; +router.post('/getTotalLikesByStudentId', async (req, res) => { + const { netId } = req.body as NetIdQuery; + let totalLikes = 0; + try { + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null) { + res.status(404).json({ + message: `Unable to find student with netId: ${netId}`, + }); + } + + const reviewIds = await getStudentReviewIds(studentDoc); + const reviews = await getNonNullReviews(reviewIds); + reviews.forEach((review) => { + if (review.likes !== undefined) { + totalLikes += review.likes; } + }); - const reviewIds = await getStudentReviewIds(studentDoc); - const reviews = await getNonNullReviews(reviewIds); - reviews.forEach((review) => { - if (review.likes !== undefined) { - totalLikes += review.likes; - } + res + .status(200) + .json({ + message: `Successfully retrieved total like by student with netid: ${netId}`, + data: totalLikes, }); - - return { code: 200, message: totalLikes }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getTotalLikesByStudentId' method"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: error.message }; - } - }, -}; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getTotalLikesByStudentId' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: error.message }); + } +}); /** * [getReviewsByStudentId] returns a list of review objects that are created by the given student's netID */ -export const getReviewsByStudentId: Endpoint = { - guard: [body("netId").notEmpty().isAscii()], - callback: async (ctx: Context, request: NetIdQuery) => { - const { netId } = request; - try { - const studentDoc = await getUserByNetId(netId); - if (studentDoc === null) { - return { - code: 404, - message: "Unable to find student with netId: ", - netId, - }; - } - const reviewIds = await getStudentReviewIds(studentDoc); - const reviews = await getNonNullReviews(reviewIds); - return { code: 200, message: reviews }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getReviewsByStudentId' method"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: error.message }; +router.post('/getReviewsbyStudentId', async (req, res) => { + const { netId } = req.body as NetIdQuery; + try { + const studentDoc = await getUserByNetId(netId); + if (studentDoc === null) { + res.status(404).json({ + message: `Unable to find student with netId: ${netId}`, + }); } - }, -}; + const reviewIds = await getStudentReviewIds(studentDoc); + const reviews = await getNonNullReviews(reviewIds); + res + .status(200) + .json({ + message: `Successfully retrieved reviews from student with id ${netId}`, + data: reviews, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getReviewsByStudentId' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: error.message }); + } +}); + +export default router; diff --git a/server/src/server.ts b/server/src/server.ts index 94b1feb62..8155067eb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,28 +4,25 @@ import sslRedirect from "heroku-ssl-redirect"; import cors from "cors"; import dotenv from "dotenv"; -import { configure } from "./endpoints"; - -import mongoose from "./utils/mongoose"; import auth from "./auth/auth.router"; -dotenv.config(); - -const port = process.env.PORT || 8080; +import mongoose from "./utils/mongoose"; +dotenv.config(); const app = express(); app.use(sslRedirect(["development", "production"])); + app.use(cors()); app.use("/api/auth", auth); -app.use(express.static(path.join(__dirname, '../../client/build'))); - -app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html"))); -configure(app); +function setup() { + const port = process.env.PORT || 8080; + app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html"))); -// eslint-disable-next-line no-console -app.listen(port, () => console.log(`Listening on port ${port}...`)); + // eslint-disable-next-line no-console + app.listen(port, () => console.log(`Listening on port ${port}...`)); +} mongoose.then(async () => { setup(); }); From 9e6ceecadc33855fef18d3eee6c624b1ad617425 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 22:55:33 -0500 Subject: [PATCH 23/33] remove unnecessary lines --- server/src/profile/profile.router.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/profile/profile.router.ts b/server/src/profile/profile.router.ts index 506ef34be..909b89b95 100644 --- a/server/src/profile/profile.router.ts +++ b/server/src/profile/profile.router.ts @@ -1,6 +1,4 @@ import express from 'express'; -import { body } from 'express-validator'; -import { Context, Endpoint } from '../endpoints'; import { ProfileRequest, NetIdQuery } from './profile.dto'; import { getUserEmail } from '../auth/auth.controller'; import { getUserByNetId, getStudentReviewIds } from '../data/Students'; From 34c9c5f6d335f95e8485466a8153b3675a979956 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 22:56:10 -0500 Subject: [PATCH 24/33] remove unnecessary --- server/src/endpoints.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index 247017cb6..e7def8912 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -78,10 +78,6 @@ export function configure(app: express.Application) { register(app, "getReviewsOverTimeTop15", getReviewsOverTimeTop15); register(app, "updateLiked", updateLiked); register(app, "userHasLiked", userHasLiked); - register(app, "getTotalLikesByStudentId", getTotalLikesByStudentId); - register(app, "getReviewsByStudentId", getReviewsByStudentId); - register(app, "countReviewsByStudentId", countReviewsByStudentId); - register(app, "getStudentEmailByToken", getStudentEmailByToken); register(app, "getRaffleWinner", getRaffleWinner); } From 00b7cadd79b77b8d741f68ceb6d2918cafc1e001 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 23:18:41 -0500 Subject: [PATCH 25/33] refactor search --- server/src/endpoints.ts | 2 - server/src/search/search.dto.ts | 2 +- server/src/search/search.router.ts | 210 +++++++++++++++-------------- 3 files changed, 108 insertions(+), 106 deletions(-) diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index e7def8912..b76783e9d 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -19,7 +19,6 @@ import { import { getCoursesByProfessor, getCoursesByMajor, - getClassesByQuery, getSubjectsByQuery, getProfessorsByQuery, } from "./search/search.router"; @@ -56,7 +55,6 @@ export function configure(app: express.Application) { // needed to get client IP apparently app.set("trust proxy", true); - register(app, "getClassesByQuery", getClassesByQuery); register(app, "getReviewsByCourseId", getReviewsByCourseId); register(app, "getCourseById", getCourseById); register(app, "getSubjectsByQuery", getSubjectsByQuery); diff --git a/server/src/search/search.dto.ts b/server/src/search/search.dto.ts index e2adf4a9c..ae563bced 100644 --- a/server/src/search/search.dto.ts +++ b/server/src/search/search.dto.ts @@ -1,4 +1,4 @@ // The type for a search query -export interface Search { +export interface SearchQuery { query: string; } diff --git a/server/src/search/search.router.ts b/server/src/search/search.router.ts index 04017b50c..5043e6a4a 100644 --- a/server/src/search/search.router.ts +++ b/server/src/search/search.router.ts @@ -1,121 +1,125 @@ -import { body } from "express-validator"; +import express from 'express'; +import { Classes, Subjects, Professors } from '../../db/dbDefs'; +import { SearchQuery } from './search.dto'; +import { courseSort, regexClassesSearch } from './search.controller'; -import { Context, Endpoint } from "../endpoints"; -import { Classes, Subjects, Professors } from "../../db/dbDefs"; -import { Search } from "./search.dto"; -import { courseSort, regexClassesSearch } from "./search.controller"; +const router = express.Router(); /* * Query for classes using a query */ -export const getClassesByQuery: Endpoint = { - guard: [body("query").notEmpty()], - callback: async (ctx: Context, search: Search) => { - // Filter by not-whitespace, then match any not word. - const query = search.query.replace(/(?=[^\s])\W/g, ""); - try { - const classes = await Classes.find( - { $text: { $search: search.query } }, - { score: { $meta: "textScore" } }, - { sort: { score: { $meta: "textScore" } } }, - ).exec(); - if (classes && classes.length > 0) { - return classes.sort(courseSort(query)); - } - return await regexClassesSearch(query); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getClassesByQuery' endpoint"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; +router.post('/getClassesByQuery', async (req, res) => { + const { query } = req.body as SearchQuery; + try { + const classes = await Classes.find( + { $text: { $search: query } }, + { score: { $meta: 'textScore' } }, + { sort: { score: { $meta: 'textScore' } } }, + ).exec(); + if (classes && classes.length > 0) { + res.status(200).json({ + message: `Successfully retrieved classes with query ${query}`, + data: classes.sort(courseSort(query)), + }); } - }, -}; + + const regexClasses = await regexClassesSearch(query); + res + .status(200) + .json({ + message: `Successfully retrieved classes with query ${query} using regex`, + data: regexClasses, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getClassesByQuery' endpoint"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); /* * Searches the database on Subjects using the text index and returns matching subjects */ -export const getSubjectsByQuery: Endpoint = { - guard: [body("query").notEmpty().isAscii()], - callback: async (ctx: Context, search: Search) => { - try { - return await Subjects.find( - { $text: { $search: search.query } }, - { score: { $meta: "textScore" } }, - { sort: { score: { $meta: "textScore" } } }, - ).exec(); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getSubjectsByQuery' endpoint"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; - } - }, -}; +router.post("/getSubjectsByQuery", async (req, res) => { + const { query } = req.body as SearchQuery; + try { + const subjects = await Subjects.find( + { $text: { $search: query } }, + { score: { $meta: 'textScore' } }, + { sort: { score: { $meta: 'textScore' } } }, + ).exec(); + + res.status(200).json({ message: `Successfully retrieved subjects with query ${query}`, data: subjects }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getSubjectsByQuery' endpoint"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); /* * Searches the database on Professors using the text index and returns matching professors */ -export const getProfessorsByQuery: Endpoint = { - guard: [body("query").notEmpty().isAscii()], - callback: async (ctx: Context, search: Search) => { - try { - return await Professors.find( - { $text: { $search: search.query } }, - { score: { $meta: "textScore" } }, - { sort: { score: { $meta: "textScore" } } }, - ).exec(); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getProfessorsByQuery' endpoint"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; - } - }, -}; +router.post("/getProfessorsByQuery", async (req, res) => { + const { query } = req.body as SearchQuery; + try { + const professors = await Professors.find( + { $text: { $search: query } }, + { score: { $meta: 'textScore' } }, + { sort: { score: { $meta: 'textScore' } } }, + ).exec(); + + res.status(200).json({ message: `Successfully retrieved professors from query ${query}`, data: professors }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getProfessorsByQuery' endpoint"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); -export const getCoursesByMajor: Endpoint = { - guard: [body("query").notEmpty().isAscii()], - callback: async (ctx: Context, search: Search) => { - try { - let courses = []; - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(search.query)) { - courses = await Classes.find({ classSub: search.query }).exec(); - } - return courses; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getCoursesByMajor' method"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; +router.post("/getCoursesByMajor", async (req, res) => { + const { query } = req.body as SearchQuery; + try { + let courses = []; + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(query)) { + courses = await Classes.find({ classSub: query }).exec(); } - }, -}; + res.status(200).json({ message: `Successfully retrieved all courses by major based on query ${query}`, data: courses }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getCoursesByMajor' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); -export const getCoursesByProfessor: Endpoint = { - guard: [body("query").notEmpty().isAscii()], - callback: async (ctx: Context, search: Search) => { - try { - let courses = []; - const regex = new RegExp(/^(?=.*[A-Z0-9])/i); - if (regex.test(search.query)) { - const professorRegex = search.query.replace("+", ".*."); - courses = await Classes.find({ - classProfessors: { $regex: professorRegex, $options: "i" }, - }).exec(); - } - return courses; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getCoursesByProfessor' method"); - // eslint-disable-next-line no-console - console.log(error); - return { error: "Internal Server Error" }; +router.post("/getCoursesByProfessor", async (req, res) => { + const { query } = req.body as SearchQuery; + try { + let courses = []; + const regex = new RegExp(/^(?=.*[A-Z0-9])/i); + if (regex.test(query)) { + const professorRegex = query.replace('+', '.*.'); + courses = await Classes.find({ + classProfessors: { $regex: professorRegex, $options: 'i' }, + }).exec(); } - }, -}; + res.status(200).json({ message: `Successfully retrieved all courses by professor based on query ${query}`, data: courses }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getCoursesByProfessor' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +export default router; From 09abd189060a46074c42dd97c25db319cfe93173 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 23:19:02 -0500 Subject: [PATCH 26/33] remove unnecessary --- server/src/endpoints.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index b76783e9d..04b48f06c 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -16,12 +16,6 @@ import { updateLiked, userHasLiked, } from "./review/review.router"; -import { - getCoursesByProfessor, - getCoursesByMajor, - getSubjectsByQuery, - getProfessorsByQuery, -} from "./search/search.router"; import { fetchReviewableClasses, reportReview, @@ -57,10 +51,6 @@ export function configure(app: express.Application) { register(app, "getReviewsByCourseId", getReviewsByCourseId); register(app, "getCourseById", getCourseById); - register(app, "getSubjectsByQuery", getSubjectsByQuery); - register(app, "getCoursesByMajor", getCoursesByMajor); - register(app, "getProfessorsByQuery", getProfessorsByQuery); - register(app, "getCoursesByProfessor", getCoursesByProfessor); register(app, "insertReview", insertReview); register(app, "insertUser", insertUser); register(app, "makeReviewVisible", makeReviewVisible); From c25a14b7ce1df2d14aeb0e5d1a8d908355d35eca Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sat, 25 Nov 2023 23:20:11 -0500 Subject: [PATCH 27/33] add search and profile endpoints --- server/src/server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index 8155067eb..ad55c5b53 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -6,6 +6,8 @@ import cors from "cors"; import dotenv from "dotenv"; import auth from "./auth/auth.router"; +import profile from "./profile/profile.router"; +import search from "./search/search.router"; import mongoose from "./utils/mongoose"; @@ -16,6 +18,8 @@ app.use(sslRedirect(["development", "production"])); app.use(cors()); app.use("/api/auth", auth); +app.use('/api/profile', profile); +app.use('/api/search', search); function setup() { const port = process.env.PORT || 8080; From 8fd6cdf1aa271f64f8d536c6997c670e64360bd4 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:04:16 -0500 Subject: [PATCH 28/33] review router refactor --- server/src/endpoints.ts | 16 - server/src/review/review.router.ts | 493 +++++++++++++++-------------- server/src/server.ts | 2 + 3 files changed, 249 insertions(+), 262 deletions(-) diff --git a/server/src/endpoints.ts b/server/src/endpoints.ts index 04b48f06c..8a24f2fcd 100644 --- a/server/src/endpoints.ts +++ b/server/src/endpoints.ts @@ -7,15 +7,6 @@ import { topSubjects, getReviewsOverTimeTop15, } from "./admin/AdminChart"; -import { - getReviewsByCourseId, - getCourseById, - insertReview, - insertUser, - getCourseByInfo, - updateLiked, - userHasLiked, -} from "./review/review.router"; import { fetchReviewableClasses, reportReview, @@ -49,23 +40,16 @@ export function configure(app: express.Application) { // needed to get client IP apparently app.set("trust proxy", true); - register(app, "getReviewsByCourseId", getReviewsByCourseId); - register(app, "getCourseById", getCourseById); - register(app, "insertReview", insertReview); - register(app, "insertUser", insertUser); register(app, "makeReviewVisible", makeReviewVisible); register(app, "fetchReviewableClasses", fetchReviewableClasses); register(app, "undoReportReview", undoReportReview); register(app, "reportReview", reportReview); register(app, "removeReview", removeReview); - register(app, "getCourseByInfo", getCourseByInfo); register(app, "totalReviews", totalReviews); register(app, "howManyReviewsEachClass", howManyReviewsEachClass); register(app, "howManyEachClass", howManyEachClass); register(app, "topSubjects", topSubjects); register(app, "getReviewsOverTimeTop15", getReviewsOverTimeTop15); - register(app, "updateLiked", updateLiked); - register(app, "userHasLiked", userHasLiked); register(app, "getRaffleWinner", getRaffleWinner); } diff --git a/server/src/review/review.router.ts b/server/src/review/review.router.ts index b6a59c965..072115018 100644 --- a/server/src/review/review.router.ts +++ b/server/src/review/review.router.ts @@ -1,9 +1,7 @@ import express from 'express'; -import { body } from 'express-validator'; import { getCrossListOR } from 'common/CourseCard'; import shortid from 'shortid'; -import { Context, Endpoint } from '../endpoints'; import { Reviews, Students } from '../../db/dbDefs'; import { getVerificationTicket } from '../auth/auth.controller'; import { @@ -12,7 +10,6 @@ import { } from '../data/Classes'; import { insertUser as insertUserCallback, - JSONNonempty, } from './review.controller'; import { getReviewById, @@ -33,58 +30,68 @@ const router = express.Router(); /** * Get a course with this course_id from the Classes collection */ -export const getCourseById: Endpoint = { - guard: [body('courseId').notEmpty().isAscii()], - callback: async (ctx: Context, arg: CourseIdQuery) => await getCourseByIdCallback(arg.courseId), -}; +router.post('/getCourseById', async (req, res) => { + const { courseId } = req.body as CourseIdQuery; + try { + const course = await getCourseByIdCallback(courseId); + res.status(200).json({ + message: `Successfully retrieved course by id ${courseId}`, + data: course, + }); + } catch (error) { + res.status(500).json({ + message: `Error trying to retrieve course with id: ${courseId}`, + }); + } +}); /* * Searches the database for a course matching the subject and course number. Used in class info retrieval. * See also: getCourseById above */ -export const getCourseByInfo: Endpoint = { - guard: [ - body('number').notEmpty().isNumeric(), - body('subject').notEmpty().isAscii(), - ], - callback: async (ctx: Context, query: ClassByInfoQuery) => { - try { - return await getClassByInfo(query.subject, query.number); - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getCourseByInfo' endpoint"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: 'Internal Server Error' }; - } - }, -}; +router.post('/getCourseByInfo', async (req, res) => { + const { number, subject } = req.body as ClassByInfoQuery; + try { + const course = await getClassByInfo(subject, number); + res.status(200).json({ + message: `Successfully retrieved course by info number: ${number} and subject: ${subject}`, + data: course, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getCourseByInfo' endpoint"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); /** * Get list of review objects for given class from class _id */ -export const getReviewsByCourseId: Endpoint = { - guard: [body('courseId').notEmpty().isAscii()], - callback: async (ctx: Context, courseId: CourseIdQuery) => { - try { - const course = await getCourseByIdCallback(courseId.courseId); - if (course) { - const crossListOR = getCrossListOR(course); - const reviews = await getReviewsByCourse(crossListOR); - - return { code: 200, message: reviews }; - } - - return { code: 400, message: 'Malformed Query' }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'getReviewsByCourseId' method"); - // eslint-disable-next-line no-console - console.log(error); - return { code: 500, message: 'Internal Server Error' }; +router.post('/getReviewsByCourseId', async (req, res) => { + const { courseId } = req.body as CourseIdQuery; + try { + const course = await getCourseByIdCallback(courseId); + if (course) { + const crossListOR = getCrossListOR(course); + const reviews = await getReviewsByCourse(crossListOR); + + res.status(200).json({ + message: `Successfully retrieved reviews by course id: ${courseId}`, + data: reviews, + }); } - }, -}; + + res.status(400).json({ error: 'Malformed Query' }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'getReviewsByCourseId' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); /** * Inserts a new user into the database, if the user was not already present @@ -92,20 +99,18 @@ export const getReviewsByCourseId: Endpoint = { * Returns 1 if the user was added to the database, or was already present * Returns 0 if there was an error */ -export const insertUser: Endpoint = { - guard: [body('googleObject').notEmpty()], - callback: async (ctx: Context, arg: InsertUserRequest) => { - const result = await insertUserCallback(arg); - if (result === 1) { - return { code: 200, message: 'User successfully added!' }; - } - return { - code: 500, - message: 'An error occurred while trying to insert a new user', - }; - }, -}; +router.post('/insertUser', async (req, res) => { + const insertUserRequest = req.body as InsertUserRequest; + const result = await insertUserCallback(insertUserRequest); + if (result === 1) { + res.status(200).json({ message: 'User successfully added!' }); + } + + res.status(500).json({ + message: 'An error occurred while trying to insert a new user', + }); +}); /** * Insert a new review into the database @@ -113,214 +118,210 @@ export const insertUser: Endpoint = { * Returns 0 if there was an error * Returns 1 on a success */ -export const insertReview: Endpoint = { - guard: [ - body('token').notEmpty().isAscii(), - body('classId').notEmpty().isAscii(), - ].concat( - JSONNonempty('review', [ - 'text', - 'difficulty', - 'rating', - 'workload', - 'professors', - 'isCovid', - ]), - ), - callback: async (ctx: Context, request: InsertReviewRequest) => { - try { - const { token } = request; - const { classId } = request; - const { review } = request; - - const ticket = await getVerificationTicket(token); - - if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - - if (ticket.hd === 'cornell.edu') { - // insert the user into the collection if not already present - - await insertUserCallback({ googleObject: ticket }); - - const netId = ticket.email.replace('@cornell.edu', ''); - const student = await Students.findOne({ netId }); - - const related = await Reviews.find({ class: classId }); - if (related.find((v) => v.text === review.text)) { - return { - code: 400, - message: - 'Review is a duplicate of an already existing review for this class!', - }; - } +router.post('/insertReview', async (req, res) => { + const { token, classId, review } = req.body as InsertReviewRequest; - try { - // Attempt to insert the review - const fullReview = new Reviews({ - _id: shortid.generate(), - text: review.text, - difficulty: review.difficulty, - rating: review.rating, - workload: review.workload, - class: classId, - date: new Date(), - visible: 0, - reported: 0, - professors: review.professors, - likes: 0, - isCovid: review.isCovid, - user: student._id, - grade: review.grade, - major: review.major, - }); + try { + const ticket = await getVerificationTicket(token); - await fullReview.save(); + if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - const newReviews = student.reviews - ? student.reviews.concat([fullReview._id]) - : [fullReview._id]; - await Students.updateOne( - { netId }, - { $set: { reviews: newReviews } }, - ).exec(); + if (ticket.hd === 'cornell.edu') { + // insert the user into the collection if not already present - return { resCode: 1, errMsg: '' }; - } catch (error) { - // eslint-disable-next-line no-console - console.log(error); - return { resCode: 1, error: 'Unexpected error when adding review' }; - } - } else { + const insertUser = await insertUserCallback({ googleObject: ticket }); + if (insertUser === 0) { + res.status(500).json({ + error: 'There was an error inserting the user into the database.', + }); + } + const netId = ticket.email.replace('@cornell.edu', ''); + const student = await Students.findOne({ netId }); + + const related = await Reviews.find({ class: classId }); + if (related.find((v) => v.text === review.text)) { + res.status(400).json({ + message: + 'Review is a duplicate of an already existing review for this class!', + }); + } + + try { + // Attempt to insert the review + const fullReview = new Reviews({ + _id: shortid.generate(), + text: review.text, + difficulty: review.difficulty, + rating: review.rating, + workload: review.workload, + class: classId, + date: new Date(), + visible: 0, + reported: 0, + professors: review.professors, + likes: 0, + isCovid: review.isCovid, + user: student._id, + grade: review.grade, + major: review.major, + }); + + await fullReview.save(); + + const newReviews = student.reviews + ? student.reviews.concat([fullReview._id]) + : [fullReview._id]; + await Students.updateOne( + { netId }, + { $set: { reviews: newReviews } }, + ).exec(); + + res + .status(200) + .json({ + message: `Successfully inserted review with id ${fullReview._id} into database`, + }); + } catch (error) { // eslint-disable-next-line no-console - console.log('Error: non-Cornell email attempted to insert review'); - return { - resCode: 1, - error: 'Error: non-Cornell email attempted to insert review', - }; + console.log(error); + res.status(500).json({ error: 'Unexpected error when adding review' }); } - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'insert' method"); + } else { // eslint-disable-next-line no-console - console.log(error); - return { resCode: 1, error: "Error: at 'insert' method" }; + console.log('Error: non-Cornell email attempted to insert review'); + res.status(400).json({ + error: 'Error: non-Cornell email attempted to insert review', + }); } - }, -}; + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'insert' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: "Error: at 'insert' method" }); + } +}); /** * Updates a like on a review. If the student has already liked the review, * removes the like, and if the student has not, adds a like. */ -export const updateLiked: Endpoint = { - guard: [body('id').notEmpty().isAscii(), body('token').notEmpty().isAscii()], - callback: async (ctx: Context, request: ReviewRequest) => { - const { token } = request; - try { - let review = await getReviewById(request.id); - - const ticket = await getVerificationTicket(token); - - if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - - if (ticket.hd === 'cornell.edu') { - await insertUserCallback({ googleObject: ticket }); - const netId = ticket.email.replace('@cornell.edu', ''); - const student = await Students.findOne({ netId }); - - // removing like - if ( - student.likedReviews !== undefined - && student.likedReviews.includes(review.id) - ) { - await Students.updateOne( - { netId }, - { $pull: { likedReviews: review.id } }, - ); - - if (review.likes === undefined) { - await updateReviewLiked(request.id, 0, student.netId); - } else { - // bound the rating at 0 - await updateReviewLiked( - request.id, - review.likes - 1, - student.netId, - ); - } - } else { - // adding like - await Students.updateOne( - { netId: student.netId }, - { $push: { likedReviews: review.id } }, - ); - - if (review.likes === undefined) { - await Reviews.updateOne( - { _id: request.id }, - { $set: { likes: 1 }, $push: { likedBy: student.id } }, - ).exec(); - } else { - await Reviews.updateOne( - { _id: request.id }, - { - $set: { likes: review.likes + 1 }, - $push: { likedBy: student.id }, - }, - ).exec(); - } - } +router.post("/updateLiked", async (req, res) => { + const { token, id } = req.body as ReviewRequest; + try { + let review = await getReviewById(id); - review = await Reviews.findOne({ _id: request.id }).exec(); + const ticket = await getVerificationTicket(token); - return { resCode: 0, review: sanitizeReview(review) }; - } - return { - resCode: 1, - error: 'Error: non-Cornell email attempted to insert review', - }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'incrementLike' method"); - // eslint-disable-next-line no-console - console.log(error); - return { resCode: 1 }; - } - }, -}; - -export const userHasLiked: Endpoint = { - guard: [body('id').notEmpty().isAscii(), body('token').notEmpty().isAscii()], - callback: async (ctx: Context, request: ReviewRequest) => { - const { token } = request; - try { - const review = await Reviews.findOne({ _id: request.id }).exec(); - const ticket = await getVerificationTicket(token); - - if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; - - if (ticket.hd !== 'cornell.edu') { - return { - resCode: 1, - error: 'Error: non-Cornell email attempted to insert review', - }; + if (!ticket) return { resCode: 1, error: 'Missing verification ticket' }; + + if (ticket.hd === 'cornell.edu') { + const insertUser = await insertUserCallback({ googleObject: ticket }); + if (insertUser === 0) { + res.status(500).json({ error: "There was an error inserting new user." }); } - await insertUserCallback({ googleObject: ticket }); const netId = ticket.email.replace('@cornell.edu', ''); const student = await Students.findOne({ netId }); - if (student.likedReviews && student.likedReviews.includes(review.id)) { - return { resCode: 0, hasLiked: true }; + // removing like + if ( + student.likedReviews !== undefined + && student.likedReviews.includes(review.id) + ) { + await Students.updateOne( + { netId }, + { $pull: { likedReviews: review.id } }, + ); + + if (review.likes === undefined) { + await updateReviewLiked(id, 0, student.netId); + } else { + // bound the rating at 0 + await updateReviewLiked(id, review.likes - 1, student.netId); + } + } else { + // adding like + await Students.updateOne( + { netId: student.netId }, + { $push: { likedReviews: review.id } }, + ); + + if (review.likes === undefined) { + await Reviews.updateOne( + { _id: id }, + { $set: { likes: 1 }, $push: { likedBy: student.id } }, + ).exec(); + } else { + await Reviews.updateOne( + { _id: id }, + { + $set: { likes: review.likes + 1 }, + $push: { likedBy: student.id }, + }, + ).exec(); + } } - return { resCode: 0, hasLiked: false }; - } catch (error) { - // eslint-disable-next-line no-console - console.log("Error: at 'decrementLike' method"); - // eslint-disable-next-line no-console - console.log(error); - return { resCode: 1 }; + review = await Reviews.findOne({ _id: id }).exec(); + + res.status(200).json({ + message: `Successfully updated review with id ${id}`, + data: sanitizeReview(review), + }); + } + + res.status(400).json({ + error: 'Error: non-Cornell email attempted to insert review', + }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'incrementLike' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ message: "Internal server error" }); + } +}); +router.post("/userHasLiked", async (req, res) => { + const { id, token } = req.body as ReviewRequest; + try { + const review = await Reviews.findOne({ _id: id }).exec(); + const ticket = await getVerificationTicket(token); + + if (!ticket) res.status(400).json({ error: 'Missing verification ticket' }); + + if (ticket.hd !== 'cornell.edu') { + res.status(400).json({ + error: 'Error: non-Cornell email attempted to insert review', + }); } - }, -}; + + const insertUser = await insertUserCallback({ googleObject: ticket }); + if (insertUser === 0) { + res.status(500).json({ error: "Error occurred while attempting to create new user." }); + } + + const netId = ticket.email.replace('@cornell.edu', ''); + const student = await Students.findOne({ netId }); + + let hasLiked = false; + if (student.likedReviews && student.likedReviews.includes(review.id)) { + hasLiked = true; + } + + res + .status(200) + .json({ + message: `Retrieved whether student with netId: ${netId} has liked review with id ${id}`, + hasLiked, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.log("Error: at 'decrementLike' method"); + // eslint-disable-next-line no-console + console.log(error); + res.status(500).json({ error: "Internal server error." }); + } +}); + +export default router; diff --git a/server/src/server.ts b/server/src/server.ts index ad55c5b53..bd8e7062f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -8,6 +8,7 @@ import dotenv from "dotenv"; import auth from "./auth/auth.router"; import profile from "./profile/profile.router"; import search from "./search/search.router"; +import review from "./review/review.router"; import mongoose from "./utils/mongoose"; @@ -20,6 +21,7 @@ app.use(cors()); app.use("/api/auth", auth); app.use('/api/profile', profile); app.use('/api/search', search); +app.use('/api/review', review); function setup() { const port = process.env.PORT || 8080; From 1deccaaab14ab4eeaf56e6631a6131002386473d Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:25:07 -0500 Subject: [PATCH 29/33] try and fix search --- .../modules/Results/Components/Results.jsx | 34 +++++++++------ .../Results/Components/ResultsDisplay.jsx | 42 ++++++++++--------- server/src/search/search.router.ts | 9 ++-- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/client/src/modules/Results/Components/Results.jsx b/client/src/modules/Results/Components/Results.jsx index 8dcf6a8ed..4fd86b3f9 100644 --- a/client/src/modules/Results/Components/Results.jsx +++ b/client/src/modules/Results/Components/Results.jsx @@ -71,21 +71,29 @@ export class Results extends Component { axios .post(`/v2/getClassesByQuery`, { query: userQuery }) .then((response) => { - const queryCourseList = response.data.result - if (queryCourseList.length !== 0) { - // Save the Class object that matches the request - this.setState({ - courseList: queryCourseList, - loading: false, - }) - } else { - this.setState({ - courseList: [], - loading: false, - }) + if (response.status === 200) { + const queryCourseList = response.data.result + if (queryCourseList.length !== 0) { + // Save the Class object that matches the request + this.setState({ + courseList: queryCourseList, + loading: false, + }) + } else { + this.setState({ + courseList: [], + loading: false, + }) + } } }) - .catch((e) => console.log('Getting courses failed!')) + .catch((e) => { + this.setState({ + courseList: [], + loading: false, + }) + console.log('Getting courses failed!') + }) } } diff --git a/client/src/modules/Results/Components/ResultsDisplay.jsx b/client/src/modules/Results/Components/ResultsDisplay.jsx index 523fa84ff..f5f5d6538 100644 --- a/client/src/modules/Results/Components/ResultsDisplay.jsx +++ b/client/src/modules/Results/Components/ResultsDisplay.jsx @@ -231,26 +231,28 @@ export default class ResultsDisplay extends Component { ? this.state.filteredItems : this.state.courseList - return items.map((result, index) => ( -
{ - if (this.computeHeight() < 992) { - this.props.history.push( - `/course/${result?.classSub?.toUpperCase()}/${result?.classNum}` - ) - } - }} - > - -
- )) + if (items.error === undefined | null) { + return items.map((result, index) => ( +
{ + if (this.computeHeight() < 992) { + this.props.history.push( + `/course/${result?.classSub?.toUpperCase()}/${result?.classNum}` + ) + } + }} + > + +
+ )) + } } renderCheckboxes(group) { diff --git a/server/src/search/search.router.ts b/server/src/search/search.router.ts index 5043e6a4a..a86fb594b 100644 --- a/server/src/search/search.router.ts +++ b/server/src/search/search.router.ts @@ -17,18 +17,17 @@ router.post('/getClassesByQuery', async (req, res) => { { sort: { score: { $meta: 'textScore' } } }, ).exec(); if (classes && classes.length > 0) { - res.status(200).json({ + return res.status(200).json({ message: `Successfully retrieved classes with query ${query}`, data: classes.sort(courseSort(query)), }); } - const regexClasses = await regexClassesSearch(query); + // const regexClasses = await regexClassesSearch(query); res - .status(200) + .status(400) .json({ - message: `Successfully retrieved classes with query ${query} using regex`, - data: regexClasses, + error: `Error retrieved classes with query ${query} using regex`, }); } catch (error) { // eslint-disable-next-line no-console From 1601fe915ae049883488ff3b312db363a82f055d Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:27:14 -0500 Subject: [PATCH 30/33] fix returns --- server/src/review/review.router.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/src/review/review.router.ts b/server/src/review/review.router.ts index 072115018..951092962 100644 --- a/server/src/review/review.router.ts +++ b/server/src/review/review.router.ts @@ -77,7 +77,7 @@ router.post('/getReviewsByCourseId', async (req, res) => { const crossListOR = getCrossListOR(course); const reviews = await getReviewsByCourse(crossListOR); - res.status(200).json({ + return res.status(200).json({ message: `Successfully retrieved reviews by course id: ${courseId}`, data: reviews, }); @@ -104,7 +104,7 @@ router.post('/insertUser', async (req, res) => { const insertUserRequest = req.body as InsertUserRequest; const result = await insertUserCallback(insertUserRequest); if (result === 1) { - res.status(200).json({ message: 'User successfully added!' }); + return res.status(200).json({ message: 'User successfully added!' }); } res.status(500).json({ @@ -131,7 +131,7 @@ router.post('/insertReview', async (req, res) => { const insertUser = await insertUserCallback({ googleObject: ticket }); if (insertUser === 0) { - res.status(500).json({ + return res.status(500).json({ error: 'There was an error inserting the user into the database.', }); } @@ -140,7 +140,7 @@ router.post('/insertReview', async (req, res) => { const related = await Reviews.find({ class: classId }); if (related.find((v) => v.text === review.text)) { - res.status(400).json({ + return res.status(400).json({ message: 'Review is a duplicate of an already existing review for this class!', }); @@ -176,7 +176,7 @@ router.post('/insertReview', async (req, res) => { { $set: { reviews: newReviews } }, ).exec(); - res + return res .status(200) .json({ message: `Successfully inserted review with id ${fullReview._id} into database`, @@ -184,7 +184,7 @@ router.post('/insertReview', async (req, res) => { } catch (error) { // eslint-disable-next-line no-console console.log(error); - res.status(500).json({ error: 'Unexpected error when adding review' }); + return res.status(500).json({ error: 'Unexpected error when adding review' }); } } else { // eslint-disable-next-line no-console @@ -218,7 +218,7 @@ router.post("/updateLiked", async (req, res) => { if (ticket.hd === 'cornell.edu') { const insertUser = await insertUserCallback({ googleObject: ticket }); if (insertUser === 0) { - res.status(500).json({ error: "There was an error inserting new user." }); + return res.status(500).json({ error: "There was an error inserting new user." }); } const netId = ticket.email.replace('@cornell.edu', ''); @@ -265,7 +265,7 @@ router.post("/updateLiked", async (req, res) => { review = await Reviews.findOne({ _id: id }).exec(); - res.status(200).json({ + return res.status(200).json({ message: `Successfully updated review with id ${id}`, data: sanitizeReview(review), }); @@ -291,14 +291,14 @@ router.post("/userHasLiked", async (req, res) => { if (!ticket) res.status(400).json({ error: 'Missing verification ticket' }); if (ticket.hd !== 'cornell.edu') { - res.status(400).json({ + return res.status(400).json({ error: 'Error: non-Cornell email attempted to insert review', }); } const insertUser = await insertUserCallback({ googleObject: ticket }); if (insertUser === 0) { - res.status(500).json({ error: "Error occurred while attempting to create new user." }); + return res.status(500).json({ error: "Error occurred while attempting to create new user." }); } const netId = ticket.email.replace('@cornell.edu', ''); @@ -309,7 +309,7 @@ router.post("/userHasLiked", async (req, res) => { hasLiked = true; } - res + return res .status(200) .json({ message: `Retrieved whether student with netId: ${netId} has liked review with id ${id}`, From b6ede16668a04b7d5ca2447a3d1e1b4cb241d92c Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:28:47 -0500 Subject: [PATCH 31/33] fix returns --- server/src/auth/auth.router.ts | 2 +- server/src/profile/profile.router.ts | 8 ++++---- server/src/review/review.router.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/auth/auth.router.ts b/server/src/auth/auth.router.ts index ec4ecaf51..426c0d90c 100644 --- a/server/src/auth/auth.router.ts +++ b/server/src/auth/auth.router.ts @@ -13,7 +13,7 @@ router.post('/isAdmin', async (req, res) => { const verify = await verifyAdminToken(adminRequest.token); if (verify === false) { - res.status(400).json({ error: `Unable to verify token: ${adminRequest.token} as an admin.` }); + return res.status(400).json({ error: `Unable to verify token: ${adminRequest.token} as an admin.` }); } res.status(200).json({ message: `Token: ${adminRequest.token} was successfully verified as an admin user.` }); diff --git a/server/src/profile/profile.router.ts b/server/src/profile/profile.router.ts index 909b89b95..20b754fcf 100644 --- a/server/src/profile/profile.router.ts +++ b/server/src/profile/profile.router.ts @@ -12,7 +12,7 @@ router.post('/getStudentEmailByToken', async (req, res) => { const email = await getUserEmail(token); if (!email) { - res.status(400).json({ message: `Email not found: ${email}` }); + return res.status(400).json({ message: `Email not found: ${email}` }); } res.status(200).json({ message: email }); @@ -33,7 +33,7 @@ router.post('/countReviewsByStudentId', async (req, res) => { try { const studentDoc = await getUserByNetId(netId); if (studentDoc === null) { - res.status(404).json({ + return res.status(404).json({ message: `Unable to find student with netId: ${netId}`, }); } @@ -58,7 +58,7 @@ router.post('/getTotalLikesByStudentId', async (req, res) => { try { const studentDoc = await getUserByNetId(netId); if (studentDoc === null) { - res.status(404).json({ + return res.status(404).json({ message: `Unable to find student with netId: ${netId}`, }); } @@ -94,7 +94,7 @@ router.post('/getReviewsbyStudentId', async (req, res) => { try { const studentDoc = await getUserByNetId(netId); if (studentDoc === null) { - res.status(404).json({ + return res.status(404).json({ message: `Unable to find student with netId: ${netId}`, }); } diff --git a/server/src/review/review.router.ts b/server/src/review/review.router.ts index 951092962..2cf24c27c 100644 --- a/server/src/review/review.router.ts +++ b/server/src/review/review.router.ts @@ -34,7 +34,7 @@ router.post('/getCourseById', async (req, res) => { const { courseId } = req.body as CourseIdQuery; try { const course = await getCourseByIdCallback(courseId); - res.status(200).json({ + return res.status(200).json({ message: `Successfully retrieved course by id ${courseId}`, data: course, }); @@ -53,7 +53,7 @@ router.post('/getCourseByInfo', async (req, res) => { const { number, subject } = req.body as ClassByInfoQuery; try { const course = await getClassByInfo(subject, number); - res.status(200).json({ + return res.status(200).json({ message: `Successfully retrieved course by info number: ${number} and subject: ${subject}`, data: course, }); @@ -62,7 +62,7 @@ router.post('/getCourseByInfo', async (req, res) => { console.log("Error: at 'getCourseByInfo' endpoint"); // eslint-disable-next-line no-console console.log(error); - res.status(500).json({ error: 'Internal Server Error' }); + return res.status(500).json({ error: 'Internal Server Error' }); } }); From 513143edd5718589902eb0fd94411e46a07f9b96 Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:35:45 -0500 Subject: [PATCH 32/33] reorg --- server/{src => }/data/Classes.ts | 2 +- server/{src => }/data/Reviews.ts | 2 +- server/{src => }/data/Students.ts | 2 +- server/src/admin/AdminActions.ts | 2 +- server/src/auth/auth.controller.ts | 2 +- server/src/profile/profile.router.ts | 4 ++-- server/src/review/review.controller.ts | 2 +- server/src/review/review.router.ts | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) rename server/{src => }/data/Classes.ts (94%) rename server/{src => }/data/Reviews.ts (96%) rename server/{src => }/data/Students.ts (96%) diff --git a/server/src/data/Classes.ts b/server/data/Classes.ts similarity index 94% rename from server/src/data/Classes.ts rename to server/data/Classes.ts index 9d9ee99ed..f3e71df8c 100644 --- a/server/src/data/Classes.ts +++ b/server/data/Classes.ts @@ -1,4 +1,4 @@ -import { Classes } from "../../db/dbDefs"; +import { Classes } from "../db/dbDefs"; // eslint-disable-next-line import/prefer-default-export export const getCourseById = async (courseId: string) => { diff --git a/server/src/data/Reviews.ts b/server/data/Reviews.ts similarity index 96% rename from server/src/data/Reviews.ts rename to server/data/Reviews.ts index 854fe9edf..dfc28cdcf 100644 --- a/server/src/data/Reviews.ts +++ b/server/data/Reviews.ts @@ -1,4 +1,4 @@ -import { ReviewDocument, Reviews } from "../../db/dbDefs"; +import { ReviewDocument, Reviews } from "../db/dbDefs"; export const getReviewById = async (reviewId: string) => { try { diff --git a/server/src/data/Students.ts b/server/data/Students.ts similarity index 96% rename from server/src/data/Students.ts rename to server/data/Students.ts index e0a58535b..dbb2ce6d3 100644 --- a/server/src/data/Students.ts +++ b/server/data/Students.ts @@ -1,4 +1,4 @@ -import { Students } from "../../db/dbDefs"; +import { Students } from "../db/dbDefs"; // Get a user with this netId from the Users collection in the local database export const getUserByNetId = async (netId: string) => { diff --git a/server/src/admin/AdminActions.ts b/server/src/admin/AdminActions.ts index e656db15c..ab80d1eb5 100644 --- a/server/src/admin/AdminActions.ts +++ b/server/src/admin/AdminActions.ts @@ -9,7 +9,7 @@ import { resetProfessorArray, } from "../../db/dbInit"; import { verifyAdminToken } from "../auth/auth.controller"; -import { getCourseById } from "../data/Classes"; +import { getCourseById } from "../../data/Classes"; import { ReviewRequest } from "../review/review.dto"; import { AdminReviewRequest, AdminProfessorsRequest, AdminRaffleWinnerRequest } from "./admin.dto"; diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 6e6c0bf92..a8741f542 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -1,5 +1,5 @@ import { OAuth2Client } from 'google-auth-library'; -import { getUserByNetId } from '../data/Students'; +import { getUserByNetId } from '../../data/Students'; import { googleAudience } from '../utils/const'; /** diff --git a/server/src/profile/profile.router.ts b/server/src/profile/profile.router.ts index 20b754fcf..c32053932 100644 --- a/server/src/profile/profile.router.ts +++ b/server/src/profile/profile.router.ts @@ -1,8 +1,8 @@ import express from 'express'; import { ProfileRequest, NetIdQuery } from './profile.dto'; import { getUserEmail } from '../auth/auth.controller'; -import { getUserByNetId, getStudentReviewIds } from '../data/Students'; -import { getNonNullReviews } from '../data/Reviews'; +import { getUserByNetId, getStudentReviewIds } from '../../data/Students'; +import { getNonNullReviews } from '../../data/Reviews'; const router = express.Router(); diff --git a/server/src/review/review.controller.ts b/server/src/review/review.controller.ts index 33abb63fc..934d79fa9 100644 --- a/server/src/review/review.controller.ts +++ b/server/src/review/review.controller.ts @@ -1,7 +1,7 @@ import { ValidationChain, body } from 'express-validator'; import shortid from 'shortid'; import { InsertUserRequest } from './review.dto'; -import { getUserByNetId, saveUser } from '../data/Students'; +import { getUserByNetId, saveUser } from '../../data/Students'; /** * Creates a ValidationChain[] where the json object denoted by [jsonFieldName] diff --git a/server/src/review/review.router.ts b/server/src/review/review.router.ts index 2cf24c27c..cd7946b0f 100644 --- a/server/src/review/review.router.ts +++ b/server/src/review/review.router.ts @@ -7,7 +7,7 @@ import { getVerificationTicket } from '../auth/auth.controller'; import { getCourseById as getCourseByIdCallback, getClassByInfo, -} from '../data/Classes'; +} from '../../data/Classes'; import { insertUser as insertUserCallback, } from './review.controller'; @@ -16,7 +16,7 @@ import { updateReviewLiked, sanitizeReview, getReviewsByCourse, -} from '../data/Reviews'; +} from '../../data/Reviews'; import { CourseIdQuery, InsertReviewRequest, From c6fedec4280b9aacf4bc48263d42f4d6493897ca Mon Sep 17 00:00:00 2001 From: Michelle Li Date: Sun, 26 Nov 2023 00:37:38 -0500 Subject: [PATCH 33/33] adjustments --- server/src/server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index bd8e7062f..ba7121ebc 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -11,6 +11,7 @@ import search from "./search/search.router"; import review from "./review/review.router"; import mongoose from "./utils/mongoose"; +import { configure } from "./endpoints"; dotenv.config(); const app = express(); @@ -23,6 +24,9 @@ app.use('/api/profile', profile); app.use('/api/search', search); app.use('/api/review', review); +// deprecate configure soon +configure(app); + function setup() { const port = process.env.PORT || 8080; app.get("*", (_, response) => response.sendFile(path.join(__dirname, "../../client/build/index.html")));