Feat/report evidence upload - #16
KatlehoMadaba wants to merge 5 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds evidence file upload support for driver reports (backend storage service, DB migration, controller validation, DTOs) and introduces an OCR-based driver info extraction feature with a new endpoint and frontend integration. The ChangesReport Evidence Upload
Estimated code review effort: 3 (Moderate) | ~30 minutes Driver Info Extraction via OCR
Estimated code review effort: 4 (Complex) | ~45 minutes Also included: a minor refactor in Sequence Diagram(s)sequenceDiagram
participant ReportsController
participant CreateReportCommandHandler
participant LocalFileStorageService
participant Report
ReportsController->>CreateReportCommandHandler: CreateReportCommand(Evidence)
loop each evidence file
CreateReportCommandHandler->>LocalFileStorageService: SaveAsync(stream, fileName)
LocalFileStorageService-->>CreateReportCommandHandler: file URL
end
CreateReportCommandHandler->>Report: set EvidenceUrls
sequenceDiagram
participant ReportDriverPage
participant verificationApi
participant VerificationController
participant ExtractDriverInfoCommandHandler
participant OcrService
ReportDriverPage->>verificationApi: extractDriverInfo(files)
verificationApi->>VerificationController: POST /api/verification/extract
VerificationController->>ExtractDriverInfoCommandHandler: ExtractDriverInfoCommand(streams)
ExtractDriverInfoCommandHandler->>OcrService: run OCR on images
OcrService-->>ExtractDriverInfoCommandHandler: OcrResult(s)
ExtractDriverInfoCommandHandler-->>VerificationController: ExtractDriverInfoResult
VerificationController-->>verificationApi: driverName, registrationNumber, phoneNumber
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/RydrSafe.Application/Features/Reports/Queries/GetReportByIdQuery.cs (1)
17-21: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftRestrict evidence URLs on report details.
GetByIdis available to any authenticated user, so returningEvidenceUrlshere lets one user retrieve another user's report image links. Since those files are served publicly from/evidence/reports/*, this exposes the evidence directly; scope report detail access to the owner or moderator/admins and keep the files off public static serving.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.Application/Features/Reports/Queries/GetReportByIdQuery.cs` around lines 17 - 21, Restrict report detail access in GetReportByIdQuery so EvidenceUrls are not returned to any authenticated user. Update the GetById mapping in ReportDto creation to omit or conditionally populate EvidenceUrls based on ownership or moderator/admin role, and ensure the ReportDto/GetById flow only exposes evidence links when the caller is authorized. Also remove public static serving for /evidence/reports/* so the files are no longer directly accessible without authorization.
🧹 Nitpick comments (5)
backend/RydrSafe.Infrastructure/Persistence/Repositories/VerificationHistoryRepository.cs (1)
28-35: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winThree round-trips instead of one for stats aggregation.
Total,Flagged, andSafeare now computed via three sequential awaitedCountAsynccalls over the same filtered set, versus a single grouped query previously. Note that these calls can't be parallelized withTask.WhenAllsince a singleDbContextisn't safe for concurrent operations — the round-trips are strictly sequential.A single query with conditional aggregation avoids the extra round-trips while still returning a valid
(0,0,0)result for empty inputs (avoiding theGroupBy-by-constant empty-source edge case the earlierFirstOrDefaultAsyncnull-coalescing was likely working around):♻️ Proposed single-query alternative
public async Task<(int Total, int Flagged, int Safe)> GetStatsByUserIdAsync(Guid userId) { - var baseQuery = db.VerificationHistories.Where(v => v.UserId == userId); - var total = await baseQuery.CountAsync(); - var flagged = await baseQuery.CountAsync(v => v.Status == "Flagged" || v.Status == "HighRisk"); - var safe = await baseQuery.CountAsync(v => v.Status == "Safe"); - return (total, flagged, safe); + var stats = await db.VerificationHistories + .Where(v => v.UserId == userId) + .GroupBy(v => 1) + .Select(g => new + { + Total = g.Count(), + Flagged = g.Count(v => v.Status == "Flagged" || v.Status == "HighRisk"), + Safe = g.Count(v => v.Status == "Safe") + }) + .FirstOrDefaultAsync(); + + return (stats?.Total ?? 0, stats?.Flagged ?? 0, stats?.Safe ?? 0); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.Infrastructure/Persistence/Repositories/VerificationHistoryRepository.cs` around lines 28 - 35, `VerificationHistoryRepository.GetStatsByUserIdAsync` is doing three sequential `CountAsync` queries over the same filtered set, causing unnecessary round-trips. Replace the separate counts with a single query that computes `Total`, `Flagged`, and `Safe` via conditional aggregation in one database call, and ensure it still returns `(0, 0, 0)` when there are no matching rows. Use the existing `baseQuery`/`db.VerificationHistories` path and keep the `GetStatsByUserIdAsync` return shape unchanged.backend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.cs (1)
19-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSequential OCR calls + unused
cancellationToken.Lines 21-27 call
ocrService.ExtractAsyncup to 3 times sequentially, each an unmitigated external HTTP call (Google Vision, per the downstreamOcrServiceimplementation). Running them concurrently viaTask.WhenAllwould reduce latency and request-thread occupancy. Additionally, the handler'scancellationTokenparameter (Line 19) is never used anywhere in the method — not passed to OCR calls, nor to the repository lookups (Lines 35, 38, 49) — so a cancelled/disconnected client won't stop in-flight work.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.cs` around lines 19 - 27, The ExtractDriverInfoCommand.Handle method is doing up to three OCR HTTP calls one after another and never uses the provided cancellationToken. Refactor the OCR extraction flow to start the Image1/Image2/Image3 extraction tasks together and await them with Task.WhenAll, then merge the results in the same Handle path. Also thread cancellationToken through every async dependency used by Handle, including ocrService.ExtractAsync and the downstream repository lookups, so cancelled requests stop work promptly.backend/RydrSafe.Infrastructure/Migrations/20260701000000_AddEvidenceUrlsToReport.cs (1)
13-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider
jsonbinstead oftextfor a JSON-backed column.The
EvidenceUrlscolumn stores serialized JSON (seeAppDbContext.csconversion) but is typedtext. On PostgreSQL,jsonbvalidates JSON on write and enables native query/indexing support at negligible cost, whereastextaccepts any string silently. This is consistent across the migration, designer, and snapshot files, so switching would need to happen in all three plusAppDbContext.cs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.Infrastructure/Migrations/20260701000000_AddEvidenceUrlsToReport.cs` around lines 13 - 19, The EvidenceUrls column is stored as serialized JSON but is currently created as text in the migration, so update the Report migration to use PostgreSQL jsonb instead and keep the schema consistent with AppDbContext’s conversion. Make the same type change in the migration, designer, and model snapshot entries that define Reports.EvidenceUrls so the database schema matches the JSON-backed property and supports native JSON validation/querying.backend/RydrSafe.API/Controllers/ReportsController.cs (1)
19-38: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winExtension-only validation is spoofable.
Only the file extension is checked; content is never inspected (e.g., magic-byte/signature check). A malicious actor can rename any file to
.jpg/.png/.webpand have it stored and later served as a static file under/evidence/reports/. Consider validating actual image content (e.g., checking file signatures or decoding via an image library) before persisting.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.API/Controllers/ReportsController.cs` around lines 19 - 38, The evidence validation in ReportsController.Create only checks Path.GetExtension, so spoofed uploads can pass as allowed images. Update the file validation flow in Create to verify actual image content before adding streams to evidenceStreams, using a signature/magic-byte check or image decoding in addition to the existing size and extension checks. Keep the current AllowedExtensions and MaxEvidenceFileSize guards, but reject files whose content does not match a real jpg, jpeg, png, or webp image before they are persisted or served.backend/RydrSafe.Infrastructure/Services/LocalFileStorageService.cs (1)
11-20: 🗄️ Data Integrity & Integration | 🔵 TrivialNo cleanup path for orphaned files on downstream failure.
Per the
CreateReportCommandHandlersnippet, evidence is saved to disk viaSaveAsyncbefore theReportentity is persisted. If persistence fails afterward, the uploaded files remain orphaned on disk with no compensating delete. Consider adding aDeleteAsync/cleanup method toIFileStorageService(or wrapping the handler's evidence-save + persist steps so failures trigger cleanup).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/RydrSafe.Infrastructure/Services/LocalFileStorageService.cs` around lines 11 - 20, The evidence save flow in LocalFileStorageService.SaveAsync can leave orphaned files if later persistence fails in CreateReportCommandHandler. Add a cleanup path by introducing a DeleteAsync (or equivalent) on IFileStorageService and implement it in LocalFileStorageService using the stored path/name. Then update the report creation flow in CreateReportCommandHandler so any exception after saving evidence triggers cleanup of the previously saved files before rethrowing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/RydrSafe.API/Program.cs`:
- Line 103: The static file middleware in Program and the evidence-serving flow
are exposing sensitive evidence publicly without access checks. Replace
app.UseStaticFiles() for evidence assets with an authenticated controller
endpoint that streams files only after authorization, and ensure the evidence
path is no longer directly web-accessible. Use the existing GetReportById flow
and the evidence file handling code to locate where URLs are generated and
served, and move that access into an authenticated, permission-checked action
instead of raw static hosting.
In
`@backend/RydrSafe.Application/Features/Reports/Commands/CreateReportCommand.cs`:
- Around line 66-82: The evidence upload flow in CreateReportCommand should be
made atomic so partially uploaded files are not left behind if a later SaveAsync
or report save fails. Update the evidence handling around request.Evidence,
fileStorageService.SaveAsync, and the Report creation path so any successful
uploads are cleaned up on exception, or defer uploads to a retryable step that
does not depend on the initial report transaction. Ensure the
CreateReportCommand and its report persistence path leave no orphaned files when
the operation fails.
In
`@backend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.cs`:
- Around line 33-52: The merge logic in ExtractDriverInfoCommand is
inconsistent: driverName is always replaced from the repository while
phoneNumber only falls back, and the second lookup still checks ocr.PhoneNumber
instead of the enriched local phoneNumber. Update the extraction flow so
driverName/phoneNumber use a consistent precedence policy in the vehicle/driver
match block, and change the fallback driver lookup to use the current
phoneNumber value after enrichment. Keep the fix within ExtractDriverInfoCommand
and the driverRepository/vehicleRepository lookup flow.
In `@backend/RydrSafe.Infrastructure/Persistence/AppDbContext.cs`:
- Around line 58-63: Add a ValueComparer for the mutable EvidenceUrls list
mapping in AppDbContext so EF Core detects in-place changes to the JSON-backed
property. Update the property configuration on EvidenceUrls to include a
comparer alongside the existing HasConversion/HasColumnType setup, using the
same serialization semantics to compare, hash, and snapshot the List<string>
values correctly.
In `@frontend/src/api/reports.ts`:
- Around line 19-26: The create method in reports.ts is overriding the multipart
Content-Type header, which prevents the runtime from adding the required
boundary. Update apiClient.post in the create flow to send the FormData without
manually setting headers, so the browser/runtime can generate the correct
multipart request for Report creation.
In `@frontend/src/api/verification.ts`:
- Around line 37-44: In extractDriverInfo, the multipart request is setting
Content-Type manually, which prevents the runtime from adding the required
boundary. Remove the headers override from the apiClient.post call, or switch
this method to use postForm like the upload path, so FormData is encoded
correctly for /api/verification/extract.
In `@frontend/src/pages/passenger/ReportDriverPage.tsx`:
- Around line 79-87: The addFiles helper in ReportDriverPage silently truncates
valid uploads when more than 3 images are selected, so update the file handling
to notify users when the 3-image cap is exceeded. In addFiles, before or while
applying setEvidenceFiles, detect any valid files that would be dropped by the
slice(0, 3) limit and show a toast explaining that only 3 images can be
attached; keep the existing ACCEPTED and MAX_SIZE checks intact.
- Around line 1-17: The tabs type in ReportDriverPage is referencing
React.ReactNode without importing the React namespace, which breaks under
react-jsx. Update the react import in ReportDriverPage to import ReactNode as a
type, then replace the React.ReactNode annotation used in the tabs definition
with ReactNode so the component stays type-safe without relying on a React
namespace import.
---
Outside diff comments:
In `@backend/RydrSafe.Application/Features/Reports/Queries/GetReportByIdQuery.cs`:
- Around line 17-21: Restrict report detail access in GetReportByIdQuery so
EvidenceUrls are not returned to any authenticated user. Update the GetById
mapping in ReportDto creation to omit or conditionally populate EvidenceUrls
based on ownership or moderator/admin role, and ensure the ReportDto/GetById
flow only exposes evidence links when the caller is authorized. Also remove
public static serving for /evidence/reports/* so the files are no longer
directly accessible without authorization.
---
Nitpick comments:
In `@backend/RydrSafe.API/Controllers/ReportsController.cs`:
- Around line 19-38: The evidence validation in ReportsController.Create only
checks Path.GetExtension, so spoofed uploads can pass as allowed images. Update
the file validation flow in Create to verify actual image content before adding
streams to evidenceStreams, using a signature/magic-byte check or image decoding
in addition to the existing size and extension checks. Keep the current
AllowedExtensions and MaxEvidenceFileSize guards, but reject files whose content
does not match a real jpg, jpeg, png, or webp image before they are persisted or
served.
In
`@backend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.cs`:
- Around line 19-27: The ExtractDriverInfoCommand.Handle method is doing up to
three OCR HTTP calls one after another and never uses the provided
cancellationToken. Refactor the OCR extraction flow to start the
Image1/Image2/Image3 extraction tasks together and await them with Task.WhenAll,
then merge the results in the same Handle path. Also thread cancellationToken
through every async dependency used by Handle, including ocrService.ExtractAsync
and the downstream repository lookups, so cancelled requests stop work promptly.
In
`@backend/RydrSafe.Infrastructure/Migrations/20260701000000_AddEvidenceUrlsToReport.cs`:
- Around line 13-19: The EvidenceUrls column is stored as serialized JSON but is
currently created as text in the migration, so update the Report migration to
use PostgreSQL jsonb instead and keep the schema consistent with AppDbContext’s
conversion. Make the same type change in the migration, designer, and model
snapshot entries that define Reports.EvidenceUrls so the database schema matches
the JSON-backed property and supports native JSON validation/querying.
In
`@backend/RydrSafe.Infrastructure/Persistence/Repositories/VerificationHistoryRepository.cs`:
- Around line 28-35: `VerificationHistoryRepository.GetStatsByUserIdAsync` is
doing three sequential `CountAsync` queries over the same filtered set, causing
unnecessary round-trips. Replace the separate counts with a single query that
computes `Total`, `Flagged`, and `Safe` via conditional aggregation in one
database call, and ensure it still returns `(0, 0, 0)` when there are no
matching rows. Use the existing `baseQuery`/`db.VerificationHistories` path and
keep the `GetStatsByUserIdAsync` return shape unchanged.
In `@backend/RydrSafe.Infrastructure/Services/LocalFileStorageService.cs`:
- Around line 11-20: The evidence save flow in LocalFileStorageService.SaveAsync
can leave orphaned files if later persistence fails in
CreateReportCommandHandler. Add a cleanup path by introducing a DeleteAsync (or
equivalent) on IFileStorageService and implement it in LocalFileStorageService
using the stored path/name. Then update the report creation flow in
CreateReportCommandHandler so any exception after saving evidence triggers
cleanup of the previously saved files before rethrowing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f90b3bb-de3c-47ad-889a-4a6fd1682225
📒 Files selected for processing (20)
backend/RydrSafe.API/Controllers/ReportsController.csbackend/RydrSafe.API/Controllers/VerificationController.csbackend/RydrSafe.API/Program.csbackend/RydrSafe.Application/Common/Interfaces/IFileStorageService.csbackend/RydrSafe.Application/DTOs/ReportDtos.csbackend/RydrSafe.Application/Features/Reports/Commands/CreateReportCommand.csbackend/RydrSafe.Application/Features/Reports/Queries/GetReportByIdQuery.csbackend/RydrSafe.Application/Features/Reports/Queries/GetReportsQuery.csbackend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.csbackend/RydrSafe.Domain/Entities/Report.csbackend/RydrSafe.Infrastructure/DependencyInjection.csbackend/RydrSafe.Infrastructure/Migrations/20260701000000_AddEvidenceUrlsToReport.Designer.csbackend/RydrSafe.Infrastructure/Migrations/20260701000000_AddEvidenceUrlsToReport.csbackend/RydrSafe.Infrastructure/Migrations/AppDbContextModelSnapshot.csbackend/RydrSafe.Infrastructure/Persistence/AppDbContext.csbackend/RydrSafe.Infrastructure/Persistence/Repositories/VerificationHistoryRepository.csbackend/RydrSafe.Infrastructure/Services/LocalFileStorageService.csfrontend/src/api/reports.tsfrontend/src/api/verification.tsfrontend/src/pages/passenger/ReportDriverPage.tsx
| c.RoutePrefix = string.Empty; | ||
| }); | ||
|
|
||
| app.UseStaticFiles(); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Evidence files served publicly with no authentication/authorization.
UseStaticFiles() runs before UseAuthentication/UseAuthorization, and static file middleware performs no per-file access checks regardless of pipeline order. Evidence images (potentially containing driver/passenger PII) become world-accessible to anyone with the URL. Given GetReportById is available to any authenticated user (not scoped to owner/moderator), evidence URLs are exposed broadly and then further exposed unauthenticated on disk. Recommend serving evidence through an authenticated controller endpoint (streaming the file after an authorization check) instead of raw static file hosting, or restricting the static-files path to a dedicated, access-controlled route.
🔒 Sketch of an authenticated evidence endpoint
-app.UseStaticFiles();
+// Serve evidence only through an authorized endpoint instead of raw static hosting.
+// e.g. [Authorize] [HttpGet("api/reports/{id:guid}/evidence/{fileName}")] that
+// verifies the caller is permitted to view report {id} before streaming the file.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/RydrSafe.API/Program.cs` at line 103, The static file middleware in
Program and the evidence-serving flow are exposing sensitive evidence publicly
without access checks. Replace app.UseStaticFiles() for evidence assets with an
authenticated controller endpoint that streams files only after authorization,
and ensure the evidence path is no longer directly web-accessible. Use the
existing GetReportById flow and the evidence file handling code to locate where
URLs are generated and served, and move that access into an authenticated,
permission-checked action instead of raw static hosting.
| var evidenceUrls = new List<string>(); | ||
| if (request.Evidence is { Count: > 0 }) | ||
| { | ||
| foreach (var (data, fileName) in request.Evidence) | ||
| evidenceUrls.Add(await fileStorageService.SaveAsync(data, fileName)); | ||
| } | ||
|
|
||
| var report = new Report | ||
| { | ||
| DriverId = driver.Id, | ||
| UserId = request.UserId, | ||
| Category = Enum.Parse<ReportCategory>(request.Category), | ||
| Severity = Enum.Parse<ReportSeverity>(request.Severity), | ||
| Description = request.Description, | ||
| IncidentDate = DateTime.SpecifyKind(request.IncidentDate, DateTimeKind.Utc) | ||
| IncidentDate = DateTime.SpecifyKind(request.IncidentDate, DateTimeKind.Utc), | ||
| EvidenceUrls = evidenceUrls, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
set -e
cd /repo
git ls-files | rg 'CreateReportCommand\.cs|IFileStorageService|FileStorageService|Report|Driver|Vehicle'Repository: KatlehoMadaba/RydrSafe
Length of output: 211
🏁 Script executed:
set -e
pwd
printf '\n-- top-level --\n'
find . -maxdepth 2 -type f | sed 's#^\./##' | sort | head -200
printf '\n-- targeted files --\n'
git ls-files | rg 'CreateReportCommand\.cs|IFileStorageService|FileStorageService|Report|Driver|Vehicle'Repository: KatlehoMadaba/RydrSafe
Length of output: 3649
🏁 Script executed:
set -e
cd /home/jailuser/git
printf '\n-- CreateReportCommand outline --\n'
ast-grep outline backend/RydrSafe.Application/Features/Reports/Commands/CreateReportCommand.cs --view expanded
printf '\n-- IFileStorageService outline --\n'
ast-grep outline backend/RydrSafe.Application/Common/Interfaces/IFileStorageService.cs --view expanded
printf '\n-- LocalFileStorageService outline --\n'
ast-grep outline backend/RydrSafe.Infrastructure/Services/LocalFileStorageService.cs --view expanded
printf '\n-- ReportRepository outline --\n'
ast-grep outline backend/RydrSafe.Infrastructure/Persistence/Repositories/ReportRepository.cs --view expandedRepository: KatlehoMadaba/RydrSafe
Length of output: 650
🏁 Script executed:
set -e
cd /home/jailuser/git
for f in \
backend/RydrSafe.Application/Features/Reports/Commands/CreateReportCommand.cs \
backend/RydrSafe.Application/Common/Interfaces/IFileStorageService.cs \
backend/RydrSafe.Infrastructure/Services/LocalFileStorageService.cs \
backend/RydrSafe.Infrastructure/Persistence/Repositories/ReportRepository.cs \
backend/RydrSafe.Domain/Entities/Report.cs
do
echo "===== $f ====="
wc -l "$f"
cat -n "$f" | sed -n '1,240p'
echo
doneRepository: KatlehoMadaba/RydrSafe
Length of output: 9231
Handle evidence upload failures atomically
Saving evidence before the report can leave already-uploaded files orphaned if a later SaveAsync fails; the first-time driver/vehicle inserts are also already committed at that point. Wrap the upload sequence with cleanup on failure or move to a flow that tolerates/upload retries evidence separately.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/RydrSafe.Application/Features/Reports/Commands/CreateReportCommand.cs`
around lines 66 - 82, The evidence upload flow in CreateReportCommand should be
made atomic so partially uploaded files are not left behind if a later SaveAsync
or report save fails. Update the evidence handling around request.Evidence,
fileStorageService.SaveAsync, and the Report creation path so any successful
uploads are cleaned up on exception, or defer uploads to a retryable step that
does not depend on the initial report transaction. Ensure the
CreateReportCommand and its report persistence path leave no orphaned files when
the operation fails.
| if (!string.IsNullOrWhiteSpace(ocr.RegistrationNumber)) | ||
| { | ||
| var vehicle = await vehicleRepository.GetByRegistrationNumberAsync(ocr.RegistrationNumber); | ||
| if (vehicle is not null) | ||
| { | ||
| var driver = await driverRepository.GetByIdAsync(vehicle.DriverId); | ||
| if (driver is not null) | ||
| { | ||
| driverName = driver.DriverName; | ||
| phoneNumber ??= driver.PhoneNumber; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (driverName is null && !string.IsNullOrWhiteSpace(ocr.PhoneNumber)) | ||
| { | ||
| var driver = await driverRepository.GetByPhoneNumberAsync(ocr.PhoneNumber); | ||
| if (driver is not null) | ||
| driverName = driver.DriverName; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Inconsistent merge precedence: driverName always overwritten, phoneNumber only fallback-filled.
At Line 41, driverName = driver.DriverName; unconditionally replaces any OCR-extracted name once a vehicle/driver match is found, but Line 42 uses phoneNumber ??= driver.PhoneNumber; (fallback-only). This asymmetry means a valid OCR-derived name is always discarded in favor of the DB record while a valid OCR-derived phone is kept. If intentional (DB trusted over noisy OCR), consider applying the same precedence to phone for consistency; otherwise this may unintentionally erase a correct OCR name.
Separately, Line 47 checks ocr.PhoneNumber instead of the enriched local phoneNumber variable. If the vehicle lookup found a driver with no DriverName but did fill phoneNumber from driver.PhoneNumber (Line 42), this fallback lookup will incorrectly use the original (possibly null) ocr.PhoneNumber instead of the already-known phoneNumber, missing a legitimate lookup opportunity.
🐛 Suggested fix
- if (driverName is null && !string.IsNullOrWhiteSpace(ocr.PhoneNumber))
+ if (driverName is null && !string.IsNullOrWhiteSpace(phoneNumber))
{
- var driver = await driverRepository.GetByPhoneNumberAsync(ocr.PhoneNumber);
+ var driver = await driverRepository.GetByPhoneNumberAsync(phoneNumber);
if (driver is not null)
driverName = driver.DriverName;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!string.IsNullOrWhiteSpace(ocr.RegistrationNumber)) | |
| { | |
| var vehicle = await vehicleRepository.GetByRegistrationNumberAsync(ocr.RegistrationNumber); | |
| if (vehicle is not null) | |
| { | |
| var driver = await driverRepository.GetByIdAsync(vehicle.DriverId); | |
| if (driver is not null) | |
| { | |
| driverName = driver.DriverName; | |
| phoneNumber ??= driver.PhoneNumber; | |
| } | |
| } | |
| } | |
| if (driverName is null && !string.IsNullOrWhiteSpace(ocr.PhoneNumber)) | |
| { | |
| var driver = await driverRepository.GetByPhoneNumberAsync(ocr.PhoneNumber); | |
| if (driver is not null) | |
| driverName = driver.DriverName; | |
| } | |
| if (!string.IsNullOrWhiteSpace(ocr.RegistrationNumber)) | |
| { | |
| var vehicle = await vehicleRepository.GetByRegistrationNumberAsync(ocr.RegistrationNumber); | |
| if (vehicle is not null) | |
| { | |
| var driver = await driverRepository.GetByIdAsync(vehicle.DriverId); | |
| if (driver is not null) | |
| { | |
| driverName = driver.DriverName; | |
| phoneNumber ??= driver.PhoneNumber; | |
| } | |
| } | |
| } | |
| if (driverName is null && !string.IsNullOrWhiteSpace(phoneNumber)) | |
| { | |
| var driver = await driverRepository.GetByPhoneNumberAsync(phoneNumber); | |
| if (driver is not null) | |
| driverName = driver.DriverName; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/RydrSafe.Application/Features/Verification/Commands/ExtractDriverInfoCommand.cs`
around lines 33 - 52, The merge logic in ExtractDriverInfoCommand is
inconsistent: driverName is always replaced from the repository while
phoneNumber only falls back, and the second lookup still checks ocr.PhoneNumber
instead of the enriched local phoneNumber. Update the extraction flow so
driverName/phoneNumber use a consistent precedence policy in the vehicle/driver
match block, and change the fallback driver lookup to use the current
phoneNumber value after enrichment. Keep the fix within ExtractDriverInfoCommand
and the driverRepository/vehicleRepository lookup flow.
| e.Property(r => r.EvidenceUrls) | ||
| .HasConversion( | ||
| v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), | ||
| v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>()) | ||
| .HasColumnType("text") | ||
| .HasDefaultValueSql("'[]'"); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== AppDbContext slice ==\n'
sed -n '1,140p' backend/RydrSafe.Infrastructure/Persistence/AppDbContext.cs
printf '\n== Search EvidenceUrls ==\n'
rg -n "EvidenceUrls|ValueComparer|HasConversion\\(" backend -SRepository: KatlehoMadaba/RydrSafe
Length of output: 6162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== EvidenceUrls mutations ==\n'
rg -n "\.EvidenceUrls\s*\.(Add|Remove|Clear|Insert|AddRange|RemoveRange|Sort|Reverse)|EvidenceUrls\s*=" backend -S
printf '\n== Report entity ==\n'
sed -n '1,120p' backend/RydrSafe.Domain/Entities/Report.csRepository: KatlehoMadaba/RydrSafe
Length of output: 1038
Add a ValueComparer for EvidenceUrls. EF Core needs one for this mutable List<string> JSON property; without it, in-place edits like report.EvidenceUrls.Add(...) may not be detected on SaveChanges.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/RydrSafe.Infrastructure/Persistence/AppDbContext.cs` around lines 58
- 63, Add a ValueComparer for the mutable EvidenceUrls list mapping in
AppDbContext so EF Core detects in-place changes to the JSON-backed property.
Update the property configuration on EvidenceUrls to include a comparer
alongside the existing HasConversion/HasColumnType setup, using the same
serialization semantics to compare, hash, and snapshot the List<string> values
correctly.
| create: ({ evidence, ...fields }: CreateReportRequest) => { | ||
| const form = new FormData() | ||
| Object.entries(fields).forEach(([k, v]) => form.append(k, v as string)) | ||
| evidence?.forEach((f) => form.append('evidence', f)) | ||
| return apiClient | ||
| .post<Report>('/api/reports', form, { headers: { 'Content-Type': 'multipart/form-data' } }) | ||
| .then((r) => r.data) | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the report API file and the HTTP client wrapper.
git ls-files frontend/src/api/reports.ts frontend/src | sed -n '1,120p'
echo '--- reports.ts ---'
cat -n frontend/src/api/reports.ts
echo '--- apiClient candidates ---'
fd -a 'apiClient|client|axios' frontend/src | sed -n '1,120p'Repository: KatlehoMadaba/RydrSafe
Length of output: 3722
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- frontend/src/api/client.ts ---'
cat -n frontend/src/api/client.ts
echo '--- search for multipart / FormData handling ---'
rg -n "multipart/form-data|FormData|Content-Type|headers" frontend/src -g '*.ts' -g '*.tsx'Repository: KatlehoMadaba/RydrSafe
Length of output: 3305
🌐 Web query:
Axios browser FormData Content-Type multipart/form-data boundary docs
💡 Result:
In Axios, you should generally leave the Content-Type header unset when passing a FormData instance as the payload [1][2]. When you provide a FormData object, the browser, web worker, or runtime environment automatically sets the correct Content-Type header, including the necessary multipart boundary, ensuring the request is formatted correctly [1][3][2]. If you manually set the Content-Type header to multipart/form-data yourself, you may inadvertently prevent the runtime from adding the required boundary string, which is essential for the server to parse the multipart data [1][4][2]. Key behaviors and recommendations: - Automatic Handling: When using a FormData instance, Axios detects the payload type and allows the runtime to handle the header, which includes generating and appending the correct boundary [1][2]. - Shortcut Methods: Axios provides convenient shortcut methods—postForm, putForm, and patchForm—that are specifically designed for multipart/form-data uploads [5][3][6]. These methods internally handle the Content-Type header setup [5][7]. - Automatic Serialization: Since version 0.27.0, if you need to send a plain JavaScript object as multipart/form-data, you can set the Content-Type header to multipart/form-data. In this specific case, Axios will automatically serialize the object into a FormData instance for you [5][1][7]. - Node.js Compatibility: In Node.js environments, Axios often relies on the form-data package or global FormData to generate these headers [5][1][6]. If using custom FormData implementations that provide a getHeaders method, Axios will copy those headers automatically by default [1][2].
Citations:
- 1: https://github.com/axios/axios
- 2: https://github.com/axios/axios?tab=readme-ov-file
- 3: https://www.mintlify.com/axios/axios/advanced/form-data
- 4: FormData request header doesn't include boundary axios/axios#4631
- 5: https://github.com/axios/axios/blob/v1.15.0/docs/pages/advanced/multipart-form-data-format.md
- 6: https://github.com/axios/axios/blob/v1.15.0/docs/pages/advanced/file-posting.md
- 7: https://github.com/axios/axios-docs/blob/master/posts/en/multipart.md
Remove the manual multipart Content-Type header
FormData uploads need the runtime to add the multipart boundary. Setting Content-Type: multipart/form-data here suppresses that boundary, so report creation will fail to parse on the server.
🐛 Proposed fix
return apiClient
- .post<Report>('/api/reports', form, { headers: { 'Content-Type': 'multipart/form-data' } })
+ .post<Report>('/api/reports', form)
.then((r) => r.data)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| create: ({ evidence, ...fields }: CreateReportRequest) => { | |
| const form = new FormData() | |
| Object.entries(fields).forEach(([k, v]) => form.append(k, v as string)) | |
| evidence?.forEach((f) => form.append('evidence', f)) | |
| return apiClient | |
| .post<Report>('/api/reports', form, { headers: { 'Content-Type': 'multipart/form-data' } }) | |
| .then((r) => r.data) | |
| }, | |
| create: ({ evidence, ...fields }: CreateReportRequest) => { | |
| const form = new FormData() | |
| Object.entries(fields).forEach(([k, v]) => form.append(k, v as string)) | |
| evidence?.forEach((f) => form.append('evidence', f)) | |
| return apiClient | |
| .post<Report>('/api/reports', form) | |
| .then((r) => r.data) | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/api/reports.ts` around lines 19 - 26, The create method in
reports.ts is overriding the multipart Content-Type header, which prevents the
runtime from adding the required boundary. Update apiClient.post in the create
flow to send the FormData without manually setting headers, so the
browser/runtime can generate the correct multipart request for Report creation.
| extractDriverInfo: (files: File[]) => { | ||
| const form = new FormData() | ||
| files.forEach((f, i) => form.append(`image${i + 1}`, f)) | ||
| return apiClient | ||
| .post<{ driverName: string | null; registrationNumber: string | null; phoneNumber: string | null }>( | ||
| '/api/verification/extract', form, { headers: { 'Content-Type': 'multipart/form-data' } }) | ||
| .then((r) => r.data) | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
printf '\n== verification.ts ==\n'
sed -n '1,120p' frontend/src/api/verification.ts
printf '\n== locate apiClient ==\n'
rg -n "apiClient|create\\(" frontend/src/api -g '*.ts' -g '*.tsx'
printf '\n== files with default headers/interceptors ==\n'
rg -n "Content-Type|interceptor|axios.create|create\\({" frontend/src -g '*.ts' -g '*.tsx'Repository: KatlehoMadaba/RydrSafe
Length of output: 6420
🏁 Script executed:
set -euo pipefail
printf '\n== client.ts ==\n'
sed -n '1,220p' frontend/src/api/client.ts
printf '\n== verification endpoint search ==\n'
rg -n "/api/verification/(extract|upload)|IFormFile|FormData|multipart" . -g '*.cs' -g '*.ts' -g '*.tsx'Repository: KatlehoMadaba/RydrSafe
Length of output: 4024
🏁 Script executed:
set -euo pipefail
sed -n '1,180p' backend/RydrSafe.API/Controllers/VerificationController.csRepository: KatlehoMadaba/RydrSafe
Length of output: 3569
🌐 Web query:
axios FormData multipart boundary do not manually set Content-Type multipart/form-data browser docs
💡 Result:
When working with FormData in axios, you should not manually set the Content-Type header [1][2]. For browser, web worker, and React Native environments, you must leave the Content-Type header unset [1][2]. This allows the runtime to automatically calculate and append the correct multipart boundary required for the request to be processed correctly by the server [1][2][3]. If you manually set Content-Type to multipart/form-data, the boundary will be missing, causing the request to fail or be rejected by the server [4][5][6]. Axios provides convenient shortcut methods like postForm, putForm, and patchForm, which are designed to handle this process automatically [7][8][9]. Alternatively, when passing a FormData instance as the payload in standard request methods, axios will detect the payload type and handle the headers automatically [2][10].
Citations:
- 1: https://axios.rest/pages/advanced/multipart-form-data-format
- 2: https://github.com/axios/axios
- 3: fix: remove forced multipart/form-data placeholder header in postForm/putForm/patchForm axios/axios#10980
- 4: Axios fetch adapter: Missing boundary when Content-Type is set manually with FormData axios/axios#7054
- 5: Axios does not append form boundary for multipart requests in node environment axios/axios#5379
- 6: https://stackoverflow.com/questions/75742226/cant-set-content-type-header-for-axios-with-formdata
- 7: https://github.com/axios/axios/blob/v1.15.0/docs/pages/advanced/multipart-form-data-format.md
- 8: https://github.com/axios/axios-docs/blob/master/posts/en/multipart.md
- 9: https://www.mintlify.com/axios/axios/advanced/form-data
- 10: https://github.com/axios/axios?tab=readme-ov-file
Remove the manual multipart Content-Type header
FormData uploads need the runtime to add the multipart boundary. Setting 'Content-Type': 'multipart/form-data' here strips that boundary and can break /api/verification/extract (same issue as upload above). Remove the header override or switch to postForm so the request is encoded correctly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/api/verification.ts` around lines 37 - 44, In extractDriverInfo,
the multipart request is setting Content-Type manually, which prevents the
runtime from adding the required boundary. Remove the headers override from the
apiClient.post call, or switch this method to use postForm like the upload path,
so FormData is encoded correctly for /api/verification/extract.
| import { useEffect, useRef, useState } from 'react' | ||
| import { useLocation } from 'react-router-dom' | ||
| import { useForm } from 'react-hook-form' | ||
| import { zodResolver } from '@hookform/resolvers/zod' | ||
| import { z } from 'zod' | ||
| import { useMutation } from '@tanstack/react-query' | ||
| import { toast } from 'sonner' | ||
| import { Upload, PenLine, X, ScanSearch } from 'lucide-react' | ||
| import { reportsApi } from '@/api/reports' | ||
| import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' | ||
| import { verificationApi } from '@/api/verification' | ||
| import { Card, CardContent, CardHeader } from '@/components/ui/card' | ||
| import { Button } from '@/components/ui/button' | ||
| import { Input } from '@/components/ui/input' | ||
| import { Label } from '@/components/ui/label' | ||
| import { Textarea } from '@/components/ui/textarea' | ||
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' | ||
| import { cn } from '@/lib/utils' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check tsconfig for allowUmdGlobalAccess and existing bare React.* usage conventions
fd -e json tsconfig -x cat {}
rg -n 'allowUmdGlobalAccess' .
rg -nP "React\.(ReactNode|FC|ReactElement)" frontend/src --type=ts -g '!**/node_modules/**' -C2Repository: KatlehoMadaba/RydrSafe
Length of output: 2975
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant section of the file and confirm the surrounding type usage.
nl -ba frontend/src/pages/passenger/ReportDriverPage.tsx | sed -n '40,60p'
# Check whether the project opts into UMD global access anywhere in app tsconfig.
rg -n 'allowUmdGlobalAccess|jsxImportSource|react-jsx' tsconfig*.json frontend/src -g '!**/node_modules/**'
# Look for the same React namespace typing pattern in nearby page code.
rg -n 'React\.(ReactNode|FC|ReactElement)' frontend/src/pages/passenger -g '*.tsx' -C 1Repository: KatlehoMadaba/RydrSafe
Length of output: 200
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section in the target file.
cat -n frontend/src/pages/passenger/ReportDriverPage.tsx | sed -n '45,60p'
# Show the same typing pattern in nearby files.
cat -n frontend/src/hooks/useAuth.tsx | sed -n '30,40p'
cat -n frontend/src/pages/passenger/VerifyDriverPage.tsx | sed -n '85,95p'
# Check React/TypeScript package versions and app tsconfig.
sed -n '1,120p' package.json
sed -n '1,120p' tsconfig.app.jsonRepository: KatlehoMadaba/RydrSafe
Length of output: 1918
Import ReactNode instead of using React.ReactNode
frontend/src/pages/passenger/ReportDriverPage.tsx:52 references React.ReactNode without a React namespace import, and the app tsconfig uses jsx: "react-jsx". Switch to import type { ReactNode } from 'react' and use ReactNode in the tabs type.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/pages/passenger/ReportDriverPage.tsx` around lines 1 - 17, The
tabs type in ReportDriverPage is referencing React.ReactNode without importing
the React namespace, which breaks under react-jsx. Update the react import in
ReportDriverPage to import ReactNode as a type, then replace the React.ReactNode
annotation used in the tabs definition with ReactNode so the component stays
type-safe without relying on a React namespace import.
| const addFiles = (incoming: FileList | null) => { | ||
| if (!incoming) return | ||
| const valid = Array.from(incoming).filter((f) => { | ||
| if (!ACCEPTED.includes(f.type)) { toast.error(`${f.name}: unsupported format`); return false } | ||
| if (f.size > MAX_SIZE) { toast.error(`${f.name}: exceeds 10MB limit`); return false } | ||
| return true | ||
| }) | ||
| setEvidenceFiles((prev) => [...prev, ...valid].slice(0, 3)) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Files beyond the 3-image cap are silently dropped.
setEvidenceFiles((prev) => [...prev, ...valid].slice(0, 3)) truncates without notifying the user, so if they select/drop 4+ valid files, some silently never appear in the list with no explanation.
💡 Suggested fix
const addFiles = (incoming: FileList | null) => {
if (!incoming) return
const valid = Array.from(incoming).filter((f) => {
if (!ACCEPTED.includes(f.type)) { toast.error(`${f.name}: unsupported format`); return false }
if (f.size > MAX_SIZE) { toast.error(`${f.name}: exceeds 10MB limit`); return false }
return true
})
- setEvidenceFiles((prev) => [...prev, ...valid].slice(0, 3))
+ setEvidenceFiles((prev) => {
+ const next = [...prev, ...valid]
+ if (next.length > 3) toast.error('Only the first 3 screenshots are kept.')
+ return next.slice(0, 3)
+ })
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const addFiles = (incoming: FileList | null) => { | |
| if (!incoming) return | |
| const valid = Array.from(incoming).filter((f) => { | |
| if (!ACCEPTED.includes(f.type)) { toast.error(`${f.name}: unsupported format`); return false } | |
| if (f.size > MAX_SIZE) { toast.error(`${f.name}: exceeds 10MB limit`); return false } | |
| return true | |
| }) | |
| setEvidenceFiles((prev) => [...prev, ...valid].slice(0, 3)) | |
| } | |
| const addFiles = (incoming: FileList | null) => { | |
| if (!incoming) return | |
| const valid = Array.from(incoming).filter((f) => { | |
| if (!ACCEPTED.includes(f.type)) { toast.error(`${f.name}: unsupported format`); return false } | |
| if (f.size > MAX_SIZE) { toast.error(`${f.name}: exceeds 10MB limit`); return false } | |
| return true | |
| }) | |
| setEvidenceFiles((prev) => { | |
| const next = [...prev, ...valid] | |
| if (next.length > 3) toast.error('Only the first 3 screenshots are kept.') | |
| return next.slice(0, 3) | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/pages/passenger/ReportDriverPage.tsx` around lines 79 - 87, The
addFiles helper in ReportDriverPage silently truncates valid uploads when more
than 3 images are selected, so update the file handling to notify users when the
3-image cap is exceeded. In addFiles, before or while applying setEvidenceFiles,
detect any valid files that would be dropped by the slice(0, 3) limit and show a
toast explaining that only 3 images can be attached; keep the existing ACCEPTED
and MAX_SIZE checks intact.
Summary by CodeRabbit
New Features
Bug Fixes