(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:
2026-01-09 18:16:53 +01:00
parent 37f617683e
commit e318523887
2 changed files with 34 additions and 4 deletions

View 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>

View File

@@ -2,7 +2,7 @@
import { DocumentIcon, TicketIcon, TrashIcon } from "@heroicons/react/24/outline";
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 { updateOrAddBill } from "../lib/actions/billActions";
import Link from "next/link";
@@ -11,6 +11,8 @@ import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoderWasm";
import { useLocale, useTranslations } from "next-intl";
import { InfoBox } from "./InfoBox";
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
// 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;
/**
* 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 handleAction = updateOrAddBillMiddleware.bind(null, locationID, billID, billYear, billMonth);
@@ -238,14 +254,15 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
// -> don't show anything
proofOfPayment.fileName ? (
<div className="mt-3 ml-[-.5rem]">
<Link
href={`/share/proof-of-payment/per-bill/${locationID}-${billID}/`}
<AsyncLink
disabled={!shareID}
href={`/share/proof-of-payment/per-bill/${shareID}-${billID}/`}
target="_blank"
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" />
{decodeURIComponent(proofOfPayment.fileName)}
</Link>
</AsyncLink>
</div>
) : null
}