diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa48f62..e35e26e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Hexagons for the stats page are now being calculated a lot faster. - Prometheus exporter is now not being started when console is being run. - Stats will now properly reflect countries and cities visited after importing new points. +- `GET /api/v1/points will now return correct latitude and longitude values. #1502 +- Deleting an import will now trigger stats recalculation for affected months. #1789 ## Changed - Onboarding modal window now features a link to the App Store and a QR code to configure the Dawarich iOS app. - A permanent option was removed from stats sharing options. Now, stats can be shared for 1, 12 or 24 hours only. +## Added + +- Added foundation for upcoming authentication from iOS app. + # [0.32.0] - 2025-09-13 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 29062343..ba20b793 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized - before_action :unread_notifications, :set_self_hosted_status + before_action :unread_notifications, :set_self_hosted_status, :store_client_header protected @@ -39,12 +39,36 @@ class ApplicationController < ActionController::Base user_not_authorized 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 + when 'ios' + payload = { api_key: resource.api_key, exp: 5.minutes.from_now.to_i } + + token = Subscription::EncodeJwtToken.new( + payload, ENV['AUTH_JWT_SECRET_KEY'] + ).call + + ios_success_path(token:) + else + super + end + end + private def set_self_hosted_status @self_hosted = DawarichSettings.self_hosted? end + def store_client_header + return unless request.headers['X-Dawarich-Client'] + + session[:dawarich_client] = request.headers['X-Dawarich-Client'] + end + def user_not_authorized redirect_back fallback_location: root_path, alert: 'You are not authorized to perform this action.', diff --git a/app/controllers/auth/ios_controller.rb b/app/controllers/auth/ios_controller.rb new file mode 100644 index 00000000..d03a0e2f --- /dev/null +++ b/app/controllers/auth/ios_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Auth + class IosController < ApplicationController + def success + # If token is provided, this is the final callback for ASWebAuthenticationSession + if params[:token].present? + # ASWebAuthenticationSession will capture this URL and extract the token + render plain: "Authentication successful! You can close this window.", status: :ok + else + # This should not happen with our current flow, but keeping for safety + render json: { + success: true, + message: 'iOS authentication successful', + redirect_url: root_url + }, status: :ok + end + end + + end +end \ No newline at end of file diff --git a/app/models/import.rb b/app/models/import.rb index 8635f2a9..b1abde92 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -11,6 +11,7 @@ class Import < ApplicationRecord after_commit -> { Import::ProcessJob.perform_later(id) unless skip_background_processing }, on: :create after_commit :remove_attached_file, on: :destroy + before_commit :recalculate_stats, on: :destroy, if: -> { points.exists? } validates :name, presence: true, uniqueness: { scope: :user_id } validate :file_size_within_limit, if: -> { user.trial? } @@ -63,8 +64,14 @@ class Import < ApplicationRecord def file_size_within_limit return unless file.attached? - if file.blob.byte_size > 11.megabytes - errors.add(:file, 'is too large. Trial users can only upload files up to 10MB.') + return unless file.blob.byte_size > 11.megabytes + + errors.add(:file, 'is too large. Trial users can only upload files up to 10MB.') + end + + def recalculate_stats + years_and_months_tracked.each do |year, month| + Stats::CalculatingJob.perform_later(user.id, year, month) end end end diff --git a/app/serializers/api/point_serializer.rb b/app/serializers/api/point_serializer.rb index e8484d38..1f5e3a0d 100644 --- a/app/serializers/api/point_serializer.rb +++ b/app/serializers/api/point_serializer.rb @@ -1,9 +1,26 @@ # frozen_string_literal: true -class Api::PointSerializer < PointSerializer - EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id import_id user_id raw_data country_id].freeze +class Api::PointSerializer + EXCLUDED_ATTRIBUTES = %w[ + created_at updated_at visit_id import_id user_id raw_data + country_id + ].freeze + + def initialize(point) + @point = point + end def call - point.attributes.except(*EXCLUDED_ATTRIBUTES) + point.attributes.except(*EXCLUDED_ATTRIBUTES).tap do |attributes| + lat = point.lat + lon = point.lon + + attributes['latitude'] = lat.nil? ? nil : lat.to_s + attributes['longitude'] = lon.nil? ? nil : lon.to_s + end end + + private + + attr_reader :point end diff --git a/app/serializers/api/user_serializer.rb b/app/serializers/api/user_serializer.rb index d3e89dfe..9d54ec32 100644 --- a/app/serializers/api/user_serializer.rb +++ b/app/serializers/api/user_serializer.rb @@ -6,15 +6,19 @@ class Api::UserSerializer end def call - { + data = { user: { email: user.email, theme: user.theme, created_at: user.created_at, updated_at: user.updated_at, - settings: settings, + settings: settings } } + + data.merge!(subscription: subscription) unless DawarichSettings.self_hosted? + + data end private @@ -41,4 +45,11 @@ class Api::UserSerializer fog_of_war_threshold: user.safe_settings.fog_of_war_threshold } end + + def subscription + { + status: user.status, + active_until: user.active_until + } + end end diff --git a/app/services/location_search/result_aggregator.rb b/app/services/location_search/result_aggregator.rb index 0c28000a..52d5d950 100644 --- a/app/services/location_search/result_aggregator.rb +++ b/app/services/location_search/result_aggregator.rb @@ -48,15 +48,16 @@ module LocationSearch last_point = sorted_points.last # Calculate visit duration - duration_minutes = if sorted_points.length > 1 - ((last_point[:timestamp] - first_point[:timestamp]) / 60.0).round - else - # Single point visit - estimate based on typical stay time - 15 # minutes - end + duration_minutes = + if sorted_points.any? + ((last_point[:timestamp] - first_point[:timestamp]) / 60.0).round + else + # Single point visit - estimate based on typical stay time + 15 # minutes + end # Find the most accurate point (lowest accuracy value means higher precision) - most_accurate_point = points.min_by { |p| p[:accuracy] || 999999 } + most_accurate_point = points.min_by { |p| p[:accuracy] || 999_999 } # Calculate average distance from search center average_distance = (points.sum { |p| p[:distance_meters] } / points.length).round(2) @@ -86,7 +87,7 @@ module LocationSearch hours = minutes / 60 remaining_minutes = minutes % 60 - if remaining_minutes == 0 + if remaining_minutes.zero? "~#{pluralize(hours, 'hour')}" else "~#{pluralize(hours, 'hour')} #{pluralize(remaining_minutes, 'minute')}" diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 1b0e0d85..bf654561 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -5,7 +5,7 @@
and take control over your location data.