mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
Update stuff, fix stuff
This commit is contained in:
parent
4287fee93d
commit
0728c21c61
28 changed files with 213 additions and 406 deletions
|
|
@ -141,7 +141,7 @@ Dawarich includes a comprehensive public sharing system that allows users to sha
|
|||
|
||||
### Technical Implementation
|
||||
- **Database**: `sharing_settings` (JSONB) and `sharing_uuid` (UUID) columns on `stats` table
|
||||
- **Routes**: `/shared/stats/:uuid` for public viewing, `/stats/:year/:month/sharing` for management
|
||||
- **Routes**: `/shared/month/:uuid` for public viewing, `/stats/:year/:month/sharing` for management
|
||||
- **API**: `/api/v1/maps/hexagons` supports public access via `uuid` parameter
|
||||
- **Controllers**: `Shared::StatsController` handles public views, sharing management integrated into existing stats flow
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
*/
|
||||
@import 'actiontext.css';
|
||||
@import 'leaflet_theme.css';
|
||||
|
||||
@layer components {
|
||||
.fade-out {
|
||||
|
|
@ -71,14 +72,6 @@
|
|||
right: 310px;
|
||||
}
|
||||
|
||||
.leaflet-control-button {
|
||||
background-color: white !important;
|
||||
color: #374151 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-button:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Drawer Panel Styles */
|
||||
.leaflet-drawer {
|
||||
|
|
|
|||
141
app/assets/stylesheets/leaflet_theme.css
Normal file
141
app/assets/stylesheets/leaflet_theme.css
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* Leaflet Theme Styles - Light and Dark mode support */
|
||||
|
||||
/* CSS Custom Properties for Light Theme */
|
||||
[data-theme="light"] {
|
||||
--leaflet-bg-color: #ffffff;
|
||||
--leaflet-text-color: #000000;
|
||||
--leaflet-border-color: #e5e7eb;
|
||||
--leaflet-shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--leaflet-hover-color: #f3f4f6;
|
||||
--leaflet-link-color: #0066cc;
|
||||
--leaflet-scale-bg: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* CSS Custom Properties for Dark Theme */
|
||||
[data-theme="dark"] {
|
||||
--leaflet-bg-color: #374151;
|
||||
--leaflet-text-color: #ffffff;
|
||||
--leaflet-border-color: #4b5563;
|
||||
--leaflet-shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--leaflet-hover-color: #4b5563;
|
||||
--leaflet-link-color: #66b3ff;
|
||||
--leaflet-scale-bg: rgba(55, 65, 81, 0.9);
|
||||
}
|
||||
|
||||
/* Leaflet default controls theme override */
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-zoom,
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle,
|
||||
.leaflet-control-layers-list,
|
||||
.leaflet-control-draw {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-color: var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet zoom buttons */
|
||||
.leaflet-control-zoom a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-bottom: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet layer control */
|
||||
.leaflet-control-layers-toggle {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers label {
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet Draw controls */
|
||||
.leaflet-draw-toolbar a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-bottom: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar a:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet popups */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
}
|
||||
|
||||
/* Attribution control */
|
||||
.leaflet-control-attribution a {
|
||||
color: var(--leaflet-link-color) !important;
|
||||
}
|
||||
|
||||
/* Custom control buttons */
|
||||
.leaflet-control-button,
|
||||
.add-visit-button,
|
||||
.leaflet-bar button {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-button:hover,
|
||||
.add-visit-button:hover,
|
||||
.leaflet-bar button:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Any other custom controls */
|
||||
.leaflet-top .leaflet-control button,
|
||||
.leaflet-bottom .leaflet-control button,
|
||||
.leaflet-left .leaflet-control button,
|
||||
.leaflet-right .leaflet-control button {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
/* Location search button */
|
||||
.location-search-toggle,
|
||||
#location-search-toggle {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
}
|
||||
|
||||
.location-search-toggle:hover,
|
||||
#location-search-toggle:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Distance scale control */
|
||||
.leaflet-control-scale {
|
||||
background: var(--leaflet-scale-bg) !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
# Check both current request header and stored session value
|
||||
client_type = request.headers['X-Dawarich-Client'] || session[:dawarich_client]
|
||||
|
||||
case client_type
|
||||
|
|
|
|||
|
|
@ -59,23 +59,8 @@ class Settings::UsersController < ApplicationController
|
|||
return
|
||||
end
|
||||
|
||||
archive_param = params[:archive]
|
||||
|
||||
# Handle both direct upload (signed_id) and traditional upload (file)
|
||||
if archive_param.is_a?(String)
|
||||
# Direct upload: archive_param is a signed blob ID
|
||||
import = create_import_from_signed_archive_id(archive_param)
|
||||
else
|
||||
# Traditional upload: archive_param is an uploaded file
|
||||
validate_archive_file(archive_param)
|
||||
|
||||
import = current_user.imports.build(
|
||||
name: archive_param.original_filename,
|
||||
source: :user_data_archive
|
||||
)
|
||||
|
||||
import.file.attach(archive_param)
|
||||
end
|
||||
import =
|
||||
create_import_from_signed_archive_id(params[:archive])
|
||||
|
||||
if import.save
|
||||
redirect_to edit_user_registration_path,
|
||||
|
|
|
|||
|
|
@ -19,16 +19,12 @@ export default class extends Controller {
|
|||
this.currentPopup = null;
|
||||
this.mapsController = null;
|
||||
|
||||
// Listen for theme changes
|
||||
document.addEventListener('theme:changed', this.handleThemeChange.bind(this));
|
||||
|
||||
// Wait for the map to be initialized
|
||||
this.waitForMap();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanup();
|
||||
document.removeEventListener('theme:changed', this.handleThemeChange.bind(this));
|
||||
console.log("Add visit controller disconnected");
|
||||
}
|
||||
|
||||
|
|
@ -435,16 +431,6 @@ export default class extends Controller {
|
|||
});
|
||||
}
|
||||
|
||||
handleThemeChange(event) {
|
||||
console.log('Add visit controller: Theme changed to', event.detail.theme);
|
||||
this.userThemeValue = event.detail.theme;
|
||||
|
||||
// Update button theme if it exists
|
||||
if (this.addVisitButton && !this.isAddingVisit) {
|
||||
applyThemeToButton(this.addVisitButton, this.userThemeValue);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.map) {
|
||||
this.map.off('click', this.onMapClick, this);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import { TileMonitor } from "../maps/tile_monitor";
|
|||
import BaseController from "./base_controller";
|
||||
import { createAllMapLayers } from "../maps/layers";
|
||||
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
|
||||
import { injectThemeStyles } from "../maps/theme_styles";
|
||||
|
||||
export default class extends BaseController {
|
||||
static targets = ["container"];
|
||||
|
|
@ -65,9 +64,6 @@ export default class extends BaseController {
|
|||
this.selfHosted = this.element.dataset.self_hosted;
|
||||
this.userTheme = this.element.dataset.user_theme || 'dark';
|
||||
|
||||
// Inject theme styles for Leaflet controls
|
||||
injectThemeStyles(this.userTheme);
|
||||
|
||||
try {
|
||||
this.markers = this.element.dataset.coordinates ? JSON.parse(this.element.dataset.coordinates) : [];
|
||||
} catch (error) {
|
||||
|
|
@ -1005,7 +1001,6 @@ export default class extends BaseController {
|
|||
if (newSettings.theme && newSettings.theme !== this.userTheme) {
|
||||
this.userTheme = newSettings.theme;
|
||||
mapElement.setAttribute('data-user_theme', this.userTheme);
|
||||
injectThemeStyles(this.userTheme);
|
||||
|
||||
// Dispatch theme change event for other controllers
|
||||
document.dispatchEvent(new CustomEvent('theme:changed', {
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,6 @@ class LocationSearch {
|
|||
return new Date(dateString).toLocaleDateString() + ' ' +
|
||||
new Date(dateString).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { LocationSearch };
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
// Dynamic CSS injection for theme-aware Leaflet controls
|
||||
export function injectThemeStyles(userTheme) {
|
||||
// Remove existing theme styles if any
|
||||
const existingStyle = document.getElementById('leaflet-theme-styles');
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
const themeColors = getThemeColors(userTheme);
|
||||
|
||||
const css = `
|
||||
/* Leaflet default controls theme override */
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-zoom,
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle,
|
||||
.leaflet-control-layers-list,
|
||||
.leaflet-control-draw {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border-color: ${themeColors.borderColor} !important;
|
||||
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
|
||||
}
|
||||
|
||||
/* Leaflet zoom buttons */
|
||||
.leaflet-control-zoom a {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border-bottom: 1px solid ${themeColors.borderColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background-color: ${themeColors.hoverColor} !important;
|
||||
}
|
||||
|
||||
/* Leaflet layer control */
|
||||
.leaflet-control-layers-toggle {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers label {
|
||||
color: ${themeColors.textColor} !important;
|
||||
}
|
||||
|
||||
/* Leaflet Draw controls */
|
||||
.leaflet-draw-toolbar a {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border-bottom: 1px solid ${themeColors.borderColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar a:hover {
|
||||
background-color: ${themeColors.hoverColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions a {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
}
|
||||
|
||||
/* Leaflet popups */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
}
|
||||
|
||||
/* Attribution control */
|
||||
.leaflet-control-attribution a {
|
||||
color: ${userTheme === 'light' ? '#0066cc' : '#66b3ff'} !important;
|
||||
}
|
||||
|
||||
/* Custom control buttons */
|
||||
.leaflet-control-button,
|
||||
.add-visit-button,
|
||||
.leaflet-bar button {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border: 1px solid ${themeColors.borderColor} !important;
|
||||
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
|
||||
}
|
||||
|
||||
.leaflet-control-button:hover,
|
||||
.add-visit-button:hover,
|
||||
.leaflet-bar button:hover {
|
||||
background-color: ${themeColors.hoverColor} !important;
|
||||
}
|
||||
|
||||
/* Any other custom controls */
|
||||
.leaflet-top .leaflet-control button,
|
||||
.leaflet-bottom .leaflet-control button,
|
||||
.leaflet-left .leaflet-control button,
|
||||
.leaflet-right .leaflet-control button {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border: 1px solid ${themeColors.borderColor} !important;
|
||||
}
|
||||
|
||||
/* Location search button */
|
||||
.location-search-toggle,
|
||||
#location-search-toggle {
|
||||
background-color: ${themeColors.backgroundColor} !important;
|
||||
color: ${themeColors.textColor} !important;
|
||||
border: 1px solid ${themeColors.borderColor} !important;
|
||||
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
|
||||
}
|
||||
|
||||
.location-search-toggle:hover,
|
||||
#location-search-toggle:hover {
|
||||
background-color: ${themeColors.hoverColor} !important;
|
||||
}
|
||||
|
||||
/* Distance scale control - minimal theming to avoid duplication */
|
||||
.leaflet-control-scale {
|
||||
background: rgba(${userTheme === 'light' ? '255, 255, 255' : '55, 65, 81'}, 0.9) !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject the CSS
|
||||
const style = document.createElement('style');
|
||||
style.id = 'leaflet-theme-styles';
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function getThemeColors(userTheme) {
|
||||
if (userTheme === 'light') {
|
||||
return {
|
||||
backgroundColor: '#ffffff',
|
||||
textColor: '#000000',
|
||||
borderColor: '#e5e7eb',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
hoverColor: '#f3f4f6'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: '#374151',
|
||||
textColor: '#ffffff',
|
||||
borderColor: '#4b5563',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
hoverColor: '#4b5563'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,8 @@ class Api::PointSerializer
|
|||
lat = point.lat
|
||||
lon = point.lon
|
||||
|
||||
attributes['latitude'] = lat.nil? ? nil : lat.to_s
|
||||
attributes['longitude'] = lon.nil? ? nil : lon.to_s
|
||||
attributes['latitude'] = lat&.to_s
|
||||
attributes['longitude'] = lon&.to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ module Maps
|
|||
|
||||
def execute_bounds_query(start_timestamp, end_timestamp)
|
||||
ActiveRecord::Base.connection.exec_query(
|
||||
"SELECT MIN(latitude) as min_lat, MAX(latitude) as max_lat,
|
||||
MIN(longitude) as min_lng, MAX(longitude) as max_lng
|
||||
"SELECT ST_YMin(ST_Extent(lonlat::geometry)) as min_lat,
|
||||
ST_YMax(ST_Extent(lonlat::geometry)) as max_lat,
|
||||
ST_XMin(ST_Extent(lonlat::geometry)) as min_lng,
|
||||
ST_XMax(ST_Extent(lonlat::geometry)) as max_lng
|
||||
FROM points
|
||||
WHERE user_id = $1
|
||||
AND timestamp BETWEEN $2 AND $3",
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Maps
|
||||
class DateParameterCoercer
|
||||
class InvalidDateFormatError < StandardError; end
|
||||
|
||||
def initialize(param)
|
||||
@param = param
|
||||
end
|
||||
|
||||
def call
|
||||
coerce_date(@param)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :param
|
||||
|
||||
def coerce_date(param)
|
||||
case param
|
||||
when String
|
||||
coerce_string_param(param)
|
||||
when Integer
|
||||
param
|
||||
else
|
||||
param.to_i
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
Rails.logger.error "Invalid date format: #{param} - #{e.message}"
|
||||
raise InvalidDateFormatError, "Invalid date format: #{param}"
|
||||
end
|
||||
|
||||
def coerce_string_param(param)
|
||||
return param.to_i if param.match?(/^\d+$/)
|
||||
|
||||
Time.parse(param).to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,8 +32,7 @@ module Maps
|
|||
end
|
||||
|
||||
def recalculate_h3_hex_ids
|
||||
service = Stats::CalculateMonth.new(user.id, stat.year, stat.month)
|
||||
service.send(:calculate_h3_hex_ids)
|
||||
Stats::HexagonCalculator.new(user.id, stat.year, stat.month).call
|
||||
end
|
||||
|
||||
def update_stat_with_new_hex_ids(new_hex_ids)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class Stats::CalculateMonth
|
|||
toponyms: toponyms,
|
||||
h3_hex_ids: calculate_h3_hex_ids
|
||||
)
|
||||
stat.save
|
||||
|
||||
stat.save!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,6 @@ class Stats::HexagonCalculator
|
|||
calculate_h3_hexagon_centers(h3_resolution)
|
||||
end
|
||||
|
||||
def calculate_h3_hex_ids
|
||||
result = calculate_hexagons(DEFAULT_H3_RESOLUTION)
|
||||
return {} if result.nil?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year, :month
|
||||
|
|
|
|||
|
|
@ -73,17 +73,49 @@ class Users::ImportData
|
|||
zip_file.each do |entry|
|
||||
next if entry.directory?
|
||||
|
||||
extraction_path = File.join(@import_directory, entry.name)
|
||||
# Sanitize entry name to prevent path traversal attacks
|
||||
sanitized_name = sanitize_zip_entry_name(entry.name)
|
||||
next if sanitized_name.nil?
|
||||
|
||||
# Compute absolute destination path
|
||||
extraction_path = File.expand_path(File.join(@import_directory, sanitized_name))
|
||||
|
||||
# Verify the extraction path is within the import directory
|
||||
safe_import_dir = File.expand_path(@import_directory) + File::SEPARATOR
|
||||
unless extraction_path.start_with?(safe_import_dir) || extraction_path == File.expand_path(@import_directory)
|
||||
Rails.logger.warn "Skipping potentially malicious ZIP entry: #{entry.name} (would extract to #{extraction_path})"
|
||||
next
|
||||
end
|
||||
|
||||
Rails.logger.debug "Extracting #{entry.name} to #{extraction_path}"
|
||||
|
||||
FileUtils.mkdir_p(File.dirname(extraction_path))
|
||||
|
||||
# Use destination_directory parameter for rubyzip 3.x compatibility
|
||||
entry.extract(entry.name, destination_directory: @import_directory)
|
||||
entry.extract(sanitized_name, destination_directory: @import_directory)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sanitize_zip_entry_name(entry_name)
|
||||
# Remove leading slashes, backslashes, and dots
|
||||
sanitized = entry_name.gsub(%r{^[/\\]+}, '')
|
||||
|
||||
# Reject entries with path traversal attempts
|
||||
if sanitized.include?('..') || sanitized.start_with?('/') || sanitized.start_with?('\\')
|
||||
Rails.logger.warn "Rejecting potentially malicious ZIP entry name: #{entry_name}"
|
||||
return nil
|
||||
end
|
||||
|
||||
# Reject absolute paths
|
||||
if Pathname.new(sanitized).absolute?
|
||||
Rails.logger.warn "Rejecting absolute path in ZIP entry: #{entry_name}"
|
||||
return nil
|
||||
end
|
||||
|
||||
sanitized
|
||||
end
|
||||
|
||||
def load_json_data
|
||||
json_path = @import_directory.join('data.json')
|
||||
|
||||
|
|
|
|||
|
|
@ -76,10 +76,16 @@
|
|||
<div class="join">
|
||||
<%= link_to "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}" do %>
|
||||
<span class="join-item btn btn-sm <%= trial_button_class(current_user) %>">
|
||||
<% if current_user.active_until.past? %>
|
||||
<span class="tooltip tooltip-bottom">Trial expired 🥺</span>
|
||||
<% expiry = current_user.active_until %>
|
||||
<% if expiry.blank? || expiry.past? %>
|
||||
<span class="tooltip tooltip-bottom" data-tip="Trial expired" title="Trial expired">Trial expired 🥺</span>
|
||||
<% else %>
|
||||
<span class="tooltip tooltip-bottom" data-tip="Your trial will end in <%= distance_of_time_in_words(current_user.active_until, Time.current) %>"><%= (current_user.active_until.to_date - Time.current.to_date).to_i %> days remaining</span>
|
||||
<% days_left = [(expiry.to_date - Time.zone.today).to_i, 0].max %>
|
||||
<span class="tooltip tooltip-bottom"
|
||||
data-tip="Your trial will end in <%= distance_of_time_in_words(expiry, Time.current) %>"
|
||||
title="Your trial will end in <%= distance_of_time_in_words(expiry, Time.current) %>">
|
||||
<%= pluralize(days_left, 'day') %> remaining
|
||||
</span>
|
||||
<% end %>
|
||||
</span><span class="join-item btn btn-sm btn-success">
|
||||
Subscribe
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if defined?(Rails::Server) && !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
|
||||
# Initialize Prometheus exporter for web processes, but exclude console, rake tasks, and tests
|
||||
should_initialize = DawarichSettings.prometheus_exporter_enabled? &&
|
||||
!Rails.env.test? &&
|
||||
!defined?(Rails::Console) &&
|
||||
!File.basename($PROGRAM_NAME).include?('rake')
|
||||
|
||||
if should_initialize
|
||||
require 'prometheus_exporter/middleware'
|
||||
require 'prometheus_exporter/instrumentation'
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Rails.application.routes.draw do
|
|||
to: 'stats#update',
|
||||
as: :update_year_month_stats,
|
||||
constraints: { year: /\d{4}/, month: /\d{1,2}|all/ }
|
||||
get 'shared/stats/:uuid', to: 'shared/stats#show', as: :shared_stat
|
||||
get 'shared/month/:uuid', to: 'shared/stats#show', as: :shared_stat
|
||||
|
||||
# Sharing management endpoint (requires auth)
|
||||
patch 'stats/:year/:month/sharing',
|
||||
|
|
@ -85,7 +85,6 @@ Rails.application.routes.draw do
|
|||
|
||||
root to: 'home#index'
|
||||
|
||||
# iOS mobile auth success endpoint
|
||||
get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success
|
||||
|
||||
if SELF_HOSTED
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ daily_track_generation_job:
|
|||
nightly_reverse_geocoding_job:
|
||||
cron: "15 1 * * *" # every day at 01:15
|
||||
class: "Points::NightlyReverseGeocodingJob"
|
||||
queue: tracks
|
||||
queue: reverse_geocoding
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSharingFieldsToStats < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_column :stats, :sharing_settings, :jsonb
|
||||
add_column :stats, :sharing_uuid, :uuid
|
||||
|
||||
change_column_default :stats, :sharing_settings, {}
|
||||
|
||||
BulkStatsCalculatingJob.set(wait: 5.minutes).perform_later
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -1,19 +1 @@
|
|||
{
|
||||
"name": "Dawarich",
|
||||
"short_name": "Dawarich",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/favicon/android-chrome-192x192-f9610e2af28e4e48ff0472572c0cb9e3902d29bccc2b07f8f03aabf684822355.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/favicon/android-chrome-512x512-c2ec8132d773ae99f53955360cdd5691bb38e0ed141bddebd39d896b78b5afb6.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
{"name":"Dawarich","short_name":"Dawarich","icons":[{"src":"/assets/favicon/android-chrome-192x192-f9610e2af28e4e48ff0472572c0cb9e3902d29bccc2b07f8f03aabf684822355.png","sizes":"192x192","type":"image/png"},{"src":"/assets/favicon/android-chrome-512x512-c2ec8132d773ae99f53955360cdd5691bb38e0ed141bddebd39d896b78b5afb6.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
|
|
@ -26,10 +26,8 @@ RSpec.describe Tracks::DailyGenerationJob, type: :job do
|
|||
active_user.update!(points_count: active_user.points.count)
|
||||
trial_user.update!(points_count: trial_user.points.count)
|
||||
|
||||
# Mock User.active_or_trial to only return test users
|
||||
active_or_trial_mock = double('ActiveRecord::Relation')
|
||||
allow(User).to receive(:active_or_trial).and_return(active_or_trial_mock)
|
||||
allow(active_or_trial_mock).to receive(:find_each).and_yield(active_user).and_yield(trial_user)
|
||||
allow(User).to receive(:active_or_trial)
|
||||
.and_return(User.where(id: [active_user.id, trial_user.id]))
|
||||
|
||||
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe 'Shared::Stats', type: :request do
|
|||
let(:user) { create(:user) }
|
||||
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
|
||||
|
||||
describe 'GET /shared/stats/:uuid' do
|
||||
describe 'GET /shared/month/:uuid' do
|
||||
context 'with valid sharing UUID' do
|
||||
before do
|
||||
# Create some test points for data bounds calculation
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ RSpec.describe Api::PointSerializer do
|
|||
let(:all_excluded) { Api::PointSerializer::EXCLUDED_ATTRIBUTES }
|
||||
let(:expected_json) do
|
||||
point.attributes.except(*all_excluded).tap do |attributes|
|
||||
# API serializer extracts coordinates from PostGIS geometry
|
||||
attributes['latitude'] = point.lat.to_s
|
||||
attributes['longitude'] = point.lon.to_s
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Maps::DateParameterCoercer do
|
||||
describe '.call' do
|
||||
subject(:coerce_date) { described_class.new(param).call }
|
||||
|
||||
context 'with integer parameter' do
|
||||
let(:param) { 1_717_200_000 }
|
||||
|
||||
it 'returns the integer unchanged' do
|
||||
expect(coerce_date).to eq(1_717_200_000)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with numeric string parameter' do
|
||||
let(:param) { '1717200000' }
|
||||
|
||||
it 'converts to integer' do
|
||||
expect(coerce_date).to eq(1_717_200_000)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ISO date string parameter' do
|
||||
let(:param) { '2024-06-01T00:00:00Z' }
|
||||
|
||||
it 'parses and converts to timestamp' do
|
||||
expected_timestamp = Time.parse('2024-06-01T00:00:00Z').to_i
|
||||
expect(coerce_date).to eq(expected_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with date string parameter' do
|
||||
let(:param) { '2024-06-01' }
|
||||
|
||||
it 'parses and converts to timestamp' do
|
||||
expected_timestamp = Time.parse('2024-06-01').to_i
|
||||
expect(coerce_date).to eq(expected_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid date string' do
|
||||
let(:param) { 'invalid-date' }
|
||||
|
||||
it 'raises InvalidDateFormatError' do
|
||||
expect { coerce_date }.to raise_error(
|
||||
Maps::DateParameterCoercer::InvalidDateFormatError,
|
||||
'Invalid date format: invalid-date'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil parameter' do
|
||||
let(:param) { nil }
|
||||
|
||||
it 'converts to 0' do
|
||||
expect(coerce_date).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with float parameter' do
|
||||
let(:param) { 1_717_200_000.5 }
|
||||
|
||||
it 'converts to integer' do
|
||||
expect(coerce_date).to eq(1_717_200_000)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -81,43 +81,4 @@ RSpec.describe Stats::HexagonCalculator do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#calculate_h3_hex_ids' do
|
||||
subject(:calculate_hex_ids) { described_class.new(user.id, year, month).calculate_h3_hex_ids }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:year) { 2024 }
|
||||
let(:month) { 1 }
|
||||
|
||||
context 'when there are no points' do
|
||||
it 'returns empty hash' do
|
||||
expect(calculate_hex_ids).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are points' do
|
||||
let(:timestamp1) { DateTime.new(year, month, 1, 12).to_i }
|
||||
let!(:import) { create(:import, user:) }
|
||||
let!(:point1) do
|
||||
create(:point,
|
||||
user:,
|
||||
import:,
|
||||
timestamp: timestamp1,
|
||||
lonlat: 'POINT(14.452712811406352 52.107902115161316)')
|
||||
end
|
||||
|
||||
it 'returns hash with H3 hex IDs' do
|
||||
result = calculate_hex_ids
|
||||
|
||||
expect(result).to be_a(Hash)
|
||||
expect(result).not_to be_empty
|
||||
|
||||
result.each do |h3_index, data|
|
||||
expect(h3_index).to be_a(String)
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.size).to eq(3) # [count, earliest, latest]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue