diff --git a/Gemfile b/Gemfile index 3929ec9d..ec872bfe 100644 --- a/Gemfile +++ b/Gemfile @@ -42,8 +42,10 @@ gem 'strong_migrations' gem 'tailwindcss-rails' gem 'turbo-rails' 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 1ce8cf07..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) @@ -181,6 +183,8 @@ GEM json (2.10.1) json-schema (5.0.1) addressable (~> 2.8) + jwt (2.10.1) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -474,6 +478,7 @@ DEPENDENCIES aws-sdk-kms (~> 1.96.0) aws-sdk-s3 (~> 1.177.0) bootsnap + brakeman chartkick data_migrate database_consistency @@ -489,6 +494,7 @@ DEPENDENCIES groupdate httparty importmap-rails + jwt kaminari lograge oj 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 new file mode 100644 index 00000000..05c39cbd --- /dev/null +++ b/app/controllers/settings/subscriptions_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Settings::SubscriptionsController < ApplicationController + before_action :authenticate_user! + before_action :authenticate_non_self_hosted! + + def index; end + + def subscription_callback + token = params[:token] + + begin + decoded_token = JWT.decode( + token, + ENV['JWT_SECRET_KEY'], + true, + { algorithm: 'HS256' } + ).first.symbolize_keys + + 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], active_until: decoded_token[:active_until]) + + redirect_to settings_subscriptions_path, notice: 'Your subscription has been updated successfully!' + rescue JWT::DecodeError + redirect_to settings_subscriptions_path, alert: 'Failed to verify subscription update.' + rescue ArgumentError + redirect_to settings_subscriptions_path, alert: 'Invalid subscription data received.' + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e9045092..c66e3262 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -137,4 +137,17 @@ module ApplicationHelper speed * 3.6 end + + def days_left(active_until) + return unless active_until + + time_words = distance_of_time_in_words(Time.zone.now, active_until) + + content_tag( + :span, + time_words, + class: 'tooltip', + data: { tip: "Expires on #{active_until.iso8601}" } + ) + end end 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/models/user.rb b/app/models/user.rb index fd56c07b..dc0bb532 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -100,6 +100,22 @@ class User < ApplicationRecord end end + def can_subscribe? + active_until&.past? && !DawarichSettings.self_hosted? + end + + def generate_subscription_token + payload = { + user_id: id, + email: email, + exp: 30.minutes.from_now.to_i + } + + secret_key = ENV['JWT_SECRET_KEY'] + + JWT.encode(payload, secret_key, 'HS256') + end + private def create_api_key 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/app/views/settings/_navigation.html.erb b/app/views/settings/_navigation.html.erb index 8b5e51e0..40ec1ddb 100644 --- a/app/views/settings/_navigation.html.erb +++ b/app/views/settings/_navigation.html.erb @@ -5,4 +5,7 @@ <%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab #{active_tab?(settings_background_jobs_path)}" %> <% end %> <%= link_to 'Map', settings_maps_path, role: 'tab', class: "tab #{active_tab?(settings_maps_path)}" %> + <% if !DawarichSettings.self_hosted? %> + <%= link_to 'Subscriptions', settings_subscriptions_path, role: 'tab', class: "tab #{active_tab?(settings_subscriptions_path)}" %> + <% end %> diff --git a/app/views/settings/subscriptions/index.html.erb b/app/views/settings/subscriptions/index.html.erb new file mode 100644 index 00000000..093b58a9 --- /dev/null +++ b/app/views/settings/subscriptions/index.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, "Subscriptions" %> + +
+ <%= render 'settings/navigation' %> + +
+
+
+

Hello there!

+ <% if current_user.active_until.future? %> +

+ You are currently subscribed to Dawarich, hurray! +

+ +

+ Your subscription will be valid for the next <%= days_left(current_user.active_until) %>. +

+ + <%= link_to 'Manage subscription', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %> + <% else %> +

+ You are currently not subscribed to Dawarich. How about we fix that? +

+ + <%= link_to 'Manage subscription', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %> + <% end %> +
+
+
+
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index cbbc32a4..0621b407 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -19,6 +19,9 @@ + <% if user_signed_in? && current_user.can_subscribe? %> +
  • <%= link_to 'Subscribe', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %>
  • + <% end %> <%= link_to 'Dawarich', root_path, class: 'btn btn-ghost normal-case text-xl'%> @@ -67,6 +70,10 @@