-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathreviews.ts
320 lines (291 loc) · 9.56 KB
/
reviews.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/**
@module ReviewRoute
*/
import express, { Request } from 'express';
import { ObjectId } from 'mongodb';
import { ReviewData, VoteData } from '../types/types';
import { verifyCaptcha } from '../helpers/recaptcha';
import Review from '../models/review';
import Vote from '../models/vote';
import Report from '../models/report';
const router = express.Router();
/**
* Get review scores
*/
interface ScoresQuery {
type: 'course' | 'professor';
id: string;
}
router.get('/scores', async function (req: Request<never, unknown, never, ScoresQuery>, res) {
// match filters all reviews with given field
// group aggregates by field
let matchField = '';
let groupField = '';
if (req.query.type == 'professor') {
matchField = 'professorID';
groupField = '$courseID';
} else if (req.query.type == 'course') {
matchField = 'courseID';
groupField = '$professorID';
}
// execute aggregation on the reviews collection
const aggreg = await Review.aggregate([
{ $match: { [matchField]: req.query.id } },
{ $group: { _id: groupField, score: { $avg: '$rating' } } },
]);
// returns the results in an array
const array = aggreg as ReviewData[];
// rename _id to name
const results = array.map((v) => {
return { name: v._id, score: v.score };
});
res.json(results);
});
/**
* Get featured review
*/
interface FeaturedQuery {
type: 'course' | 'professor';
id: string;
}
router.get('/featured', async function (req: Request<never, unknown, never, FeaturedQuery>, res) {
// search by professor or course field
let field = '';
if (req.query.type == 'course') {
field = 'courseID';
} else if (req.query.type == 'professor') {
field = 'professorID';
}
// find first review with the highest score
const reviewsCollection = await Review.find({ [field]: req.query.id })
.sort({ score: -1 })
.limit(1);
if (reviewsCollection) {
res.json(reviewsCollection);
} else {
res.json([]);
}
});
interface ReviewFilter {
courseID: string;
professorID: string;
userID: string;
_id?: ObjectId;
verified?: boolean;
}
/**
* Query reviews
*/
router.get('/', async function (req, res) {
const courseID = req.query.courseID as string;
const professorID = req.query.professorID as string;
const userID = req.query.userID as string;
const reviewID = req.query.reviewID as string;
const verified = req.query.verified as string;
const query: ReviewFilter = {
courseID,
professorID,
userID,
_id: reviewID === undefined ? undefined : new ObjectId(reviewID),
verified: verified === undefined ? undefined : verified === 'true' ? true : false,
};
// remove null params
for (const param in query) {
if (query[param as keyof ReviewFilter] === null || query[param as keyof ReviewFilter] === undefined) {
delete query[param as keyof ReviewFilter];
}
}
const reviews = await Review.find(query);
if (reviews) {
res.json(reviews);
} else {
res.json([]);
}
});
/**
* Add a review
*/
router.post('/', async function (req, res) {
if (req.session.passport) {
//^ this should be a middleware check smh
// check if user is trusted
const verifiedCount = await Review.find({
userID: req.session.passport.user.id,
verified: true,
})
.countDocuments()
.exec();
// Set on server so the client can't automatically verify their own review.
req.body.verified = verifiedCount >= 3; // auto-verify if use has posted 3+ reviews
// Verify the captcha
const verifyResponse = await verifyCaptcha(req.body);
if (!verifyResponse?.success)
return res.status(400).json({ error: 'ReCAPTCHA token is invalid', data: verifyResponse });
delete req.body.captchaToken; // so it doesn't get stored in DB
//check if review already exists for same professor, course, and user
const query: ReviewFilter = {
courseID: req.body.courseID,
professorID: req.body.professorID,
userID: req.session.passport.user.id,
};
const reviews = await Review.find(query);
if (reviews?.length > 0)
return res.status(400).json({ error: 'Review already exists for this professor and course!' });
// add review to mongo
req.body.userDisplay =
req.body.userDisplay === 'Anonymous Peter' ? 'Anonymous Peter' : req.session.passport.user.name;
req.body.userID = req.session.passport.user.id;
await new Review(req.body).save();
// echo back body
res.json(req.body);
} else {
res.json({ error: 'Must be logged in to add a review!' });
}
});
/**
* Delete a review
*/
router.delete('/', async (req, res) => {
const checkUser = async () => {
return await Review.findOne({ _id: req.body.id as string, userID: req.session.passport?.user.id }).exec();
};
if (req.session.passport?.admin || (await checkUser())) {
await Review.deleteOne({ _id: req.body.id });
await Vote.deleteMany({ reviewID: req.body.id });
await Report.deleteMany({ reviewID: req.body.id });
res.status(200).send();
} else {
res.json({ error: 'Must be an admin or review author to delete reviews!' });
}
});
/**
* Upvote or downvote a review
*/
router.patch('/vote', async function (req, res) {
if (req.session?.passport != null) {
//get id and delta score from initial vote
const id = req.body['id'];
let deltaScore = req.body['upvote'] ? 1 : -1;
//query to search for a vote matching the same review and user
const currentVotes = {
userID: req.session.passport.user.id,
reviewID: id,
};
//either length 1 or 0 array(ideally) 0 if no existing vote, 1 if existing vote
const existingVote = (await Vote.find(currentVotes)) as VoteData[];
//check if there is an existing vote and it has the same vote as the previous vote
if (existingVote.length != 0 && deltaScore == existingVote[0].score) {
//remove the vote
res.json({ deltaScore: -1 * deltaScore });
//delete the existing vote from the votes collection
await Vote.deleteMany(currentVotes);
//update the votes document with a lowered score
await Review.updateOne({ _id: id }, { $inc: { score: -1 * deltaScore } });
} else if (existingVote.length != 0 && deltaScore != existingVote[0].score) {
//there is an existing vote but the vote was different
deltaScore *= 2;
//*2 to reverse the old vote and implement the new one
await Review.updateOne({ _id: id }, { $inc: { score: deltaScore } });
//override old vote with new data
await Vote.updateOne({ _id: existingVote[0]._id }, { $set: { score: deltaScore / 2 } });
res.json({ deltaScore: deltaScore });
} else {
//no old vote, just add in new vote data
console.log(`Voting Review ${id} with delta ${deltaScore}`);
await Review.updateOne({ _id: id }, { $inc: { score: deltaScore } });
//sends in vote
await new Vote({ userID: req.session.passport.user.id, reviewID: id, score: deltaScore }).save();
res.json({ deltaScore: deltaScore });
}
}
//
});
/**
* Get whether or not the color of a button should be colored
*/
router.patch('/getVoteColor', async function (req, res) {
//make sure user is logged in
if (req.session?.passport != null) {
//query of the user's email and the review id
const query = {
userID: req.session.passport.user.email,
reviewID: req.body['id'],
};
//get any existing vote in the db
const existingVote = (await Vote.find(query)) as VoteData[];
//result an array of either length 1 or empty
if (existingVote.length == 0) {
//if empty, both should be uncolored
res.json([false, false]);
} else {
//if not empty, there is a vote, so color it accordingly
if (existingVote[0].score == 1) {
res.json([true, false]);
} else {
res.json([false, true]);
}
}
}
});
/**
* Get multiple review colors
*/
router.patch('/getVoteColors', async function (req, res) {
if (req.session?.passport != null) {
//query of the user's email and the review id
const ids: string[] = req.body.ids;
const votes = await Vote.find({ userID: req.session.passport.user.id, reviewID: { $in: ids } });
const r: { [key: string]: number } = votes.reduce((acc: { [key: string]: number }, v) => {
acc[v.reviewID.toString()] = v.score;
return acc;
}, {});
res.json(r);
} else {
res.json({});
}
});
/*
* Verify a review
*/
router.patch('/verify', async function (req, res) {
if (req.session.passport?.admin) {
console.log(`Verifying review ${req.body.id}`);
const status = await Review.updateOne({ _id: req.body.id }, { verified: true });
res.json(status);
} else {
res.json({ error: 'Must be an admin to verify reviews!' });
}
});
/**
* Clear all reviews
*/
router.delete('/clear', async function (req, res) {
if (process.env.NODE_ENV != 'production') {
const status = await Review.deleteMany({});
res.json(status);
} else {
res.json({ error: 'Can only clear on development environment' });
}
});
/**
* Updating the review
*/
router.patch('/updateReview', async function (req, res) {
if (req.session.passport) {
const updatedReviewBody = req.body;
const query = {
_id: new ObjectId(req.body._id),
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _id, ...updateWithoutId } = updatedReviewBody;
await updateDocument(COLLECTION_NAMES.REVIEWS, query, { $set: updateWithoutId });
const responseWithId = {
_id: query._id,
...updateWithoutId,
};
res.json(responseWithId);
} else {
res.status(401).json({ error: 'Must be logged in to update a review.' });
}
});
export default router;