// Sync geographic fields between different formats function syncGeoFields(data) { // If we have latitude and longitude but no Geo-Location, create it if (data.latitude && data.longitude && !data['Geo-Location']) { // Keep as strings to preserve precision const lat = typeof data.latitude === 'string' ? data.latitude : String(data.latitude); const lng = typeof data.longitude === 'string' ? data.longitude : String(data.longitude); data['Geo-Location'] = `${lat};${lng}`; data.geodata = `${lat};${lng}`; // Keep original values without parsing data.latitude = lat; data.longitude = lng; } // If we have Geo-Location but no lat/lng, parse it else if (data['Geo-Location'] && (!data.latitude || !data.longitude)) { const geoLocation = data['Geo-Location'].toString(); // Try semicolon-separated first let parts = geoLocation.split(';'); if (parts.length === 2) { // Keep as strings to preserve precision const lat = parts[0].trim(); const lng = parts[1].trim(); // Only validate they're numeric, don't convert if (!isNaN(parseFloat(lat)) && !isNaN(parseFloat(lng))) { data.latitude = lat; data.longitude = lng; data.geodata = `${lat};${lng}`; return data; } } // Try comma-separated parts = geoLocation.split(','); if (parts.length === 2) { const lat = parts[0].trim(); const lng = parts[1].trim(); if (!isNaN(parseFloat(lat)) && !isNaN(parseFloat(lng))) { data.latitude = lat; data.longitude = lng; data.geodata = `${lat};${lng}`; // Normalize Geo-Location to semicolon format for NocoDB GeoData data['Geo-Location'] = `${lat};${lng}`; } } } return data; } // Validate coordinates function validateCoordinates(lat, lng) { // Keep original string values const latStr = typeof lat === 'string' ? lat : String(lat); const lngStr = typeof lng === 'string' ? lng : String(lng); // Parse only for validation const latitude = parseFloat(latStr); const longitude = parseFloat(lngStr); if (isNaN(latitude) || isNaN(longitude)) { return { valid: false, error: 'Invalid coordinate values' }; } if (latitude < -90 || latitude > 90) { return { valid: false, error: 'Latitude must be between -90 and 90' }; } if (longitude < -180 || longitude > 180) { return { valid: false, error: 'Longitude must be between -180 and 180' }; } // Return the original string values to preserve precision return { valid: true, latitude: latStr, longitude: lngStr }; } // Check if coordinates are within bounds function checkBounds(lat, lng, bounds) { if (!bounds) return true; // Parse only for comparison const latitude = parseFloat(lat); const longitude = parseFloat(lng); return latitude <= bounds.north && latitude >= bounds.south && longitude <= bounds.east && longitude >= bounds.west; } // Validate URL format function validateUrl(url) { if (!url || typeof url !== 'string') { return ''; } const trimmed = url.trim(); if (!trimmed) { return ''; } // Basic URL validation try { new URL(trimmed); return trimmed; } catch (e) { // If not a valid URL, check if it's a relative path or missing protocol if (trimmed.startsWith('/') || !trimmed.includes('://')) { // For relative paths or missing protocol, return as-is return trimmed; } return ''; } } // Get cookie configuration based on request function getCookieConfig(req) { const host = req?.get('host') || ''; const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1') || host.match(/^\d+\.\d+\.\d+\.\d+/); const config = { httpOnly: true, maxAge: 24 * 60 * 60 * 1000, // 24 hours sameSite: 'lax', secure: false, domain: undefined }; // Only set domain and secure for production non-localhost access if (process.env.NODE_ENV === 'production' && !isLocalhost && process.env.COOKIE_DOMAIN) { const cookieDomain = process.env.COOKIE_DOMAIN.replace(/^\./, ''); if (host.includes(cookieDomain)) { config.domain = process.env.COOKIE_DOMAIN; config.secure = true; } } return config; } // Extract ID from NocoDB response function extractId(record) { return record.Id || record.id || record.ID || record._id; } // Sanitize user data for response function sanitizeUser(user) { const { Password, password, ...safeUser } = user; return safeUser; } // Extract walk sheet configuration from NocoDB data, handling different field name formats function extractWalkSheetConfig(data, defaults = {}) { if (!data) return defaults; return { walk_sheet_title: data.walk_sheet_title !== undefined ? data.walk_sheet_title : data['Walk Sheet Title'] !== undefined ? data['Walk Sheet Title'] : defaults.walk_sheet_title, walk_sheet_subtitle: data.walk_sheet_subtitle !== undefined ? data.walk_sheet_subtitle : data['Walk Sheet Subtitle'] !== undefined ? data['Walk Sheet Subtitle'] : defaults.walk_sheet_subtitle, walk_sheet_footer: data.walk_sheet_footer !== undefined ? data.walk_sheet_footer : data['Walk Sheet Footer'] !== undefined ? data['Walk Sheet Footer'] : defaults.walk_sheet_footer, qr_code_1_url: data.qr_code_1_url !== undefined ? data.qr_code_1_url : data['QR Code 1 URL'] !== undefined ? data['QR Code 1 URL'] : defaults.qr_code_1_url, qr_code_1_label: data.qr_code_1_label !== undefined ? data.qr_code_1_label : data['QR Code 1 Label'] !== undefined ? data['QR Code 1 Label'] : defaults.qr_code_1_label, qr_code_2_url: data.qr_code_2_url !== undefined ? data.qr_code_2_url : data['QR Code 2 URL'] !== undefined ? data['QR Code 2 URL'] : defaults.qr_code_2_url, qr_code_2_label: data.qr_code_2_label !== undefined ? data.qr_code_2_label : data['QR Code 2 Label'] !== undefined ? data['QR Code 2 Label'] : defaults.qr_code_2_label, qr_code_3_url: data.qr_code_3_url !== undefined ? data.qr_code_3_url : data['QR Code 3 URL'] !== undefined ? data['QR Code 3 URL'] : defaults.qr_code_3_url, qr_code_3_label: data.qr_code_3_label !== undefined ? data.qr_code_3_label : data['QR Code 3 Label'] !== undefined ? data['QR Code 3 Label'] : defaults.qr_code_3_label }; } module.exports = { syncGeoFields, validateUrl, getCookieConfig, extractId, validateCoordinates, checkBounds, sanitizeUser, extractWalkSheetConfig };