feat: add share link generation and validation functions
- Add generateShareLink() for owners to create share URLs with checksums - Add validateShareAccess() to validate checksum and TTL on tenant visits - Implement automatic TTL reset (10 days → 1 hour after first visit) - Include automatic cleanup of expired shares 🤖 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,6 +10,7 @@ import { gotoHomeWithMessage } from './navigationActions';
|
||||
import { unstable_noStore, revalidatePath } from 'next/cache';
|
||||
import { IntlTemplateFn } from '@/app/i18n';
|
||||
import { getTranslations, getLocale } from "next-intl/server";
|
||||
import { generateShareChecksum, validateShareChecksum } from '../shareChecksum';
|
||||
|
||||
export type State = {
|
||||
errors?: {
|
||||
@@ -698,4 +699,118 @@ export const uploadUtilBillsProofOfPayment = async (locationID: string, formData
|
||||
console.error('Error uploading util bills proof of payment:', error);
|
||||
return { success: false, error: error.message || 'Upload failed' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate/activate share link for location
|
||||
* Called when owner clicks "Share" button
|
||||
* Sets shareTTL to 10 days from now
|
||||
*/
|
||||
export const generateShareLink = withUser(
|
||||
async (user: AuthenticatedUser, locationId: string) => {
|
||||
|
||||
const { id: userId } = user;
|
||||
const dbClient = await getDbClient();
|
||||
|
||||
// Verify ownership
|
||||
const location = await dbClient.collection<BillingLocation>("lokacije").findOne({
|
||||
_id: locationId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!location) {
|
||||
return { error: 'Location not found' };
|
||||
}
|
||||
|
||||
// Calculate TTL (10 days from now, configurable)
|
||||
const initialDays = parseInt(process.env.SHARE_TTL_INITIAL_DAYS || '10', 10);
|
||||
const shareTTL = new Date(Date.now() + initialDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Activate sharing by setting TTL
|
||||
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||
{ _id: locationId },
|
||||
{
|
||||
$set: { shareTTL },
|
||||
$unset: { shareFirstVisitedAt: "" } // Reset first visit tracking
|
||||
}
|
||||
);
|
||||
|
||||
// Generate checksum
|
||||
const checksum = generateShareChecksum(locationId);
|
||||
|
||||
// Build share URL
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
const shareUrl = `${baseUrl}/share/location/${locationId}/${checksum}`;
|
||||
|
||||
return { shareUrl };
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Validate share link and update TTL on first visit
|
||||
* Called when tenant visits share link
|
||||
*
|
||||
* SECURITY:
|
||||
* 1. Validates checksum (stateless, prevents enumeration)
|
||||
* 2. Checks TTL in database (time-based access control)
|
||||
* 3. Marks first visit and resets TTL to 1 hour
|
||||
*/
|
||||
export async function validateShareAccess(
|
||||
locationId: string,
|
||||
checksum: string
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
|
||||
// 1. Validate checksum FIRST (before DB query - stateless validation)
|
||||
if (!validateShareChecksum(locationId, checksum)) {
|
||||
return { valid: false, error: 'Invalid share link' };
|
||||
}
|
||||
|
||||
// 2. Check TTL in database
|
||||
const dbClient = await getDbClient();
|
||||
const location = await dbClient.collection<BillingLocation>("lokacije").findOne(
|
||||
{ _id: locationId },
|
||||
{ projection: { shareTTL: 1, shareFirstVisitedAt: 1 } }
|
||||
);
|
||||
|
||||
if (!location) {
|
||||
return { valid: false, error: 'Invalid share link' };
|
||||
}
|
||||
|
||||
// 3. Check if sharing is enabled
|
||||
if (!location.shareTTL) {
|
||||
return { valid: false, error: 'This content is no longer shared' };
|
||||
}
|
||||
|
||||
// 4. Check if TTL expired
|
||||
const now = new Date();
|
||||
if (now > location.shareTTL) {
|
||||
// Clean up expired share
|
||||
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||
{ _id: locationId },
|
||||
{ $unset: { shareTTL: "", shareFirstVisitedAt: "" } }
|
||||
);
|
||||
|
||||
return { valid: false, error: 'This content is no longer shared' };
|
||||
}
|
||||
|
||||
// 5. Mark first visit if applicable (resets TTL to 1 hour)
|
||||
if (!location.shareFirstVisitedAt) {
|
||||
const visitHours = parseInt(process.env.SHARE_TTL_AFTER_VISIT_HOURS || '1', 10);
|
||||
const newTTL = new Date(Date.now() + visitHours * 60 * 60 * 1000);
|
||||
|
||||
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||
{
|
||||
_id: locationId,
|
||||
shareFirstVisitedAt: null // Only update if not already set
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
shareFirstVisitedAt: new Date(),
|
||||
shareTTL: newTTL
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
Reference in New Issue
Block a user