refactor: Use sessionStorage instead of localStorage for decoy booking data (#24450)
* feat: Add async spam check integration and decoy booking response - Integrate SpamCheckService with handleNewBooking workflow - Implement parallel spam check execution for minimal performance impact - Add decoy booking response with localStorage-based success page - Extract organization ID from event type for org-specific blocking - Add comprehensive test coverage for spam detection scenarios - Create reusable components for booking success cards - Implement fail-open behavior to never block legitimate bookings This builds on the spam blocker DI infrastructure from PR #24040 by adding the actual integration into the booking flow and implementing the decoy response mechanism to avoid revealing spam detection to malicious actors. Related: #24040 Co-Authored-By: [email protected] <[email protected]> * Do checks in paralle * Fix leaking host name in title * Reduce expoiry time localstorage * refactor: Use sessionStorage instead of localStorage for decoy booking data - Replace localStorage with sessionStorage for automatic expiration on tab close - Remove timestamp tracking and TTL logic (no longer needed) - Improve privacy by auto-clearing data when browser tab/window closes - Update documentation to reflect sessionStorage behavior This change addresses privacy concerns by ensuring decoy booking data (including attendee email) is automatically removed when the user closes the tab, rather than persisting for 5 minutes or requiring manual cleanup. Co-Authored-By: [email protected] <[email protected]> * feat: Add sessionStorage wrapper to webstorage module Co-Authored-By: [email protected] <[email protected]> * Reset RegularBookingService.ts to main's version exactly * feat: Add 5-minute expiration timeout to decoy booking data - Adds timestamp to DecoyBookingData interface - Checks expiration when retrieving booking data - Automatically removes expired data from sessionStorage - Provides defense-in-depth against potential misuse - Works alongside sessionStorage auto-clear on tab close --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
010ee0d066
commit
bf38eb8cb1
@@ -1,6 +1,7 @@
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { sessionStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
const BOOKING_SUCCESS_STORAGE_KEY_PREFIX = "cal.successfulBooking";
|
||||
const DECOY_BOOKING_EXPIRATION_MS = 5 * 60 * 1000;
|
||||
|
||||
interface DecoyBookingData {
|
||||
booking: {
|
||||
@@ -20,7 +21,8 @@ function getStorageKey(uid: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores decoy booking data in localStorage using the booking's uid
|
||||
* Stores decoy booking data in sessionStorage using the booking's uid
|
||||
* Data automatically expires when the browser tab/window is closed or after 5 minutes
|
||||
* @param booking - The booking data to store (must include uid)
|
||||
*/
|
||||
export function storeDecoyBooking(booking: Record<string, unknown> & { uid: string }): void {
|
||||
@@ -29,11 +31,11 @@ export function storeDecoyBooking(booking: Record<string, unknown> & { uid: stri
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const storageKey = getStorageKey(booking.uid);
|
||||
localStorage.setItem(storageKey, JSON.stringify(bookingSuccessData));
|
||||
sessionStorage.setItem(storageKey, JSON.stringify(bookingSuccessData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves decoy booking data from localStorage
|
||||
* Retrieves decoy booking data from sessionStorage
|
||||
* @param uid - The booking uid
|
||||
* @returns The stored booking data or null if not found or expired
|
||||
*/
|
||||
@@ -43,7 +45,7 @@ export function getDecoyBooking(uid: string): DecoyBookingData | null {
|
||||
}
|
||||
|
||||
const storageKey = getStorageKey(uid);
|
||||
const dataStr = localStorage.getItem(storageKey);
|
||||
const dataStr = sessionStorage.getItem(storageKey);
|
||||
|
||||
if (!dataStr) {
|
||||
return null;
|
||||
@@ -52,26 +54,22 @@ export function getDecoyBooking(uid: string): DecoyBookingData | null {
|
||||
try {
|
||||
const data: DecoyBookingData = JSON.parse(dataStr);
|
||||
|
||||
// Check if the data is too old (5 min)
|
||||
const dataAge = Date.now() - data.timestamp;
|
||||
const maxAge = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
|
||||
if (dataAge > maxAge) {
|
||||
// Remove the data from localStorage if expired
|
||||
localStorage.removeItem(storageKey);
|
||||
const isExpired = Date.now() - data.timestamp > DECOY_BOOKING_EXPIRATION_MS;
|
||||
if (isExpired) {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch {
|
||||
// If parsing fails, remove the corrupted data
|
||||
localStorage.removeItem(storageKey);
|
||||
sessionStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes decoy booking data from localStorage
|
||||
* Removes decoy booking data from sessionStorage
|
||||
* @param uid - The booking uid
|
||||
*/
|
||||
export function removeDecoyBooking(uid: string): void {
|
||||
@@ -80,7 +78,7 @@ export function removeDecoyBooking(uid: string): void {
|
||||
}
|
||||
|
||||
const storageKey = getStorageKey(uid);
|
||||
localStorage.removeItem(storageKey);
|
||||
sessionStorage.removeItem(storageKey);
|
||||
}
|
||||
|
||||
export type { DecoyBookingData };
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
|
||||
* Provides a wrapper around localStorage and sessionStorage to avoid errors in case of restricted storage access.
|
||||
*
|
||||
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
|
||||
*/
|
||||
export const localStorage = {
|
||||
getItem(key: string) {
|
||||
try {
|
||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||
return window.localStorage.getItem(key);
|
||||
} catch {
|
||||
// In case storage is restricted. Possible reasons
|
||||
@@ -16,7 +15,6 @@ export const localStorage = {
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
try {
|
||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// In case storage is restricted. Possible reasons
|
||||
@@ -27,7 +25,6 @@ export const localStorage = {
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
try {
|
||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||
window.localStorage.removeItem(key);
|
||||
} catch {
|
||||
return;
|
||||
@@ -35,11 +32,6 @@ export const localStorage = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
|
||||
*
|
||||
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
|
||||
*/
|
||||
export const sessionStorage = {
|
||||
getItem(key: string) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user