diff --git a/Gemfile b/Gemfile index 402ebaf7..ec872bfe 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'jwt' group :development, :test do + gem 'brakeman', require: false gem 'debug', platforms: %i[mri mingw x64_mingw] gem 'dotenv-rails' gem 'factory_bot_rails' diff --git a/Gemfile.lock b/Gemfile.lock index 51383c7f..ff5a3ab7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,6 +101,8 @@ GEM bigdecimal (3.1.9) bootsnap (1.18.4) msgpack (~> 1.2) + brakeman (7.0.2) + racc builder (3.3.0) byebug (11.1.3) chartkick (5.1.3) @@ -476,6 +478,7 @@ DEPENDENCIES aws-sdk-kms (~> 1.96.0) aws-sdk-s3 (~> 1.177.0) bootsnap + brakeman chartkick data_migrate database_consistency diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 314c143c..500b9711 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -31,6 +31,12 @@ class ApplicationController < ActionController::Base redirect_to root_path, notice: 'Your account is not active.', status: :see_other end + def authenticate_non_self_hosted! + return unless DawarichSettings.self_hosted? + + redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other + end + private def set_self_hosted_status diff --git a/app/controllers/settings/subscriptions_controller.rb b/app/controllers/settings/subscriptions_controller.rb index 4e91e108..05c39cbd 100644 --- a/app/controllers/settings/subscriptions_controller.rb +++ b/app/controllers/settings/subscriptions_controller.rb @@ -2,6 +2,7 @@ class Settings::SubscriptionsController < ApplicationController before_action :authenticate_user! + before_action :authenticate_non_self_hosted! def index; end @@ -16,13 +17,12 @@ class Settings::SubscriptionsController < ApplicationController { algorithm: 'HS256' } ).first.symbolize_keys - # Verify this is for the current user unless decoded_token[:user_id] == current_user.id redirect_to settings_subscriptions_path, alert: 'Invalid subscription update request.' return end - current_user.update!(status: decoded_token[:status]) + current_user.update!(status: decoded_token[:status], active_until: decoded_token[:active_until]) redirect_to settings_subscriptions_path, notice: 'Your subscription has been updated successfully!' rescue JWT::DecodeError diff --git a/app/models/concerns/distanceable.rb b/app/models/concerns/distanceable.rb index 6b2d1546..72b12792 100644 --- a/app/models/concerns/distanceable.rb +++ b/app/models/concerns/distanceable.rb @@ -59,12 +59,11 @@ module Distanceable return 0 if points.length < 2 total_meters = points.each_cons(2).sum do |point1, point2| - connection.select_value(<<-SQL.squish) - SELECT ST_Distance( - ST_GeomFromEWKT('#{point1.lonlat}')::geography, - ST_GeomFromEWKT('#{point2.lonlat}')::geography - ) - SQL + connection.select_value( + 'SELECT ST_Distance(ST_GeomFromEWKT($1)::geography, ST_GeomFromEWKT($2)::geography)', + nil, + [point1.lonlat, point2.lonlat] + ) end total_meters.to_f / DISTANCE_UNITS[unit.to_sym] diff --git a/app/services/visits/find_within_bounding_box.rb b/app/services/visits/find_within_bounding_box.rb index 74b72ed7..d5bdb74a 100644 --- a/app/services/visits/find_within_bounding_box.rb +++ b/app/services/visits/find_within_bounding_box.rb @@ -12,13 +12,17 @@ module Visits end def call - bounding_box = "ST_MakeEnvelope(#{sw_lng}, #{sw_lat}, #{ne_lng}, #{ne_lat}, 4326)" - Visit .includes(:place) .where(user:) .joins(:place) - .where("ST_Contains(#{bounding_box}, ST_SetSRID(places.lonlat::geometry, 4326))") + .where( + 'ST_Contains(ST_MakeEnvelope(?, ?, ?, ?, 4326), ST_SetSRID(places.lonlat::geometry, 4326))', + sw_lng, + sw_lat, + ne_lng, + ne_lat + ) .order(started_at: :desc) end diff --git a/spec/jobs/bulk_visits_suggesting_job_spec.rb b/spec/jobs/bulk_visits_suggesting_job_spec.rb index 6c76c745..b4545701 100644 --- a/spec/jobs/bulk_visits_suggesting_job_spec.rb +++ b/spec/jobs/bulk_visits_suggesting_job_spec.rb @@ -7,7 +7,7 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do let(:start_at) { 1.day.ago.beginning_of_day } let(:end_at) { 1.day.ago.end_of_day } let(:user) { create(:user) } - let(:inactive_user) { create(:user, status: :inactive) } + let(:inactive_user) { create(:user, :inactive) } let(:user_with_points) { create(:user) } let(:time_chunks) { [[start_at, end_at]] } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 09966000..99aa9f68 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -31,7 +31,7 @@ RSpec.describe User, type: :model do describe '#activate' do context 'when self-hosted' do - let!(:user) { create(:user, status: :inactive, active_until: 1.day.ago) } + let!(:user) { create(:user, :inactive) } before do allow(DawarichSettings).to receive(:self_hosted?).and_return(true) @@ -49,7 +49,7 @@ RSpec.describe User, type: :model do end it 'does not activate user' do - user = create(:user, status: :inactive, active_until: 1.day.ago) + user = create(:user, :inactive) expect(user.active?).to be_falsey expect(user.active_until).to be_within(1.minute).of(1.day.ago) @@ -194,7 +194,7 @@ RSpec.describe User, type: :model do end context 'when user is inactive' do - let(:user) { create(:user, status: :inactive, active_until: 1.day.ago) } + let(:user) { create(:user, :inactive) } it 'returns false' do expect(user.can_subscribe?).to be_falsey @@ -216,7 +216,7 @@ RSpec.describe User, type: :model do end context 'when user is inactive' do - let(:user) { create(:user, status: :inactive, active_until: 1.day.ago) } + let(:user) { create(:user, :inactive) } it 'returns true' do expect(user.can_subscribe?).to be_truthy diff --git a/spec/requests/settings/subscriptions_spec.rb b/spec/requests/settings/subscriptions_spec.rb index 09862f55..15fff7e9 100644 --- a/spec/requests/settings/subscriptions_spec.rb +++ b/spec/requests/settings/subscriptions_spec.rb @@ -1,7 +1,141 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe "Settings::Subscriptions", type: :request do - describe "GET /index" do - pending "add some examples (or delete) #{__FILE__}" +RSpec.describe 'Settings::Subscriptions', type: :request do + let(:user) { create(:user, :inactive) } + let(:jwt_secret) { ENV['JWT_SECRET_KEY'] } + + before do + stub_const('ENV', ENV.to_h.merge('JWT_SECRET_KEY' => 'test_secret')) + stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags') + .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {}) + end + + context 'when Dawarich is not self-hosted' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + end + + describe 'GET /settings/subscriptions' do + context 'when user is not authenticated' do + it 'redirects to login page' do + get settings_subscriptions_path + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when user is authenticated' do + before { sign_in user } + + it 'returns successful response' do + get settings_subscriptions_path + + expect(response).to be_successful + end + end + end + + describe 'GET /settings/subscriptions/callback' do + context 'when user is not authenticated' do + it 'redirects to login page' do + get subscription_callback_settings_subscriptions_path(token: 'invalid') + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when user is authenticated' do + before { sign_in user } + + context 'with valid token' do + let(:token) do + JWT.encode( + { user_id: user.id, status: 'active', active_until: 1.year.from_now }, + jwt_secret, + 'HS256' + ) + end + + it 'updates user status and redirects with success message' do + get subscription_callback_settings_subscriptions_path(token: token) + + expect(user.reload.status).to eq('active') + expect(user.active_until).to be_within(1.day).of(1.year.from_now) + expect(response).to redirect_to(settings_subscriptions_path) + expect(flash[:notice]).to eq('Your subscription has been updated successfully!') + end + end + + context 'with token for different user' do + let(:other_user) { create(:user) } + let(:token) do + JWT.encode( + { user_id: other_user.id, status: 'active' }, + jwt_secret, + 'HS256' + ) + end + + it 'does not update status and redirects with error' do + get subscription_callback_settings_subscriptions_path(token: token) + + expect(user.reload.status).not_to eq('active') + expect(response).to redirect_to(settings_subscriptions_path) + expect(flash[:alert]).to eq('Invalid subscription update request.') + end + end + + context 'with invalid token' do + it 'redirects with decode error message' do + get subscription_callback_settings_subscriptions_path(token: 'invalid') + + expect(response).to redirect_to(settings_subscriptions_path) + expect(flash[:alert]).to eq('Failed to verify subscription update.') + end + end + + context 'with malformed token data' do + let(:token) do + JWT.encode({ user_id: 'invalid', status: nil }, jwt_secret, 'HS256') + end + + it 'redirects with invalid data message' do + get subscription_callback_settings_subscriptions_path(token: token) + + expect(response).to redirect_to(settings_subscriptions_path) + expect(flash[:alert]).to eq('Invalid subscription update request.') + end + end + end + end + end + + context 'when Dawarich is self-hosted' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) + sign_in user + end + + describe 'GET /settings/subscriptions' do + context 'when user is not authenticated' do + it 'redirects to root path' do + get settings_subscriptions_path + + expect(response).to redirect_to(root_path) + end + end + end + + describe 'GET /settings/subscriptions/callback' do + context 'when user is not authenticated' do + it 'redirects to root path' do + get subscription_callback_settings_subscriptions_path(token: 'invalid') + + expect(response).to redirect_to(root_path) + end + end + end end end