Skip to content

Commit f308134

Browse files
authored
Merge pull request #2330 from Freika/feature/lite
Lite plan for Cloud
2 parents 317c231 + fda7ac1 commit f308134

File tree

68 files changed

+2478
-472
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2478
-472
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ Even in these cases, wrap the integration in a Stimulus controller and connect i
372372
4. **Testing**: Include both unit and integration tests for location-based features
373373
5. **Performance**: Consider database indexes for geographic queries
374374
6. **Security**: Never log or expose user location data inappropriately
375-
7. **Public Sharing**: When implementing features that interact with stats, consider public sharing access patterns:
375+
7. **Migrations**: Put all migrations (schema and data) in `db/migrate/`, not `db/data/`. Data manipulation migrations use the same `ActiveRecord::Migration` class and should run in the standard migration sequence.
376+
8. **Public Sharing**: When implementing features that interact with stats, consider public sharing access patterns:
376377
- Use `public_accessible?` method to check if a stat can be publicly accessed
377378
- Support UUID-based access in API endpoints when appropriate
378379
- Respect expiration settings and disable sharing when expired

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ gem 'pg'
3535
gem 'prometheus_exporter'
3636
gem 'puma'
3737
gem 'pundit', '>= 2.5.1'
38+
gem 'rack-attack'
3839
gem 'rails', '~> 8.0'
3940
gem 'rails_icons'
4041
gem 'rails_pulse'

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ GEM
393393
raabro (1.4.0)
394394
racc (1.8.1)
395395
rack (3.2.4)
396+
rack-attack (6.8.0)
397+
rack (>= 1.0, < 4)
396398
rack-oauth2 (2.3.0)
397399
activesupport
398400
attr_required
@@ -696,6 +698,7 @@ DEPENDENCIES
696698
pry-rails
697699
puma
698700
pundit (>= 2.5.1)
701+
rack-attack
699702
rails (~> 8.0)
700703
rails_icons
701704
rails_pulse

app/assets/builds/tailwind.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controllers/api/v1/points_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ class Api::V1::PointsController < ApiController
44
include SafeTimestampParser
55

66
before_action :authenticate_active_api_user!, only: %i[create update destroy bulk_destroy]
7+
before_action :require_write_api!, only: %i[create update destroy bulk_destroy]
78
before_action :validate_points_limit, only: %i[create]
89

910
def index
1011
start_at = params[:start_at].present? ? safe_timestamp(params[:start_at]) : nil
1112
end_at = params[:end_at].present? ? safe_timestamp(params[:end_at]) : Time.zone.now.to_i
1213
order = params[:order] || 'desc'
1314

14-
points = current_api_user
15-
.points
15+
points = scoped_points
1616
.without_raw_data
1717
.where(timestamp: start_at..end_at)
1818

app/controllers/api/v1/settings_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ def index
1010
}, status: :ok
1111
end
1212

13+
# NOTE: For Lite plan users, Pro-only settings (gated map layers, globe_projection)
14+
# are silently stripped before persistence by TransportationThresholdsUpdater.
15+
# The response reflects the filtered state via safe_settings.config.
1316
def update
1417
result = Users::TransportationThresholdsUpdater.new(current_api_user, settings_params).call
1518

app/controllers/api/v1/subscriptions_controller.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ def callback
77
decoded_token = Subscription::DecodeJwtToken.new(params[:token]).call
88

99
user = User.find(decoded_token[:user_id])
10-
user.update!(status: decoded_token[:status], active_until: decoded_token[:active_until])
10+
attrs = { status: decoded_token[:status], active_until: decoded_token[:active_until] }
11+
12+
if decoded_token[:plan].present?
13+
unless User.plans.key?(decoded_token[:plan])
14+
return render json: { message: "Invalid plan: #{decoded_token[:plan]}" }, status: :unprocessable_content
15+
end
16+
17+
attrs[:plan] = decoded_token[:plan]
18+
end
19+
20+
user.update!(attrs)
21+
22+
# Bust rate-limit plan cache so new limits take effect immediately
23+
Rails.cache.delete("rack_attack/plan/#{user.api_key}") if attrs.key?(:plan)
1124

1225
render json: { message: 'Subscription updated successfully' }
1326
rescue JWT::DecodeError => e

app/controllers/api/v1/tracks/points_controller.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ def index
66

77
# First try to get points directly associated with the track
88
points = track.points.without_raw_data.includes(:country).order(timestamp: :asc)
9+
points = apply_plan_scope(points)
910

