feat: secure combined uploads and update UI components

Changes:
- Secure uploadUtilBillsProofOfPayment with checksum validation
- Update ViewLocationCard to accept and use shareId prop
- Update ViewBillCard to accept shareId and use it for uploads
- Update ViewBillBadge to pass shareId to bill detail pages
- Add client-side validation check for shareId before upload
- Update back button links to use shareId

Security improvements:
- Both per-bill and combined uploads now validate checksum and TTL
- IP-based rate limiting applied to both upload types
- PDF magic bytes validation for both upload types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-08 00:25:26 +01:00
parent 844e386e18
commit 81dddb526a
4 changed files with 102 additions and 49 deletions

View File

@@ -17,9 +17,10 @@ import QRCode from "react-qr-code";
export interface ViewLocationCardProps {
location: BillingLocation;
userSettings: UserSettings | null;
shareId?: string;
}
export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSettings }) => {
export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSettings, shareId }) => {
const {
_id,
@@ -47,13 +48,18 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
const file = e.target.files?.[0];
if (!file) return;
// Validate file type
// Validate file type client-side (quick feedback)
if (file.type !== 'application/pdf') {
setUploadError('Only PDF files are accepted');
e.target.value = ''; // Reset input
return;
}
if (!shareId) {
setUploadError('Invalid upload link');
return;
}
setIsUploading(true);
setUploadError(null);
@@ -61,7 +67,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
const formData = new FormData();
formData.append('utilBillsProofOfPayment', file);
const result = await uploadUtilBillsProofOfPayment(_id, formData);
const result = await uploadUtilBillsProofOfPayment(shareId, formData);
if (result.success) {
setAttachmentFilename(file.name);
@@ -121,7 +127,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
<h2 className="card-title mr-[2em] text-[1.3rem]">{formatYearMonth(yearMonth)} {locationName}</h2>
<div className="card-actions mt-[1em] mb-[1em]">
{
bills.filter(bill => (bill.billedTo ?? BilledTo.Tenant) === BilledTo.Tenant).map(bill => <ViewBillBadge key={`${_id}-${bill._id}`} locationId={_id} bill={bill} />)
bills.filter(bill => (bill.billedTo ?? BilledTo.Tenant) === BilledTo.Tenant).map(bill => <ViewBillBadge key={`${_id}-${bill._id}`} locationId={_id} shareId={shareId} bill={bill} />)
}
</div>
{