(bugfix) Fix proof of payment download URL to use shareID with checksum
Fixed bug where proof of payment download links used raw locationID instead of shareID (locationID + checksum), causing link validation to fail. Added AsyncLink component to handle async shareID generation gracefully. Changes: - BillEditForm: Generate shareID using generateShareId server action - BillEditForm: Use AsyncLink to prevent broken links during async load - AsyncLink: New reusable component for links that need async data - Updated download URL from locationID-billID to shareID-billID format Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
13
web-app/app/ui/AsyncLink.tsx
Normal file
13
web-app/app/ui/AsyncLink.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
/** Link component that can be disabled */
|
||||||
|
export const AsyncLink: React.FC<{ href: string; children: React.ReactNode, target?: string, className?: string, disabled?: boolean }> = ({ href, children, target, className, disabled }) =>
|
||||||
|
disabled ? <span className={className}>{children}</span> :
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { DocumentIcon, TicketIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { DocumentIcon, TicketIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import { Bill, BilledTo, BillingLocation } from "../lib/db-types";
|
import { Bill, BilledTo, BillingLocation } from "../lib/db-types";
|
||||||
import React, { FC, useEffect } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import { updateOrAddBill } from "../lib/actions/billActions";
|
import { updateOrAddBill } from "../lib/actions/billActions";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -11,6 +11,8 @@ import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoderWasm";
|
|||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { InfoBox } from "./InfoBox";
|
import { InfoBox } from "./InfoBox";
|
||||||
import { Pdf417Barcode } from "./Pdf417Barcode";
|
import { Pdf417Barcode } from "./Pdf417Barcode";
|
||||||
|
import { generateShareId } from "../lib/actions/locationActions";
|
||||||
|
import { AsyncLink } from "./AsyncLink";
|
||||||
|
|
||||||
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
|
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
|
||||||
// This is a workaround for that
|
// This is a workaround for that
|
||||||
@@ -35,6 +37,20 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
|
|
||||||
const { yearMonth: { year: billYear, month: billMonth }, _id: locationID, proofOfPaymentType } = location;
|
const { yearMonth: { year: billYear, month: billMonth }, _id: locationID, proofOfPaymentType } = location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share ID for viewing-only links (locationID + checksum)
|
||||||
|
* Note: This is different from the share button which calls `generateShareLink`
|
||||||
|
* to activate sharing and set TTL in the database
|
||||||
|
*/
|
||||||
|
const [shareID, setShareID] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// share ID can be generated server-side since it requires a secret key
|
||||||
|
// which we don't want to expose to the client
|
||||||
|
(async () => setShareID(await generateShareId(locationID)))();
|
||||||
|
}, [locationID]);
|
||||||
|
|
||||||
|
|
||||||
const initialState = { message: null, errors: {} };
|
const initialState = { message: null, errors: {} };
|
||||||
|
|
||||||
const handleAction = updateOrAddBillMiddleware.bind(null, locationID, billID, billYear, billMonth);
|
const handleAction = updateOrAddBillMiddleware.bind(null, locationID, billID, billYear, billMonth);
|
||||||
@@ -238,14 +254,15 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
// -> don't show anything
|
// -> don't show anything
|
||||||
proofOfPayment.fileName ? (
|
proofOfPayment.fileName ? (
|
||||||
<div className="mt-3 ml-[-.5rem]">
|
<div className="mt-3 ml-[-.5rem]">
|
||||||
<Link
|
<AsyncLink
|
||||||
href={`/share/proof-of-payment/per-bill/${locationID}-${billID}/`}
|
disabled={!shareID}
|
||||||
|
href={`/share/proof-of-payment/per-bill/${shareID}-${billID}/`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className='text-center w-full max-w-[20rem] text-nowrap truncate inline-block'
|
className='text-center w-full max-w-[20rem] text-nowrap truncate inline-block'
|
||||||
>
|
>
|
||||||
<TicketIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1 mt-[-.2em] text-teal-500" />
|
<TicketIcon className="h-[1em] w-[1em] text-2xl inline-block mr-1 mt-[-.2em] text-teal-500" />
|
||||||
{decodeURIComponent(proofOfPayment.fileName)}
|
{decodeURIComponent(proofOfPayment.fileName)}
|
||||||
</Link>
|
</AsyncLink>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user