1011
# If no points are associated, fall back to fetching by time range
1112
# This handles tracks created before point association was implemented
1213
if points.empty?
13-
points = current_api_user.points
14-
.without_raw_data
15-
.includes(:country)
16-
.where(timestamp: track.start_at.to_i..track.end_at.to_i)
17-
.order(timestamp: :asc)
14+
points = scoped_points
15+
.without_raw_data
16+
.includes(:country)
17+
.where(timestamp: track.start_at.to_i..track.end_at.to_i)
18+
.order(timestamp: :asc)
1819
end
1920

2021
# Support optional pagination (backward compatible - returns all if no page param)

app/controllers/api_controller.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class ApiController < ApplicationController
44
skip_before_action :verify_authenticity_token
55
before_action :set_version_header
66
before_action :authenticate_api_key
7+
after_action :set_rate_limit_headers
78

89
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
910

@@ -37,6 +38,45 @@ def authenticate_api_key
3738
true
3839
end
3940

41+
def require_pro_api!
42+
return unless current_api_user # auth already handled by authenticate_api_key
43+
return if DawarichSettings.self_hosted?
44+
return if current_api_user.pro?
45+
46+
render json: {
47+
error: 'pro_plan_required',
48+
message: 'This feature requires a Pro plan.',
49+
upgrade_url: DawarichSettings::UPGRADE_URL
50+
}, status: :forbidden
51+
end
52+
53+
def require_write_api!
54+
return unless current_api_user # auth already handled by authenticate_api_key
55+
return if DawarichSettings.self_hosted?
56+
return if current_api_user.pro?
57+
58+
render json: {
59+
error: 'write_api_restricted',
60+
message: 'Write API access requires a Pro plan. Your data was not modified.',
61+
upgrade_url: DawarichSettings::UPGRADE_URL
62+
}, status: :forbidden
63+
end
64+
65+
# Returns points scoped to the user's plan data window.
66+
# Lite users see only the last 12 months; Pro users see everything.
67+
def scoped_points(user = current_api_user)
68+
apply_plan_scope(user.points, user)
69+
end
70+
71+
# Applies the 12-month plan window to any point relation.
72+
# Use this when scoping points that don't start from user.points (e.g. track.points).
73+
def apply_plan_scope(relation, user = current_api_user)
74+
return relation if DawarichSettings.self_hosted?
75+
return relation unless user&.lite?
76+
77+
relation.where('timestamp >= ?', 12.months.ago.to_i)
78+
end
79+
4080
def authenticate_active_api_user!
4181
if current_api_user.nil?
4282
render json: { error: 'User account is not active or has been deleted' }, status: :unauthorized
@@ -82,4 +122,21 @@ def validate_points_limit
82122

83123
render json: { error: 'Points limit exceeded' }, status: :unauthorized if limit_exceeded
84124
end
125+
126+
def set_rate_limit_headers
127+
return unless current_api_user
128+
return if DawarichSettings.self_hosted?
129+
130+
throttle_data = request.env['rack.attack.throttle_data']&.dig('api/token')
131+
return unless throttle_data
132+
133+
limit = throttle_data[:limit]
134+
count = throttle_data[:count]
135+
period = throttle_data[:period]
136+
now = Time.zone.now.to_i
137+
138+
response.set_header('X-RateLimit-Limit', limit.to_s)
139+
response.set_header('X-RateLimit-Remaining', [limit - count, 0].max.to_s)
140+
response.set_header('X-RateLimit-Reset', (now + (period - (now % period))).to_s)
141+
end
85142
end

app/controllers/application_controller.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ def after_sign_in_path_for(resource)
6666
end
6767
end
6868

69+
def require_pro!
70+
return if DawarichSettings.self_hosted?
71+
72+
unless current_user
73+
respond_to do |format|
74+
format.html { redirect_to new_user_session_path, alert: 'Please sign in to continue.', status: :see_other }
75+
format.json { render json: { error: 'You need to sign in first.' }, status: :unauthorized }
76+
format.turbo_stream do
77+
redirect_to new_user_session_path, alert: 'Please sign in to continue.', status: :see_other
78+
end
79+
end
80+
return
81+
end
82+
83+
return if current_user.pro?
84+
85+
respond_to do |format|
86+
format.html do
87+
redirect_back fallback_location: root_path,
88+
alert: 'This feature requires a Pro plan.',
89+
status: :see_other
90+
end
91+
format.json { render json: { error: 'This feature requires a Pro plan.' }, status: :forbidden }
92+
format.turbo_stream do
93+
redirect_back fallback_location: root_path,
94+
alert: 'This feature requires a Pro plan.',
95+
status: :see_other
96+
end
97+
end
98+
end
99+
69100
def ensure_family_feature_enabled!
70101
return if DawarichSettings.family_feature_enabled?
71102

0 commit comments

Comments
 (0)