Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ready for Review] Fix report abuse #380

Merged
merged 13 commits into from
Dec 7, 2024
71 changes: 59 additions & 12 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ app.post('/api/new-review', authenticate, async (req, res) => {
if (review.overallRating === 0 || review.reviewText === '') {
res.status(401).send('Error: missing fields');
}
doc.set({ ...review, date: new Date(review.date), likes: 0, status: 'PENDING' });
doc.set({ ...review, date: new Date(review.date), likes: 0, status: 'PENDING', reports: [] });
res.status(201).send(doc.id);
} catch (err) {
console.error(err);
Expand All @@ -95,13 +95,23 @@ app.post('/api/edit-review/:reviewId', authenticate, async (req, res) => {
res.status(401).send('Error: user is not the review owner. not authorized');
return;
}
// Don't allow edits if review is reported
if (reviewData.status === 'REPORTED') {
res.status(403).send('Error: cannot edit a reported review');
return;
}
const updatedReview = req.body as Review;
if (updatedReview.overallRating === 0 || updatedReview.reviewText === '') {
res.status(401).send('Error: missing fields');
return;
}
reviewDoc
.update({ ...updatedReview, date: new Date(updatedReview.date), status: 'PENDING' })
.update({
...updatedReview,
date: new Date(updatedReview.date),
status: 'PENDING',
reports: reviewData.reports || [],
})
.then(() => {
res.status(201).send(reviewId);
});
Expand Down Expand Up @@ -744,12 +754,15 @@ app.post('/api/remove-saved-landlord', authenticate, saveLandlordHandler(false))
*
* Permissions:
* - An admin can update a review from any status to any status
* - A regular user can only update their own reviews from any status to deleted
* - A regular user can only update their own reviews to deleted
* - A regular user can report any review except:
* - Their own reviews
* - Pending reviews
* - A regular user cannot update other users' reviews
*
* @param reviewDocId - The document ID of the review to update
* @param newStatus - The new status to set for the review
* - must be one of 'PENDING', 'APPROVED', 'DECLINED', or 'DELETED'
* - must be one of 'PENDING', 'APPROVED', 'DECLINED', 'DELETED', or 'REPORTED'
* @returns status 200 if successfully updates status,
* 400 if the new status is invalid,
* 401 if authentication fails,
Expand All @@ -761,28 +774,62 @@ app.put('/api/update-review-status/:reviewDocId/:newStatus', authenticate, async
const { reviewDocId, newStatus } = req.params; // Extracting parameters from the URL
const { uid, email } = req.user;
const isAdmin = email && admins.includes(email);
const statusList = ['PENDING', 'APPROVED', 'DECLINED', 'DELETED'];
const statusList = ['PENDING', 'APPROVED', 'DECLINED', 'DELETED', 'REPORTED'];

try {
// Validating if the new status is within the allowed list
if (!statusList.includes(newStatus)) {
res.status(400).send('Invalid status type');
return;
}

const reviewDoc = reviewCollection.doc(reviewDocId);
const reviewData = (await reviewDoc.get()).data();
const currentStatus = reviewData?.status || '';
const reviewOwnerId = reviewData?.userId || '';
// Check if user is authorized to change this review's status
if (!isAdmin && (uid !== reviewOwnerId || newStatus !== 'DELETED')) {

// Check if user is authorized to change this review's status:
// Admin: Can change the status of a review to any status
// Non-review owner: Can report a review
// Review owner: Can update or delete their own review if the review is not already REPORTED.
if (
!isAdmin &&
(uid !== reviewOwnerId || newStatus !== 'DELETED') &&
newStatus !== 'REPORTED'
) {
res.status(403).send('Unauthorized');
return;
}
// Updating the review's status in Firestore
await reviewDoc.update({ status: newStatus });

if (newStatus === 'REPORTED') {
// Check if user is trying to report their own review
if (uid === reviewOwnerId) {
res.status(403).send('Cannot report your own review');
return;
}

if (currentStatus === 'PENDING') {
res.status(403).send('Cannot report a pending review');
return;
}

// Updating the review's status in Firestore and adding a report
const existingReports = reviewData?.reports || [];
const newReport = {
date: new Date(),
userId: uid,
reason: 'No reason provided',
};
await reviewDoc.update({ status: newStatus, reports: [...existingReports, newReport] });
} else {
// Updating the review's status in Firestore
await reviewDoc.update({ status: newStatus });
}
res.status(200).send('Success'); // Sending a success response
/* If firebase successfully updates status to approved, then send an email
to the review's creator to inform them that their review has been approved */
if (newStatus === 'APPROVED' && currentStatus !== 'APPROVED') {

/* If firebase successfully updates status to approved (not from an ignored report),
then send an email to the review's creator to inform them that their review has been approved */
if (newStatus === 'APPROVED' && !['REPORTED', 'APPROVED'].includes(currentStatus)) {
// get user id
const reviewData = (await reviewCollection.doc(reviewDocId).get()).data();
const userId = reviewData?.userId;
Expand Down
9 changes: 8 additions & 1 deletion common/types/db-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export type DetailedRating = {
readonly conditions: number;
};

type ReportEntry = {
readonly date: Date;
readonly userId: string;
readonly reason: string;
};

export type Review = {
readonly aptId: string | null;
readonly likes?: number;
Expand All @@ -24,8 +30,9 @@ export type Review = {
readonly overallRating: number;
readonly photos: readonly string[];
readonly reviewText: string;
readonly status?: 'PENDING' | 'APPROVED' | 'DECLINED' | 'DELETED';
readonly status?: 'PENDING' | 'APPROVED' | 'DECLINED' | 'DELETED' | 'REPORTED';
readonly userId?: string | null;
readonly reports?: readonly ReportEntry[];
};

export type ReviewWithId = Review & Id;
Expand Down
85 changes: 60 additions & 25 deletions frontend/src/components/Admin/AdminReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ type Props = {
readonly review: ReviewWithId;
/** Function to toggle the display. */
readonly setToggle: React.Dispatch<React.SetStateAction<boolean>>;
/** Indicates if the review is in the declined section. */
readonly declinedSection: boolean;
/** Indicates whether to show decline button. */
readonly showDecline?: boolean;
/** Indicates whether to show delete button. */
readonly showDelete?: boolean;
/** Indicates whether to show ignore button for reported reviews. */
readonly showIgnore?: boolean;
/** Indicates whether to show approve button for pending reviews. */
readonly showApprove?: boolean;
/** Function to trigger the photo carousel. */
readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void;
};
Expand Down Expand Up @@ -94,15 +100,29 @@ const useStyles = makeStyles(() => ({
}));

/**
* AdminReviewComponent displays an individual review for admin approval or deletion.
* AdminReviewComponent - Displays a review card with approval/deletion controls for administrators.
*
* @param review review - The review to approve
* @returns The rendered component.
* @remarks
* Renders a review's details including ratings, text, photos, and apartment info along with
* buttons for admins to approve, decline or delete the review. Fetches associated apartment
* and landlord data on mount.
*
* @param {ReviewWithId} props.review - The review object containing all review data to display
* @param {React.Dispatch<React.SetStateAction<boolean>>} props.setToggle - Function to control visibility of the review
* @param {boolean} props.showDecline - Optional flag to show decline button, defaults to false
* @param {boolean} props.showDelete - Optional flag to show delete button, defaults to false
* @param {boolean} props.showIgnore - Optional flag to show ignore button, defaults to false
* @param {boolean} props.showApprove - Optional flag to show approve button, defaults to false
*
* @returns {ReactElement} - A Material-UI Card component containing the review details and admin controls
*/
const AdminReviewComponent = ({
review,
setToggle,
declinedSection,
showDecline = false,
showDelete = false,
showIgnore = false,
showApprove = false,
triggerPhotoCarousel,
}: Props): ReactElement => {
const { detailedRatings, overallRating, bedrooms, price, date, reviewText, photos } = review;
Expand Down Expand Up @@ -242,7 +262,7 @@ const AdminReviewComponent = ({

<CardActions>
<Grid container spacing={2} alignItems="center" justifyContent="flex-end">
{declinedSection && (
{showDelete && (
<Grid item>
<Button
onClick={() => changeStatus('DELETED')}
Expand All @@ -253,24 +273,39 @@ const AdminReviewComponent = ({
</Button>
</Grid>
)}
<Grid item>
<Button
onClick={() => changeStatus('DECLINED')}
variant="outlined"
style={{ color: colors.red1 }}
>
<strong>Decline</strong>
</Button>
</Grid>
<Grid item>
<Button
onClick={() => changeStatus('APPROVED')}
variant="contained"
style={{ backgroundColor: colors.green1 }}
>
<strong>Approve</strong>
</Button>
</Grid>
{showDecline && (
<Grid item>
<Button
onClick={() => changeStatus('DECLINED')}
variant="outlined"
style={{ color: colors.red1 }}
>
<strong>Decline</strong>
</Button>
</Grid>
)}
{showIgnore && (
<Grid item>
<Button
onClick={() => changeStatus('APPROVED')}
variant="outlined"
style={{ color: colors.gray2 }}
>
<strong>Ignore</strong>
</Button>
</Grid>
)}
{showApprove && (
<Grid item>
<Button
onClick={() => changeStatus('APPROVED')}
variant="contained"
style={{ backgroundColor: colors.green1 }}
>
<strong>Approve</strong>
</Button>
</Grid>
)}
</Grid>
</CardActions>
</Card>
Expand Down
Loading
Loading