mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Sanitize input
This commit is contained in:
parent
5c6b76dd63
commit
e3b2fcd415
3 changed files with 39 additions and 59 deletions
|
|
@ -130,7 +130,7 @@ npx playwright test # E2E tests
|
|||
|
||||
- **Framework**: rSwag (Swagger/OpenAPI)
|
||||
- **Location**: `/api-docs` endpoint
|
||||
- **Authentication**: JWT-based for API access
|
||||
- **Authentication**: API key (Bearer) for API access
|
||||
|
||||
## Database Schema
|
||||
|
||||
|
|
|
|||
|
|
@ -83,12 +83,6 @@ class LocationSearch {
|
|||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
id="location-search-input"
|
||||
/>
|
||||
<button
|
||||
id="location-search-submit"
|
||||
class="px-3 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
<button
|
||||
id="location-search-close"
|
||||
class="px-2 py-2 text-gray-400 hover:text-gray-600"
|
||||
|
|
@ -116,7 +110,6 @@ class LocationSearch {
|
|||
// Store references
|
||||
this.searchBar = searchBar;
|
||||
this.searchInput = document.getElementById('location-search-input');
|
||||
this.searchButton = document.getElementById('location-search-submit');
|
||||
this.closeButton = document.getElementById('location-search-close');
|
||||
this.suggestionsContainer = document.getElementById('location-search-suggestions');
|
||||
this.suggestionsPanel = document.getElementById('location-search-suggestions-panel');
|
||||
|
|
@ -200,18 +193,11 @@ class LocationSearch {
|
|||
this.hideSearchBar();
|
||||
});
|
||||
|
||||
// Search on button click
|
||||
this.searchButton.addEventListener('click', () => {
|
||||
this.performSearch();
|
||||
});
|
||||
|
||||
// Search on Enter key
|
||||
this.searchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (this.suggestionsVisible && this.currentSuggestionIndex >= 0) {
|
||||
this.selectSuggestion(this.currentSuggestionIndex);
|
||||
} else {
|
||||
this.performSearch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -284,35 +270,6 @@ class LocationSearch {
|
|||
});
|
||||
}
|
||||
|
||||
async performSearch() {
|
||||
const query = this.searchInput.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
this.currentSearchQuery = query;
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/locations?q=${encodeURIComponent(query)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.displaySearchResults(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Location search error:', error);
|
||||
this.showError('Failed to search locations. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
// Hide other panels and show results with loading
|
||||
this.suggestionsPanel.classList.add('hidden');
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ module LocationSearch
|
|||
end
|
||||
|
||||
def find_points_near(user, latitude, longitude, radius_meters, date_options = {})
|
||||
points_query = build_spatial_query(user, latitude, longitude, radius_meters, date_options)
|
||||
query_sql, bind_values = build_spatial_query(user, latitude, longitude, radius_meters, date_options)
|
||||
|
||||
ActiveRecord::Base.connection.exec_query(points_query)
|
||||
# Use sanitize_sql_array to safely execute the parameterized query
|
||||
safe_query = ActiveRecord::Base.sanitize_sql_array([query_sql] + bind_values)
|
||||
|
||||
|
||||
ActiveRecord::Base.connection.exec_query(safe_query)
|
||||
.map { |row| format_point_result(row) }
|
||||
.sort_by { |point| point[:timestamp] }
|
||||
.reverse # Most recent first
|
||||
|
|
@ -18,9 +22,14 @@ module LocationSearch
|
|||
private
|
||||
|
||||
def build_spatial_query(user, latitude, longitude, radius_meters, date_options = {})
|
||||
date_filter = build_date_filter(date_options)
|
||||
date_filter_sql, date_bind_values = build_date_filter(date_options)
|
||||
|
||||
<<~SQL
|
||||
# Build parameterized query with proper SRID using ? placeholders
|
||||
# Use a CTE to avoid duplicating the point calculation
|
||||
base_sql = <<~SQL
|
||||
WITH search_point AS (
|
||||
SELECT ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography AS geom
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.timestamp,
|
||||
|
|
@ -30,35 +39,49 @@ module LocationSearch
|
|||
p.country,
|
||||
p.altitude,
|
||||
p.accuracy,
|
||||
ST_Distance(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography) as distance_meters,
|
||||
ST_Distance(p.lonlat, search_point.geom) as distance_meters,
|
||||
TO_TIMESTAMP(p.timestamp) as recorded_at
|
||||
FROM points p
|
||||
WHERE p.user_id = #{user.id}
|
||||
AND ST_DWithin(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography, #{radius_meters})
|
||||
#{date_filter}
|
||||
ORDER BY p.timestamp DESC;
|
||||
FROM points p, search_point
|
||||
WHERE p.user_id = ?
|
||||
AND ST_DWithin(p.lonlat, search_point.geom, ?)
|
||||
#{date_filter_sql}
|
||||
ORDER BY p.timestamp DESC
|
||||
SQL
|
||||
|
||||
# Combine bind values: longitude, latitude, user_id, radius, then date filters
|
||||
bind_values = [
|
||||
longitude.to_f, # longitude for search point
|
||||
latitude.to_f, # latitude for search point
|
||||
user.id, # user_id
|
||||
radius_meters.to_f # radius_meters
|
||||
]
|
||||
bind_values.concat(date_bind_values)
|
||||
|
||||
[base_sql, bind_values]
|
||||
end
|
||||
|
||||
def build_date_filter(date_options)
|
||||
return '' unless date_options[:date_from] || date_options[:date_to]
|
||||
return ['', []] unless date_options[:date_from] || date_options[:date_to]
|
||||
|
||||
filters = []
|
||||
bind_values = []
|
||||
|
||||
if date_options[:date_from]
|
||||
timestamp_from = date_options[:date_from].to_time.to_i
|
||||
filters << "p.timestamp >= #{timestamp_from}"
|
||||
filters << "p.timestamp >= ?"
|
||||
bind_values << timestamp_from
|
||||
end
|
||||
|
||||
if date_options[:date_to]
|
||||
# Add one day to include the entire end date
|
||||
timestamp_to = (date_options[:date_to] + 1.day).to_time.to_i
|
||||
filters << "p.timestamp < #{timestamp_to}"
|
||||
filters << "p.timestamp < ?"
|
||||
bind_values << timestamp_to
|
||||
end
|
||||
|
||||
return '' if filters.empty?
|
||||
return ['', []] if filters.empty?
|
||||
|
||||
"AND #{filters.join(' AND ')}"
|
||||
["AND #{filters.join(' AND ')}", bind_values]
|
||||
end
|
||||
|
||||
def format_point_result(row)
|
||||
|
|
|
|||
Loading…
Reference in a new issue