diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index 88b7686..07d7a9e 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -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("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("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("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("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("lokacije").updateOne( + { + _id: locationId, + shareFirstVisitedAt: null // Only update if not already set + }, + { + $set: { + shareFirstVisitedAt: new Date(), + shareTTL: newTTL + } + } + ); + } + + return { valid: true }; } \ No newline at end of file