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:
@@ -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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user