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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,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 @@ -91,13 +91,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 @@ -721,11 +731,12 @@ 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 report any review
* - 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 @@ -737,25 +748,60 @@ 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')) {
// Only admins can change the status of a review to anything other than DELETED or REPORTED
// Regular users can only change the status of their own reviews to DELETED
// Or they can report a review
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;
}

// Check if review is in PENDING state
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') {
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
32 changes: 20 additions & 12 deletions frontend/src/components/Admin/AdminReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ type Props = {
/** Function to toggle the display. */
readonly setToggle: React.Dispatch<React.SetStateAction<boolean>>;
/** Indicates if the review is in the declined section. */
readonly declinedSection: boolean;
readonly showDecline?: boolean;
readonly showDelete?: boolean;
};

/**
Expand Down Expand Up @@ -87,7 +88,12 @@ const useStyles = makeStyles(() => ({
* @param review review - The review to approve
* @returns The rendered component.
*/
const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): ReactElement => {
const AdminReviewComponent = ({
review,
setToggle,
showDecline = false,
showDelete = false,
}: Props): ReactElement => {
const { detailedRatings, overallRating, bedrooms, price, date, reviewText, photos } = review;
const formattedDate = format(new Date(date), 'MMM dd, yyyy').toUpperCase();
const { root, dateText, bedroomsPriceText, ratingInfo, photoStyle, photoRowStyle } = useStyles();
Expand Down Expand Up @@ -223,7 +229,7 @@ const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): Re

<CardActions>
<Grid container spacing={2} alignItems="center" justifyContent="flex-end">
{declinedSection && (
{showDelete && (
<Grid item>
<Button
onClick={() => changeStatus('DELETED')}
Expand All @@ -234,15 +240,17 @@ const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): Re
</Button>
</Grid>
)}
<Grid item>
<Button
onClick={() => changeStatus('DECLINED')}
variant="outlined"
style={{ color: colors.red1 }}
>
<strong>Decline</strong>
</Button>
</Grid>
{showDecline && (
<Grid item>
<Button
onClick={() => changeStatus('DECLINED')}
variant="outlined"
style={{ color: colors.red1 }}
>
<strong>Decline</strong>
</Button>
</Grid>
)}
<Grid item>
<Button
onClick={() => changeStatus('APPROVED')}
Expand Down
106 changes: 88 additions & 18 deletions frontend/src/components/Review/Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Props = {
setToggle: React.Dispatch<React.SetStateAction<boolean>>;
readonly triggerEditToast: () => void;
readonly triggerDeleteToast: () => void;
readonly triggerReportToast: () => void;
user: firebase.User | null;
setUser: React.Dispatch<React.SetStateAction<firebase.User | null>>;
readonly showLabel: boolean;
Expand Down Expand Up @@ -188,6 +189,7 @@ const useStyles = makeStyles(() => ({
* @param {React.Dispatch<React.SetStateAction<boolean>>} props.setToggle - Function to toggle a state.
* @param {function} props.triggerEditToast - function to trigger a toast notification on edit.
* @param {function} props.triggerDeleteToast - function to trigger a toast notification on delete.
* @param {function} props.triggerReportToast - function to trigger a toast notification on report.
* @param {firebase.User | null} props.user - The current logged-in user.
* @param {React.Dispatch<React.SetStateAction<firebase.User | null>>} props.setUser - Function to set the current user.
* @param {boolean} props.showLabel - Indicates if the property or landlord label should be shown.
Expand All @@ -202,6 +204,7 @@ const ReviewComponent = ({
setToggle,
triggerEditToast,
triggerDeleteToast,
triggerReportToast,
user,
setUser,
showLabel,
Expand Down Expand Up @@ -237,6 +240,7 @@ const ReviewComponent = ({
const [landlordData, setLandlordData] = useState<Landlord>();
const [reviewOpen, setReviewOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [reportModalOpen, setReportModalOpen] = useState(false);
const isMobile = useMediaQuery('(max-width:600px)');
const isSmallScreen = useMediaQuery('(max-width:391px)');
const toastTime = 3500;
Expand Down Expand Up @@ -293,6 +297,48 @@ const ReviewComponent = ({
);
};

const reportModal = () => {
return (
<Dialog
open={reportModalOpen}
onClose={() => {
handleReportModalClose(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperProps={{ style: { borderRadius: '12px' } }}
>
<DialogTitle className={deleteDialogTitle} id="alert-dialog-title">
Report this review?
</DialogTitle>
<DialogContent>
<DialogContentText className={deleteDialogDesc} id="alert-dialog-description">
This review will be sent to admins for review.
</DialogContentText>
</DialogContent>
<DialogActions className={deleteDialogActions}>
<Button
className={hollowRedButton}
onClick={() => {
handleReportModalClose(false);
}}
>
Cancel
</Button>
<Button
className={submitButton}
onClick={() => {
handleReportModalClose(true);
}}
autoFocus
>
Report
</Button>
</DialogActions>
</Dialog>
);
};

const Modals = (
<>
<ReviewModal
Expand All @@ -308,6 +354,7 @@ const ReviewComponent = ({
initialReview={reviewData}
/>
{deleteModal()}
{reportModal()}
</>
);
const handleExpandClick = () => {
Expand Down Expand Up @@ -348,15 +395,32 @@ const ReviewComponent = ({
setDeleteModalOpen(true);
};

const reportAbuseHandler = async (reviewId: string) => {
const endpoint = `/api/update-review-status/${reviewData.id}/PENDING`;
if (user) {
const token = await user.getIdToken(true);
await axios.put(endpoint, {}, createAuthHeaders(token));
setToggle((cur) => !cur);
} else {
let user = await getUser(true);
setUser(user);
const handleReportModalClose = async (report: Boolean) => {
try {
if (report) {
if (!reviewData.id) {
console.error('No review ID found');
return;
}

const endpoint = `/api/update-review-status/${reviewData.id}/REPORTED`;

if (user) {
const token = await user.getIdToken(true);
const headers = createAuthHeaders(token);

await axios.put(endpoint, {}, headers);
setToggle((cur) => !cur);
} else {
let user = await getUser(true);
setUser(user);
}
if (triggerReportToast) triggerReportToast();
}
} catch (error: any) {
console.error('Error reporting review:', error.response?.data || error.message);
} finally {
setReportModalOpen(false);
}
};

Expand Down Expand Up @@ -496,6 +560,15 @@ const ReviewComponent = ({
</Grid>
);
};
const reportAbuseButton = () => {
return (
<Grid item>
<Button onClick={() => setReportModalOpen(true)} className={button} size="small">
Report Abuse
</Button>
</Grid>
);
};
return (
<Card className={root} variant="outlined">
<Box minHeight="200px">
Expand Down Expand Up @@ -533,6 +606,7 @@ const ReviewComponent = ({
{user &&
reviewData.userId &&
user.uid === reviewData.userId &&
reviewData.status !== 'REPORTED' &&
editDeleteButtons()}
</Grid>
{useMediaQuery(
Expand Down Expand Up @@ -598,15 +672,11 @@ const ReviewComponent = ({
Helpful {`(${reviewData.likes || 0})`}
</Button>
</Grid>
<Grid item>
<Button
onClick={() => reportAbuseHandler(reviewData.id)}
className={button}
size="small"
>
Report Abuse
</Button>
</Grid>
{user &&
reviewData.userId &&
reviewData.userId !== user?.uid &&
reviewData.status !== 'PENDING' &&
reportAbuseButton()}
</Grid>
</CardActions>
{Modals}
Expand Down
Loading
Loading