diff --git a/app/lib/admin/backend.dart b/app/lib/admin/backend.dart index e045bbd682..8dee6ae5fb 100644 --- a/app/lib/admin/backend.dart +++ b/app/lib/admin/backend.dart @@ -929,4 +929,16 @@ class AdminBackend { _logger.info('Deleting moderated user: ${user.userId}'); } } + + /// Whether the [ModerationCase] has been appealed by [email] already. + Future isModerationCaseAppealedByEmail({ + required String caseId, + required String email, + }) async { + final query = dbService.query() + ..filter('appealedCaseId =', caseId); + final list = await query.run().toList(); + final emails = list.map((mc) => mc.reporterEmail).toSet(); + return emails.contains(email); + } } diff --git a/app/lib/frontend/handlers/report.dart b/app/lib/frontend/handlers/report.dart index ef5ff4b4f7..c8c700450c 100644 --- a/app/lib/frontend/handlers/report.dart +++ b/app/lib/frontend/handlers/report.dart @@ -206,6 +206,17 @@ Future processReportPageHandler( final isAppeal = form.caseId != null; + if (isAppeal) { + final appealExists = await adminBackend.isModerationCaseAppealedByEmail( + caseId: form.caseId!, + email: userEmail!, + ); + if (appealExists) { + throw InvalidInputException( + 'You have previously appealed this incident, we are unable to accept another appeal.'); + } + } + bool isSubjectOwner = false; if (user != null) { if (subject.isPackage) { diff --git a/app/test/frontend/handlers/report_test.dart b/app/test/frontend/handlers/report_test.dart index 8e8a908adf..aee9c3ab26 100644 --- a/app/test/frontend/handlers/report_test.dart +++ b/app/test/frontend/handlers/report_test.dart @@ -363,11 +363,13 @@ void main() { ); }); - testWithProfile('unauthenticated appeal success', fn: () async { + testWithProfile('unauthenticated appeal success, second appeal fails', + fn: () async { await _prepareApplied( logSubject: 'package-version:oxygen/1.2.0', ); + // first report: success await withHttpPubApiClient( fn: (client) async { final msg = await client.postReport(ReportForm( @@ -396,6 +398,22 @@ void main() { expect(mc.isSubjectOwner, false); }, ); + + // second report: rejected + await withHttpPubApiClient(fn: (client) async { + await expectApiException( + client.postReport(ReportForm( + email: 'user2@pub.dev', + subject: 'package-version:oxygen/1.2.0', + caseId: 'case/1', + message: 'Huston, we have a problem.', + )), + code: 'InvalidInput', + status: 400, + message: + 'You have previously appealed this incident, we are unable to accept another appeal.', + ); + }); }); testWithProfile('authenticated appeal success', fn: () async {