feat: add rent-due share page for rent payment information

Created a new /share/rent-due/ page to display rent payment information
separately from utility bills.

Changes:
- Created /share/rent-due/[id]/ page structure with RentViewPage component
- Created ViewRentCard component to display rent amount and payment info
- Added uploadRentProofOfPayment action for tenant proof upload
- Added translation keys for rent-specific labels (en/hr)
- Updated rent email templates to link to /share/rent-due/ instead of /share/bills-due/
- Updated documentation to reflect new URL structure

The rent page displays:
- Rent amount
- IBAN or Revolut payment information with QR/barcode
- Rent proof of payment upload (when enabled)

🤖 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-31 11:56:01 +01:00
parent 1e83172491
commit 494d358130
9 changed files with 400 additions and 7 deletions

View File

@@ -784,6 +784,103 @@ export const uploadUtilBillsProofOfPayment = async (
}
}
/**
* Upload rent proof of payment (for tenants via share link)
* Similar to uploadUtilBillsProofOfPayment but for rent payments
*/
export const uploadRentProofOfPayment = async (
shareId: string,
formData: FormData,
ipAddress?: string
): Promise<{ success: boolean; error?: string }> => {
unstable_noStore();
try {
// 1. EXTRACT AND VALIDATE CHECKSUM (stateless, fast)
const extracted = extractShareId(shareId);
if (!extracted) {
console.log('shareID extraction failed');
return { success: false, error: 'Invalid share link' };
}
const { locationId: locationID, checksum } = extracted;
if (!validateShareChecksum(locationID, checksum)) {
console.log('shareID checksum validation failed');
return { success: false, error: 'Invalid share link' };
}
// 2. RATE LIMITING (per IP)
if (ipAddress) {
const rateLimit = checkUploadRateLimit(ipAddress);
if (!rateLimit.allowed) {
return {
success: false,
error: `Too many uploads. Try again in ${Math.ceil(rateLimit.resetIn / 60)} minutes.`
};
}
}
// 3. DATABASE VALIDATION
const dbClient = await getDbClient();
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne({ _id: locationID }, { projection: { userId: 1, rentProofOfPayment: 1, shareTTL: 1 } });
if (!location || !location.userId) {
return { success: false, error: 'Invalid request' };
}
// Check sharing is active and not expired
if (!location.shareTTL || new Date() > location.shareTTL) {
return { success: false, error: 'This content is no longer shared' };
}
// Check if proof of payment already uploaded
if (location.rentProofOfPayment) {
return { success: false, error: 'Proof of payment already uploaded for this location' };
}
// 4. FILE VALIDATION
const file = formData.get('rentProofOfPayment') as File;
if (!file || file.size === 0) {
return { success: false, error: 'No file provided' };
}
// Validate PDF content (magic bytes, not just MIME type)
const pdfValidation = await validatePdfFile(file);
if (!pdfValidation.valid) {
return { success: false, error: pdfValidation.error };
}
// 5. SERIALIZE & STORE FILE
const attachment = await serializeAttachment(file);
if (!attachment) {
return { success: false, error: 'Failed to process file' };
}
// 6. UPDATE DATABASE
await dbClient.collection<BillingLocation>("lokacije")
.updateOne(
{ _id: locationID },
{ $set: {
rentProofOfPayment: attachment
} }
);
// 7. REVALIDATE CACHE
revalidatePath(`/share/rent-due/${shareId}`, 'page');
return { success: true };
} catch (error: any) {
console.error('Upload error:', error);
return { success: false, error: 'Upload failed. Please try again.' };
}
}
/**
* Generate/activate share link for location
* Called when owner clicks "Share" button