refactor: use combined shareId (locationId + checksum) in URL

Changes:
- Add generateShareId() and extractShareId() helpers
- Share URLs now use single parameter: /share/location/{shareId}
- shareId = locationId (24 chars) + checksum (16 chars) = 40 chars total
- Update validateShareAccess() to extract locationId from shareId
- Update uploadProofOfPayment() to accept combined shareId
- Update LocationViewPage to validate and extract locationId from shareId

Benefits:
- Simpler URL structure (one parameter instead of two)
- Checksum extraction by length (deterministic, no parsing needed)
- Same security properties (HMAC-SHA256 validation)

🤖 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:22:59 +01:00
parent e497ad1da6
commit 844e386e18
5 changed files with 104 additions and 29 deletions

View File

@@ -10,7 +10,7 @@ import { gotoHomeWithMessage } from './navigationActions';
import { getTranslations, getLocale } from "next-intl/server";
import { IntlTemplateFn } from '@/app/i18n';
import { unstable_noStore, revalidatePath } from 'next/cache';
import { validateShareChecksum } from '../shareChecksum';
import { extractShareId, validateShareChecksum } from '../shareChecksum';
import { validatePdfFile } from '../validators/pdfValidator';
import { checkUploadRateLimit } from '../uploadRateLimiter';
@@ -492,11 +492,15 @@ export const deleteBillById = withUser(async (user: AuthenticatedUser, locationI
/**
* Uploads proof of payment for the given bill
* SECURITY: Validates checksum, TTL, PDF content, and rate limits by IP
*
* @param shareId - Combined location ID + checksum (40 chars)
* @param billID - The bill ID to attach proof of payment to
* @param formData - Form data containing the PDF file
* @param ipAddress - Optional IP address for rate limiting
*/
export const uploadProofOfPayment = async (
locationID: string,
shareId: string,
billID: string,
checksum: string,
formData: FormData,
ipAddress?: string
): Promise<{ success: boolean; error?: string }> => {
@@ -504,7 +508,14 @@ export const uploadProofOfPayment = async (
unstable_noStore();
try {
// 1. VALIDATE CHECKSUM (stateless, fast)
// 1. EXTRACT AND VALIDATE CHECKSUM (stateless, fast)
const extracted = extractShareId(shareId);
if (!extracted) {
return { success: false, error: 'Invalid share link' };
}
const { locationId: locationID, checksum } = extracted;
if (!validateShareChecksum(locationID, checksum)) {
return { success: false, error: 'Invalid share link' };
}
@@ -584,7 +595,7 @@ export const uploadProofOfPayment = async (
await cleanupExpiredShares(dbClient);
// 8. REVALIDATE CACHE
revalidatePath(`/share/location/${locationID}/${checksum}`, 'page');
revalidatePath(`/share/location/${shareId}`, 'page');
return { success: true };