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:
@@ -5,18 +5,22 @@ import { TicketIcon } from "@heroicons/react/24/outline";
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
export interface ViewBillBadgeProps {
|
||||
locationId: string,
|
||||
bill: Bill
|
||||
locationId: string;
|
||||
shareId?: string;
|
||||
bill: Bill;
|
||||
};
|
||||
|
||||
export const ViewBillBadge: FC<ViewBillBadgeProps> = ({ locationId, bill: { _id: billId, name, paid, attachment, proofOfPayment } }) => {
|
||||
export const ViewBillBadge: FC<ViewBillBadgeProps> = ({ locationId, shareId, bill: { _id: billId, name, paid, attachment, proofOfPayment } }) => {
|
||||
|
||||
const currentLocale = useLocale();
|
||||
|
||||
const className = `badge badge-lg p-[1em] ${paid ? "badge-success" : " badge-outline"} ${!paid && !!attachment ? "btn-outline btn-success" : ""} cursor-pointer`;
|
||||
|
||||
// Use shareId if available (for shared views), otherwise use locationId (for owner views)
|
||||
const billPageId = shareId || locationId;
|
||||
|
||||
return (
|
||||
<Link href={`/${currentLocale}//share/bill/${locationId}-${billId}`} className={className}>
|
||||
<Link href={`/${currentLocale}//share/bill/${billPageId}-${billId}`} className={className}>
|
||||
{name}
|
||||
{
|
||||
proofOfPayment?.uploadedAt ?
|
||||
|
||||
@@ -11,11 +11,12 @@ import { Pdf417Barcode } from "./Pdf417Barcode";
|
||||
import { uploadProofOfPayment } from "../lib/actions/billActions";
|
||||
|
||||
export interface ViewBillCardProps {
|
||||
location: BillingLocation,
|
||||
bill: Bill,
|
||||
location: BillingLocation;
|
||||
bill: Bill;
|
||||
shareId?: string;
|
||||
}
|
||||
|
||||
export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
|
||||
export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill, shareId }) => {
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations("bill-edit-form");
|
||||
@@ -31,23 +32,28 @@ export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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);
|
||||
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('proofOfPayment', file);
|
||||
|
||||
const result = await uploadProofOfPayment(locationID, billID as string, formData);
|
||||
|
||||
|
||||
const result = await uploadProofOfPayment(shareId, billID as string, formData);
|
||||
|
||||
if (result.success) {
|
||||
setProofOfPaymentFilename(file.name);
|
||||
setProofOfPaymentUploadedAt(new Date());
|
||||
@@ -161,7 +167,7 @@ export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill }) => {
|
||||
}
|
||||
|
||||
<div className="text-right">
|
||||
<Link className="btn btn-neutral ml-3" href={`/share/location/${locationID}`}>{t("back-button")}</Link>
|
||||
<Link className="btn btn-neutral ml-3" href={`/share/location/${shareId || locationID}`}>{t("back-button")}</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user