From 11677b14aef8a76aa3579d48c8e3b6adc68a52f7 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 15 Jan 2025 21:52:59 +0100 Subject: [PATCH 01/11] Introduce self-hosted mode --- app/controllers/application_controller.rb | 6 ++++ app/controllers/settings/users_controller.rb | 2 +- app/views/settings/_navigation.html.erb | 2 +- config/initializers/01_constants.rb | 4 ++- config/initializers/03_dawarich_settings.rb | 4 +++ config/routes.rb | 13 ++++--- spec/requests/settings_spec.rb | 37 +++++++++++++++++++- spec/requests/users_spec.rb | 35 ++++++++++++++++++ 8 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 spec/requests/users_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6d104eab..f1a5e617 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,4 +18,10 @@ class ApplicationController < ActionController::Base redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other end + + def authenticate_self_hosted! + return if DawarichSettings.self_hosted? + + redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other + end end diff --git a/app/controllers/settings/users_controller.rb b/app/controllers/settings/users_controller.rb index 529785db..046d34ac 100644 --- a/app/controllers/settings/users_controller.rb +++ b/app/controllers/settings/users_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Settings::UsersController < ApplicationController - before_action :authenticate_user! before_action :authenticate_admin! + before_action :authenticate_self_hosted! def index @users = User.order(created_at: :desc) diff --git a/app/views/settings/_navigation.html.erb b/app/views/settings/_navigation.html.erb index b0b20437..8ce09ba9 100644 --- a/app/views/settings/_navigation.html.erb +++ b/app/views/settings/_navigation.html.erb @@ -1,6 +1,6 @@
<%= link_to 'Integrations', settings_path, role: 'tab', class: "tab #{active_tab?(settings_path)}" %> - <% if current_user.admin? %> + <% if DawarichSettings.self_hosted? && current_user.admin? %> <%= link_to 'Users', settings_users_path, role: 'tab', class: "tab #{active_tab?(settings_users_path)}" %> <%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab #{active_tab?(settings_background_jobs_path)}" %> <% end %> diff --git a/config/initializers/01_constants.rb b/config/initializers/01_constants.rb index ce760238..37a318ed 100644 --- a/config/initializers/01_constants.rb +++ b/config/initializers/01_constants.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +SELF_HOSTED = ENV.fetch('SELF_HOSTED', 'true') == 'true' + MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km').to_sym @@ -11,7 +13,7 @@ TELEMETRY_URL = 'https://influxdb2.frey.today/api/v2/write' # Reverse geocoding settings PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil) PHOTON_API_KEY = ENV.fetch('PHOTON_API_KEY', nil) -PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'true') == 'true' +PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'false') == 'true' GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil) # /Reverse geocoding settings diff --git a/config/initializers/03_dawarich_settings.rb b/config/initializers/03_dawarich_settings.rb index 87cf4817..aa80b763 100644 --- a/config/initializers/03_dawarich_settings.rb +++ b/config/initializers/03_dawarich_settings.rb @@ -17,5 +17,9 @@ class DawarichSettings def geoapify_enabled? @geoapify_enabled ||= GEOAPIFY_API_KEY.present? end + + def self_hosted? + @self_hosted ||= SELF_HOSTED + end end end diff --git a/config/routes.rb b/config/routes.rb index 8d28efde..5b370c5d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,10 +51,15 @@ Rails.application.routes.draw do constraints: { year: /\d{4}/, month: /\d{1,2}|all/ } root to: 'home#index' - devise_for :users, skip: [:registrations] - as :user do - get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration' - put 'users' => 'devise/registrations#update', :as => 'user_registration' + + if SELF_HOSTED + devise_for :users, skip: [:registrations] + as :user do + get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration' + put 'users' => 'devise/registrations#update', :as => 'user_registration' + end + else + devise_for :users end get 'map', to: 'map#index' diff --git a/spec/requests/settings_spec.rb b/spec/requests/settings_spec.rb index e214b1c4..0ced085e 100644 --- a/spec/requests/settings_spec.rb +++ b/spec/requests/settings_spec.rb @@ -58,7 +58,7 @@ RSpec.describe 'Settings', type: :request do end it 'generates an API key for the user' do - expect { post '/settings/generate_api_key' }.to change { user.reload.api_key } + expect { post '/settings/generate_api_key' }.to(change { user.reload.api_key }) end it 'redirects back' do @@ -83,4 +83,39 @@ RSpec.describe 'Settings', type: :request do expect(user.reload.settings).to eq(params[:settings]) end end + + describe 'GET /settings/users' do + let!(:user) { create(:user, admin: true) } + + before do + stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags') + .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {}) + + sign_in user + end + + context 'when self-hosted' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) + end + + it 'returns http success' do + get '/settings/users' + + expect(response).to have_http_status(:success) + end + end + + context 'when not self-hosted' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + end + + it 'redirects to root path' do + get '/settings/users' + + expect(response).to redirect_to(root_path) + end + end + end end diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb new file mode 100644 index 00000000..8c0bcdf5 --- /dev/null +++ b/spec/requests/users_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Users', type: :request do + before do + stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags') + .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {}) + end + + describe 'GET /users/sign_up' do + context 'when self-hosted' do + before do + stub_const('SELF_HOSTED', true) + end + + it 'returns http success' do + get '/users/sign_up' + expect(response).to have_http_status(:not_found) + end + end + + context 'when not self-hosted' do + before do + stub_const('SELF_HOSTED', false) + Rails.application.reload_routes! + end + + it 'returns http success' do + get '/users/sign_up' + expect(response).to have_http_status(:success) + end + end + end +end From 86fd2311f90cc400116fbba348681ad101aee49b Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 11:32:52 +0100 Subject: [PATCH 02/11] Fix importing Immich and Photoprism geolocation data for non-admin users --- CHANGELOG.md | 6 ++++ .../settings/background_jobs_controller.rb | 14 +++++++-- .../requests/settings/background_jobs_spec.rb | 30 ++++++++++++++++++- spec/requests/settings/users_spec.rb | 2 +- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aab7b37..7b90f69b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# 0.24.2 - 2025-02-15 + +## Fixed + +- Fixed a bug where background jobs to import Immich and Photoprism geolocation data data could not be created by non-admin users. + # 0.24.1 - 2025-02-13 ## Custom map tiles diff --git a/app/controllers/settings/background_jobs_controller.rb b/app/controllers/settings/background_jobs_controller.rb index 8079b7e5..113b1a58 100644 --- a/app/controllers/settings/background_jobs_controller.rb +++ b/app/controllers/settings/background_jobs_controller.rb @@ -2,7 +2,9 @@ class Settings::BackgroundJobsController < ApplicationController before_action :authenticate_user! - before_action :authenticate_admin! + before_action :authenticate_admin!, unless: lambda { + %w[start_immich_import start_photoprism_import].include?(params[:job_name]) + } def index @queues = Sidekiq::Queue.all @@ -13,7 +15,15 @@ class Settings::BackgroundJobsController < ApplicationController flash.now[:notice] = 'Job was successfully created.' - redirect_to settings_background_jobs_path, notice: 'Job was successfully created.' + redirect_path = + case params[:job_name] + when 'start_immich_import', 'start_photoprism_import' + imports_path + else + settings_background_jobs_path + end + + redirect_to redirect_path, notice: 'Job was successfully created.' end def destroy diff --git a/spec/requests/settings/background_jobs_spec.rb b/spec/requests/settings/background_jobs_spec.rb index 616b74d3..1f2a2791 100644 --- a/spec/requests/settings/background_jobs_spec.rb +++ b/spec/requests/settings/background_jobs_spec.rb @@ -17,7 +17,9 @@ RSpec.describe '/settings/background_jobs', type: :request do end context 'when user is authenticated' do - before { sign_in create(:user) } + let(:user) { create(:user, admin: false) } + + before { sign_in user } context 'when user is not an admin' do it 'redirects to root page' do @@ -26,6 +28,32 @@ RSpec.describe '/settings/background_jobs', type: :request do expect(response).to redirect_to(root_url) expect(flash[:notice]).to eq('You are not authorized to perform this action.') end + + context 'when job name is start_immich_import' do + it 'redirects to imports page' do + post settings_background_jobs_url, params: { job_name: 'start_immich_import' } + + expect(response).to redirect_to(imports_url) + end + + it 'enqueues a new job' do + expect do + post settings_background_jobs_url, params: { job_name: 'start_immich_import' } + end.to have_enqueued_job(EnqueueBackgroundJob) + end + end + + context 'when job name is start_photoprism_import' do + it 'redirects to imports page' do + get settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } + end + + it 'enqueues a new job' do + expect do + post settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } + end.to have_enqueued_job(EnqueueBackgroundJob) + end + end end context 'when user is an admin' do diff --git a/spec/requests/settings/users_spec.rb b/spec/requests/settings/users_spec.rb index d2cc7806..ddec5949 100644 --- a/spec/requests/settings/users_spec.rb +++ b/spec/requests/settings/users_spec.rb @@ -10,7 +10,7 @@ RSpec.describe '/settings/users', type: :request do it 'redirects to sign in page' do post settings_users_url, params: { user: valid_attributes } - expect(response).to redirect_to(new_user_session_url) + expect(response).to redirect_to(root_url) end end From 74cfc9020e1f69b92f972cb6c688b038b23f40a5 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 11:40:51 +0100 Subject: [PATCH 03/11] Restrict background jobs to self-hosted mode --- .../settings/background_jobs_controller.rb | 1 + .../requests/settings/background_jobs_spec.rb | 231 +++++++++++++----- 2 files changed, 169 insertions(+), 63 deletions(-) diff --git a/app/controllers/settings/background_jobs_controller.rb b/app/controllers/settings/background_jobs_controller.rb index 113b1a58..4e71d4b9 100644 --- a/app/controllers/settings/background_jobs_controller.rb +++ b/app/controllers/settings/background_jobs_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Settings::BackgroundJobsController < ApplicationController + before_action :authenticate_self_hosted! before_action :authenticate_user! before_action :authenticate_admin!, unless: lambda { %w[start_immich_import start_photoprism_import].include?(params[:job_name]) diff --git a/spec/requests/settings/background_jobs_spec.rb b/spec/requests/settings/background_jobs_spec.rb index 1f2a2791..6ed203e4 100644 --- a/spec/requests/settings/background_jobs_spec.rb +++ b/spec/requests/settings/background_jobs_spec.rb @@ -8,97 +8,202 @@ RSpec.describe '/settings/background_jobs', type: :request do .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {}) end - context 'when user is not authenticated' do - it 'redirects to sign in page' do - get settings_background_jobs_url - - expect(response).to redirect_to(new_user_session_url) + context 'when Dawarich is in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) end - end - context 'when user is authenticated' do - let(:user) { create(:user, admin: false) } - - before { sign_in user } - - context 'when user is not an admin' do - it 'redirects to root page' do + context 'when user is not authenticated' do + it 'redirects to sign in page' do get settings_background_jobs_url - expect(response).to redirect_to(root_url) - expect(flash[:notice]).to eq('You are not authorized to perform this action.') - end - - context 'when job name is start_immich_import' do - it 'redirects to imports page' do - post settings_background_jobs_url, params: { job_name: 'start_immich_import' } - - expect(response).to redirect_to(imports_url) - end - - it 'enqueues a new job' do - expect do - post settings_background_jobs_url, params: { job_name: 'start_immich_import' } - end.to have_enqueued_job(EnqueueBackgroundJob) - end - end - - context 'when job name is start_photoprism_import' do - it 'redirects to imports page' do - get settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } - end - - it 'enqueues a new job' do - expect do - post settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } - end.to have_enqueued_job(EnqueueBackgroundJob) - end + expect(response).to redirect_to(new_user_session_url) end end - context 'when user is an admin' do - before { sign_in create(:user, :admin) } + context 'when user is authenticated' do + let(:user) { create(:user, admin: false) } - describe 'GET /index' do - it 'renders a successful response' do + before { sign_in user } + + context 'when user is not an admin' do + it 'redirects to root page' do get settings_background_jobs_url - expect(response).to be_successful + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + + context 'when job name is start_immich_import' do + it 'redirects to imports page' do + post settings_background_jobs_url, params: { job_name: 'start_immich_import' } + + expect(response).to redirect_to(imports_url) + end + + it 'enqueues a new job' do + expect do + post settings_background_jobs_url, params: { job_name: 'start_immich_import' } + end.to have_enqueued_job(EnqueueBackgroundJob) + end + end + + context 'when job name is start_photoprism_import' do + it 'redirects to imports page' do + get settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } + end + + it 'enqueues a new job' do + expect do + post settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } + end.to have_enqueued_job(EnqueueBackgroundJob) + end end end - describe 'POST /create' do - let(:params) { { job_name: 'start_reverse_geocoding' } } + context 'when user is an admin' do + before { sign_in create(:user, :admin) } - context 'with valid parameters' do - it 'enqueues a new job' do - expect do - post settings_background_jobs_url, params: - end.to have_enqueued_job(EnqueueBackgroundJob) + describe 'GET /index' do + it 'renders a successful response' do + get settings_background_jobs_url + + expect(response).to be_successful + end + end + + describe 'POST /create' do + let(:params) { { job_name: 'start_reverse_geocoding' } } + + context 'with valid parameters' do + it 'enqueues a new job' do + expect do + post settings_background_jobs_url, params: + end.to have_enqueued_job(EnqueueBackgroundJob) + end + + it 'redirects to the created settings_background_job' do + post(settings_background_jobs_url, params:) + + expect(response).to redirect_to(settings_background_jobs_url) + end + end + end + + describe 'DELETE /destroy' do + it 'clears the Sidekiq queue' do + queue = instance_double(Sidekiq::Queue) + allow(Sidekiq::Queue).to receive(:new).and_return(queue) + + expect(queue).to receive(:clear) + + delete settings_background_job_url('queue_name') end - it 'redirects to the created settings_background_job' do - post(settings_background_jobs_url, params:) + it 'redirects to the settings_background_jobs list' do + delete settings_background_job_url('queue_name') expect(response).to redirect_to(settings_background_jobs_url) end end end + end + end - describe 'DELETE /destroy' do - it 'clears the Sidekiq queue' do - queue = instance_double(Sidekiq::Queue) - allow(Sidekiq::Queue).to receive(:new).and_return(queue) + context 'when Dawarich is not in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + end - expect(queue).to receive(:clear) + context 'when user is not authenticated' do + it 'redirects to sign in page' do + get settings_background_jobs_url - delete settings_background_job_url('queue_name') + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + context 'when user is authenticated' do + let(:user) { create(:user) } + + before { sign_in user } + + describe 'GET /index' do + it 'redirects to root page' do + get settings_background_jobs_url + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') end - it 'redirects to the settings_background_jobs list' do + context 'when user is an admin' do + before { sign_in create(:user, :admin) } + + it 'redirects to root page' do + get settings_background_jobs_url + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + end + + describe 'POST /create' do + it 'redirects to root page' do + post settings_background_jobs_url, params: { job_name: 'start_reverse_geocoding' } + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + + context 'when job name is start_immich_import' do + it 'redirects to imports page' do + post settings_background_jobs_url, params: { job_name: 'start_immich_import' } + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + context 'when job name is start_photoprism_import' do + it 'redirects to imports page' do + post settings_background_jobs_url, params: { job_name: 'start_photoprism_import' } + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + context 'when user is an admin' do + before { sign_in create(:user, :admin) } + + it 'redirects to root page' do + get settings_background_jobs_url + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + end + + describe 'DELETE /destroy' do + it 'redirects to root page' do delete settings_background_job_url('queue_name') - expect(response).to redirect_to(settings_background_jobs_url) + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + context 'when user is an admin' do + before { sign_in create(:user, :admin) } + + it 'redirects to root page' do + get settings_background_jobs_url + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') end end end From 9e34d30383644e25957f830da90031c9ac900e0d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 11:45:53 +0100 Subject: [PATCH 04/11] Restrict user settings to self-hosted mode --- .../settings/background_jobs_controller.rb | 1 - spec/requests/settings/users_spec.rb | 152 +++++++++++------- 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/app/controllers/settings/background_jobs_controller.rb b/app/controllers/settings/background_jobs_controller.rb index 4e71d4b9..6eafb4c7 100644 --- a/app/controllers/settings/background_jobs_controller.rb +++ b/app/controllers/settings/background_jobs_controller.rb @@ -2,7 +2,6 @@ class Settings::BackgroundJobsController < ApplicationController before_action :authenticate_self_hosted! - before_action :authenticate_user! before_action :authenticate_admin!, unless: lambda { %w[start_immich_import start_photoprism_import].include?(params[:job_name]) } diff --git a/spec/requests/settings/users_spec.rb b/spec/requests/settings/users_spec.rb index ddec5949..51079587 100644 --- a/spec/requests/settings/users_spec.rb +++ b/spec/requests/settings/users_spec.rb @@ -6,81 +6,123 @@ RSpec.describe '/settings/users', type: :request do let(:valid_attributes) { { email: 'user@domain.com', password: '4815162342' } } let!(:admin) { create(:user, :admin) } - context 'when user is not authenticated' do - it 'redirects to sign in page' do - post settings_users_url, params: { user: valid_attributes } - - expect(response).to redirect_to(root_url) + context 'when Dawarich is in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) end - end - context 'when user is authenticated' do - context 'when user is not an admin' do - before { sign_in create(:user) } - - it 'redirects to root page' do + context 'when user is not authenticated' do + it 'redirects to sign in page' do post settings_users_url, params: { user: valid_attributes } expect(response).to redirect_to(root_url) end end - context 'when user is an admin' do - describe 'POST /create' do - before { sign_in admin } + context 'when user is authenticated' do + context 'when user is not an admin' do + before { sign_in create(:user) } - context 'with valid parameters' do - it 'creates a new User' do - expect do - post settings_users_url, params: { user: valid_attributes } - end.to change(User, :count).by(1) + it 'redirects to root page' do + post settings_users_url, params: { user: valid_attributes } - expect(User.last.email).to eq(valid_attributes[:email]) - expect(User.last.valid_password?(valid_attributes[:password])).to be_truthy - end - - it 'redirects to the created settings_user' do - post settings_users_url, params: { user: valid_attributes } - - expect(response).to redirect_to(settings_users_url) - expect(flash[:notice]).to eq('User was successfully created') - end - end - - context 'with invalid parameters' do - let(:invalid_attributes) { { email: nil } } - - it 'does not create a new User' do - expect do - post settings_users_url, params: { user: invalid_attributes } - end.to change(User, :count).by(0) - end - - it 'renders a response with 422 status (i.e. to display the "new" template)' do - post settings_users_url, params: { user: invalid_attributes } - - expect(response).to have_http_status(:unprocessable_entity) - end + expect(response).to redirect_to(root_url) end end - describe 'PATCH /update' do - let(:user) { create(:user) } + context 'when user is an admin' do + describe 'POST /create' do + before { sign_in admin } - before { sign_in admin } + context 'with valid parameters' do + it 'creates a new User' do + expect do + post settings_users_url, params: { user: valid_attributes } + end.to change(User, :count).by(1) - context 'with valid parameters' do - let(:new_attributes) { { email: FFaker::Internet.email, password: '4815162342' } } + expect(User.last.email).to eq(valid_attributes[:email]) + expect(User.last.valid_password?(valid_attributes[:password])).to be_truthy + end - it 'updates the requested user' do - patch settings_user_url(user), params: { user: new_attributes } + it 'redirects to the created settings_user' do + post settings_users_url, params: { user: valid_attributes } - user.reload - expect(user.email).to eq(new_attributes[:email]) - expect(user.valid_password?(new_attributes[:password])).to be_truthy + expect(response).to redirect_to(settings_users_url) + expect(flash[:notice]).to eq('User was successfully created') + end + end + + context 'with invalid parameters' do + let(:invalid_attributes) { { email: nil } } + + it 'does not create a new User' do + expect do + post settings_users_url, params: { user: invalid_attributes } + end.to change(User, :count).by(0) + end + + it 'renders a response with 422 status (i.e. to display the "new" template)' do + post settings_users_url, params: { user: invalid_attributes } + + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe 'PATCH /update' do + let(:user) { create(:user) } + + before { sign_in admin } + + context 'with valid parameters' do + let(:new_attributes) { { email: FFaker::Internet.email, password: '4815162342' } } + + it 'updates the requested user' do + patch settings_user_url(user), params: { user: new_attributes } + + user.reload + expect(user.email).to eq(new_attributes[:email]) + expect(user.valid_password?(new_attributes[:password])).to be_truthy + end end end end end end + + context 'when Dawarich is not in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + sign_in admin + end + + describe 'GET /index' do + it 'redirects to root page' do + get settings_users_url + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + describe 'POST /create' do + it 'redirects to root page' do + post settings_users_url, params: { user: valid_attributes } + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + + describe 'PATCH /update' do + let(:user) { create(:user) } + + it 'redirects to root page' do + patch settings_user_url(user), params: { user: valid_attributes } + + expect(response).to redirect_to(root_url) + expect(flash[:notice]).to eq('You are not authorized to perform this action.') + end + end + end end From 7dfec304e432584942e15f70de3d210a8925965a Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 11:50:56 +0100 Subject: [PATCH 05/11] Restrict access to Sidekiq in non self-hosted mode --- config/routes.rb | 2 +- spec/requests/sidekiq_spec.rb | 76 +++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index ac6c27ae..8cabfc85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,7 +6,7 @@ Rails.application.routes.draw do mount ActionCable.server => '/cable' mount Rswag::Api::Engine => '/api-docs' mount Rswag::Ui::Engine => '/api-docs' - authenticate :user, ->(u) { u.admin? } do + authenticate :user, ->(u) { u.admin? && DawarichSettings.self_hosted? } do mount Sidekiq::Web => '/sidekiq' end diff --git a/spec/requests/sidekiq_spec.rb b/spec/requests/sidekiq_spec.rb index a8440e1a..b1dbca16 100644 --- a/spec/requests/sidekiq_spec.rb +++ b/spec/requests/sidekiq_spec.rb @@ -3,39 +3,71 @@ require 'rails_helper' RSpec.describe '/sidekiq', type: :request do - context 'when user is not authenticated' do - it 'redirects to sign in page' do - get sidekiq_url + context 'when Dawarich is in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) + end - expect(response).to redirect_to('/users/sign_in') + context 'when user is not authenticated' do + it 'redirects to sign in page' do + get sidekiq_url + + expect(response).to redirect_to('/users/sign_in') + end + end + + context 'when user is authenticated' do + context 'when user is not admin' do + before { sign_in create(:user) } + + it 'redirects to root page' do + get sidekiq_url + + expect(response).to redirect_to(root_url) + end + + it 'shows flash message' do + get sidekiq_url + + expect(flash[:error]).to eq('You are not authorized to perform this action.') + end + end + + context 'when user is admin' do + before { sign_in create(:user, :admin) } + + it 'renders a successful response' do + get sidekiq_url + + expect(response).to be_successful + end + end end end - context 'when user is authenticated' do - context 'when user is not admin' do - before { sign_in create(:user) } + context 'when Dawarich is not in self-hosted mode' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + Rails.application.reload_routes! + end + + context 'when user is not authenticated' do + it 'redirects to sign in page' do + get sidekiq_url + + expect(response).to redirect_to('/users/sign_in') + end + end + + context 'when user is authenticated' do + before { sign_in create(:user, :admin) } it 'redirects to root page' do get sidekiq_url expect(response).to redirect_to(root_url) - end - - it 'shows flash message' do - get sidekiq_url - expect(flash[:error]).to eq('You are not authorized to perform this action.') end end - - context 'when user is admin' do - before { sign_in create(:user, :admin) } - - it 'renders a successful response' do - get sidekiq_url - - expect(response).to be_successful - end - end end end From aaa3c7716277aa68aee67402df2f33e5773ee887 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 16:48:03 +0100 Subject: [PATCH 06/11] Inherit all stimulus controllers from base_controller --- CHANGELOG.md | 6 +++++ app/controllers/application_controller.rb | 8 ++++++- app/javascript/controllers/base_controller.js | 23 +++++++++++++++++++ .../checkbox_select_all_controller.js | 4 ++-- .../controllers/datetime_controller.js | 4 ++-- .../controllers/imports_controller.js | 4 ++-- .../controllers/map_preview_controller.js | 4 ++-- app/javascript/controllers/maps_controller.js | 6 +++-- .../controllers/notifications_controller.js | 5 ++-- .../controllers/removals_controller.js | 4 ++-- .../controllers/trip_map_controller.js | 4 ++-- .../controllers/trips_controller.js | 4 ++-- .../controllers/visit_modal_map_controller.js | 10 ++++---- .../visit_modal_places_controller.js | 9 +++++--- .../controllers/visit_name_controller.js | 4 ++-- app/views/layouts/application.html.erb | 2 +- 16 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 app/javascript/controllers/base_controller.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b90f69b..398c57b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed a bug where background jobs to import Immich and Photoprism geolocation data data could not be created by non-admin users. +### Changed + +- Restrict access to Sidekiq in non self-hosted mode. +- Restrict access to background jobs in non self-hosted mode. +- Restrict access to users management in non self-hosted mode. + # 0.24.1 - 2025-02-13 ## Custom map tiles diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f1a5e617..7b7c27d0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,7 +3,7 @@ class ApplicationController < ActionController::Base include Pundit::Authorization - before_action :unread_notifications + before_action :unread_notifications, :set_self_hosted_status protected @@ -24,4 +24,10 @@ class ApplicationController < ActionController::Base redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other end + + private + + def set_self_hosted_status + @self_hosted = DawarichSettings.self_hosted? + end end diff --git a/app/javascript/controllers/base_controller.js b/app/javascript/controllers/base_controller.js new file mode 100644 index 00000000..ab6f12f7 --- /dev/null +++ b/app/javascript/controllers/base_controller.js @@ -0,0 +1,23 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + selfHosted: Boolean + } + + // Every controller that extends BaseController and uses initialize() + // should call super.initialize() + // Example: + // export default class extends BaseController { + // initialize() { + // super.initialize() + // } + // } + initialize() { + // Get the self-hosted value from the HTML root element + if (!this.hasSelfHostedValue) { + const selfHosted = document.documentElement.dataset.selfHosted === 'true' + this.selfHostedValue = selfHosted + } + } +} diff --git a/app/javascript/controllers/checkbox_select_all_controller.js b/app/javascript/controllers/checkbox_select_all_controller.js index 5e77773f..1b542f84 100644 --- a/app/javascript/controllers/checkbox_select_all_controller.js +++ b/app/javascript/controllers/checkbox_select_all_controller.js @@ -1,7 +1,7 @@ -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" // Connects to data-controller="checkbox-select-all" -export default class extends Controller { +export default class extends BaseController { static targets = ["parent", "child"] connect() { diff --git a/app/javascript/controllers/datetime_controller.js b/app/javascript/controllers/datetime_controller.js index b56f07e3..b03df4ca 100644 --- a/app/javascript/controllers/datetime_controller.js +++ b/app/javascript/controllers/datetime_controller.js @@ -2,9 +2,9 @@ // - trips/new // - trips/edit -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" -export default class extends Controller { +export default class extends BaseController { static targets = ["startedAt", "endedAt", "apiKey"] static values = { tripsId: String } diff --git a/app/javascript/controllers/imports_controller.js b/app/javascript/controllers/imports_controller.js index fd00d5c9..d39455a0 100644 --- a/app/javascript/controllers/imports_controller.js +++ b/app/javascript/controllers/imports_controller.js @@ -1,7 +1,7 @@ -import { Controller } from "@hotwired/stimulus"; +import BaseController from "./base_controller"; import consumer from "../channels/consumer"; -export default class extends Controller { +export default class extends BaseController { static targets = ["index"]; connect() { diff --git a/app/javascript/controllers/map_preview_controller.js b/app/javascript/controllers/map_preview_controller.js index 3b610a33..e55f2b83 100644 --- a/app/javascript/controllers/map_preview_controller.js +++ b/app/javascript/controllers/map_preview_controller.js @@ -1,8 +1,8 @@ -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" import L from "leaflet" import { showFlashMessage } from "../maps/helpers" -export default class extends Controller { +export default class extends BaseController { static targets = ["urlInput", "mapContainer", "saveButton"] DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index d2f59dbb..9cf82ee4 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -13,7 +13,7 @@ import { import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas"; -import { showFlashMessage, fetchAndDisplayPhotos, debounce } from "../maps/helpers"; +import { showFlashMessage, fetchAndDisplayPhotos } from "../maps/helpers"; import { osmMapLayer, @@ -31,8 +31,9 @@ import { countryCodesMap } from "../maps/country_codes"; import "leaflet-draw"; import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war"; import { TileMonitor } from "../maps/tile_monitor"; +import BaseController from "./base_controller"; -export default class extends Controller { +export default class extends BaseController { static targets = ["container"]; settingsButtonAdded = false; @@ -41,6 +42,7 @@ export default class extends Controller { trackedMonthsCache = null; connect() { + super.connect(); console.log("Map controller connected"); this.apiKey = this.element.dataset.api_key; diff --git a/app/javascript/controllers/notifications_controller.js b/app/javascript/controllers/notifications_controller.js index 6ba44514..c40a4db5 100644 --- a/app/javascript/controllers/notifications_controller.js +++ b/app/javascript/controllers/notifications_controller.js @@ -1,11 +1,12 @@ -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" import consumer from "../channels/consumer" -export default class extends Controller { +export default class extends BaseController { static targets = ["badge", "list"] static values = { userId: Number } initialize() { + super.initialize() this.subscription = null } diff --git a/app/javascript/controllers/removals_controller.js b/app/javascript/controllers/removals_controller.js index cf487d07..c5f30b32 100644 --- a/app/javascript/controllers/removals_controller.js +++ b/app/javascript/controllers/removals_controller.js @@ -1,6 +1,6 @@ -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" -export default class extends Controller { +export default class extends BaseController { static values = { timeout: Number } diff --git a/app/javascript/controllers/trip_map_controller.js b/app/javascript/controllers/trip_map_controller.js index 1bbdc207..01b4a9e5 100644 --- a/app/javascript/controllers/trip_map_controller.js +++ b/app/javascript/controllers/trip_map_controller.js @@ -1,10 +1,10 @@ // This controller is being used on: // - trips/index -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" import L from "leaflet" -export default class extends Controller { +export default class extends BaseController { static values = { tripId: Number, path: String, diff --git a/app/javascript/controllers/trips_controller.js b/app/javascript/controllers/trips_controller.js index 974feb30..6dc0c544 100644 --- a/app/javascript/controllers/trips_controller.js +++ b/app/javascript/controllers/trips_controller.js @@ -3,7 +3,7 @@ // - trips/edit // - trips/new -import { Controller } from "@hotwired/stimulus" +import BaseController from "./base_controller" import L from "leaflet" import { osmMapLayer, @@ -22,7 +22,7 @@ import { showFlashMessage } from '../maps/helpers'; -export default class extends Controller { +export default class extends BaseController { static targets = ["container", "startedAt", "endedAt"] static values = { } diff --git a/app/javascript/controllers/visit_modal_map_controller.js b/app/javascript/controllers/visit_modal_map_controller.js index 5fcb0547..f9b164f6 100644 --- a/app/javascript/controllers/visit_modal_map_controller.js +++ b/app/javascript/controllers/visit_modal_map_controller.js @@ -1,12 +1,12 @@ -import { Controller } from "@hotwired/stimulus" -import L, { latLng } from "leaflet"; -import { osmMapLayer } from "../maps/layers"; +import BaseController from "./base_controller" +import L from "leaflet" +import { osmMapLayer } from "../maps/layers" // This controller is used to display a map of all coordinates for a visit // on the "Map" modal of a visit on the Visits page -export default class extends Controller { - static targets = ["container"]; +export default class extends BaseController { + static targets = ["container"] connect() { this.coordinates = JSON.parse(this.element.dataset.coordinates); diff --git a/app/javascript/controllers/visit_modal_places_controller.js b/app/javascript/controllers/visit_modal_places_controller.js index ad6259f2..b697622e 100644 --- a/app/javascript/controllers/visit_modal_places_controller.js +++ b/app/javascript/controllers/visit_modal_places_controller.js @@ -1,10 +1,13 @@ -import { Controller } from "@hotwired/stimulus"; +import BaseController from "./base_controller" -export default class extends Controller { +export default class extends BaseController { + static targets = ["name", "input"] connect() { - this.visitId = this.element.dataset.id; this.apiKey = this.element.dataset.api_key; + this.visitId = this.element.dataset.id; + + this.element.addEventListener("visit-name:updated", this.updateAll.bind(this)); } // Action to handle selection change diff --git a/app/javascript/controllers/visit_name_controller.js b/app/javascript/controllers/visit_name_controller.js index 70af33b2..24b33273 100644 --- a/app/javascript/controllers/visit_name_controller.js +++ b/app/javascript/controllers/visit_name_controller.js @@ -1,7 +1,7 @@ -import { Controller } from "@hotwired/stimulus"; +import BaseController from "./base_controller" // This controller is used to handle the updating of visit names on the Visits page -export default class extends Controller { +export default class extends BaseController { static targets = ["name", "input"]; connect() { diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 4063fad1..f41baeda 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,5 @@ - + <%= full_title(yield(:title)) %> From 38573e703ed681e38c6bfe76dc6e6e3d97795a64 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 17:42:10 +0100 Subject: [PATCH 07/11] Move base maps to a separate file --- app/javascript/controllers/base_controller.js | 2 + app/javascript/controllers/maps_controller.js | 21 +- app/javascript/maps/layers.js | 195 ++++-------------- app/javascript/maps/maps_config.js | 44 ++++ config/initializers/01_constants.rb | 2 + 5 files changed, 93 insertions(+), 171 deletions(-) create mode 100644 app/javascript/maps/maps_config.js diff --git a/app/javascript/controllers/base_controller.js b/app/javascript/controllers/base_controller.js index ab6f12f7..f9cd6faa 100644 --- a/app/javascript/controllers/base_controller.js +++ b/app/javascript/controllers/base_controller.js @@ -19,5 +19,7 @@ export default class extends Controller { const selfHosted = document.documentElement.dataset.selfHosted === 'true' this.selfHostedValue = selfHosted } + + console.log(`Self-hosted mode in base controller: ${this.selfHostedValue}`) } } diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 9cf82ee4..1ebacde0 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -32,6 +32,7 @@ import "leaflet-draw"; import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war"; import { TileMonitor } from "../maps/tile_monitor"; import BaseController from "./base_controller"; +import { createMapLayer, createAllMapLayers } from "../maps/layers"; export default class extends BaseController { static targets = ["container"]; @@ -404,17 +405,15 @@ export default class extends BaseController { baseMaps() { let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap"; - let maps = { - OpenStreetMap: osmMapLayer(this.map, selectedLayerName), - "OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName), - OPNV: OPNVMapLayer(this.map, selectedLayerName), - openTopo: openTopoMapLayer(this.map, selectedLayerName), - cyclOsm: cyclOsmMapLayer(this.map, selectedLayerName), - esriWorldStreet: esriWorldStreetMapLayer(this.map, selectedLayerName), - esriWorldTopo: esriWorldTopoMapLayer(this.map, selectedLayerName), - esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName), - esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName) - }; + let maps; // Declare the variable first + + if (this.selfHostedValue) { + maps = createAllMapLayers(this.map, selectedLayerName); + } else { + maps = { + OpenStreetMap: createMapLayer(this.map, selectedLayerName, "OpenStreetMap") + }; + } // Add custom map if it exists in settings if (this.userSettings.maps && this.userSettings.maps.url) { diff --git a/app/javascript/maps/layers.js b/app/javascript/maps/layers.js index c32200cc..87501e65 100644 --- a/app/javascript/maps/layers.js +++ b/app/javascript/maps/layers.js @@ -1,5 +1,40 @@ // Yeah I know it should be DRY but this is me doing a KISS at 21:00 on a Sunday night +// Import the maps configuration +import { mapsConfig } from './maps_config'; + +export function createMapLayer(map, selectedLayerName, layerKey) { + const config = mapsConfig[layerKey]; + + if (!config) { + console.warn(`No configuration found for layer: ${layerKey}`); + return null; + } + + let layer = L.tileLayer(config.url, { + maxZoom: config.maxZoom, + attribution: config.attribution, + // Add any other config properties that might be needed + }); + + if (selectedLayerName === layerKey) { + return layer.addTo(map); + } else { + return layer; + } +} + +// Helper function to create all map layers +export function createAllMapLayers(map, selectedLayerName) { + const layers = {}; + + Object.keys(mapsConfig).forEach(layerKey => { + layers[layerKey] = createMapLayer(map, selectedLayerName, layerKey); + }); + + return layers; +} + export function osmMapLayer(map, selectedLayerName) { let layerName = 'OpenStreetMap'; @@ -57,166 +92,6 @@ export function openTopoMapLayer(map, selectedLayerName) { } } -// export function stadiaAlidadeSmoothMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaAlidadeSmooth'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaAlidadeSmoothDarkMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaAlidadeSmoothDark'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaAlidadeSatelliteMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaAlidadeSatellite'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', -// ext: 'jpg' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaOsmBrightMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaOsmBright'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaOutdoorMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaOutdoor'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaStamenTonerMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaStamenToner'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaStamenTonerBackgroundMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaStamenTonerBackground'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_background/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaStamenTonerLiteMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaStamenTonerLite'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 20, -// attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaStamenWatercolorMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaStamenWatercolor'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.{ext}', { -// minZoom: 1, -// maxZoom: 16, -// attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', -// ext: 'jpg' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - -// export function stadiaStamenTerrainMapLayer(map, selectedLayerName) { -// let layerName = 'stadiaStamenTerrain'; -// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}{r}.{ext}', { -// minZoom: 0, -// maxZoom: 18, -// attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', -// ext: 'png' -// }); - -// if (selectedLayerName === layerName) { -// return layer.addTo(map); -// } else { -// return layer; -// } -// } - export function cyclOsmMapLayer(map, selectedLayerName) { let layerName = 'cyclOsm'; let layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { diff --git a/app/javascript/maps/maps_config.js b/app/javascript/maps/maps_config.js new file mode 100644 index 00000000..c0017df6 --- /dev/null +++ b/app/javascript/maps/maps_config.js @@ -0,0 +1,44 @@ +export const mapsConfig = { + "OpenStreetMap": { + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + maxZoom: 19, + attribution: "© OpenStreetMap" + }, + "OpenStreetMap.HOT": { + url: "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", + maxZoom: 19, + attribution: "© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France" + }, + "OPNV": { + url: "https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png", + maxZoom: 18, + attribution: "Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors" + }, + "openTopo": { + url: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", + maxZoom: 17, + attribution: "Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)" + }, + "cyclOsm": { + url: "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png", + maxZoom: 20, + attribution: "CyclOSM | Map data: © OpenStreetMap contributors" + }, + "esriWorldStreet": { + url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}", + attribution: "Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012" + }, + "esriWorldTopo": { + url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", + attribution: "Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community" + }, + "esriWorldImagery": { + url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + }, + "esriWorldGrayCanvas": { + url: "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}", + attribution: "Tiles © Esri — Esri, DeLorme, NAVTEQ", + maxZoom: 16 + } +}; diff --git a/config/initializers/01_constants.rb b/config/initializers/01_constants.rb index 47dbf379..09d57374 100644 --- a/config/initializers/01_constants.rb +++ b/config/initializers/01_constants.rb @@ -21,3 +21,5 @@ NOMINATIM_API_USE_HTTPS = ENV.fetch('NOMINATIM_API_USE_HTTPS', 'true') == 'true' GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil) # /Reverse geocoding settings + +DEFAULT_MAP_TILES_URL = ENV.fetch('DEFAULT_MAP_TILES_URL', nil) From 62f4ec49100e4e5049022530e8223760cc906106 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 15 Feb 2025 17:58:33 +0100 Subject: [PATCH 08/11] Update flash message styles --- app/javascript/controllers/base_controller.js | 2 -- app/javascript/controllers/maps_controller.js | 12 ------- app/javascript/maps/helpers.js | 33 ++++++++++++------- app/views/visits/index.html.erb | 2 +- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/app/javascript/controllers/base_controller.js b/app/javascript/controllers/base_controller.js index f9cd6faa..ab6f12f7 100644 --- a/app/javascript/controllers/base_controller.js +++ b/app/javascript/controllers/base_controller.js @@ -19,7 +19,5 @@ export default class extends Controller { const selfHosted = document.documentElement.dataset.selfHosted === 'true' this.selfHostedValue = selfHosted } - - console.log(`Self-hosted mode in base controller: ${this.selfHostedValue}`) } } diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 1ebacde0..bc3bb93f 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -14,18 +14,6 @@ import { import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas"; import { showFlashMessage, fetchAndDisplayPhotos } from "../maps/helpers"; - -import { - osmMapLayer, - osmHotMapLayer, - OPNVMapLayer, - openTopoMapLayer, - cyclOsmMapLayer, - esriWorldStreetMapLayer, - esriWorldTopoMapLayer, - esriWorldImageryMapLayer, - esriWorldGrayCanvasMapLayer -} from "../maps/layers"; import { countryCodesMap } from "../maps/country_codes"; import "leaflet-draw"; diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js index 7c850f03..59234a98 100644 --- a/app/javascript/maps/helpers.js +++ b/app/javascript/maps/helpers.js @@ -87,10 +87,19 @@ export function haversineDistance(lat1, lon1, lat2, lon2, unit = 'km') { } export function showFlashMessage(type, message) { - // Create the outer flash container div + // Get or create the flash container + let flashContainer = document.getElementById('flash-messages'); + if (!flashContainer) { + flashContainer = document.createElement('div'); + flashContainer.id = 'flash-messages'; + flashContainer.className = 'fixed top-5 right-5 flex flex-col-reverse gap-2 z-40'; + document.body.appendChild(flashContainer); + } + + // Create the flash message div const flashDiv = document.createElement('div'); flashDiv.setAttribute('data-controller', 'removals'); - flashDiv.className = `flex items-center fixed top-5 right-5 ${classesForFlash(type)} py-3 px-5 rounded-lg`; + flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg z-40`; // Create the message div const messageDiv = document.createElement('div'); @@ -101,6 +110,7 @@ export function showFlashMessage(type, message) { const closeButton = document.createElement('button'); closeButton.setAttribute('type', 'button'); closeButton.setAttribute('data-action', 'click->removals#remove'); + closeButton.className = 'ml-auto'; // Ensures button stays on the right // Create the SVG icon for the close button const closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); @@ -116,21 +126,22 @@ export function showFlashMessage(type, message) { closeIconPath.setAttribute('stroke-width', '2'); closeIconPath.setAttribute('d', 'M6 18L18 6M6 6l12 12'); - // Append the path to the SVG + // Append all elements closeIcon.appendChild(closeIconPath); - // Append the SVG to the close button closeButton.appendChild(closeIcon); - - // Append the message and close button to the flash div flashDiv.appendChild(messageDiv); flashDiv.appendChild(closeButton); + flashContainer.appendChild(flashDiv); - // Append the flash message to the body or a specific flash container - document.body.appendChild(flashDiv); - - // Optional: Automatically remove the flash message after 5 seconds + // Automatically remove after 5 seconds setTimeout(() => { - flashDiv.remove(); + if (flashDiv && flashDiv.parentNode) { + flashDiv.remove(); + // Remove container if empty + if (flashContainer && !flashContainer.hasChildNodes()) { + flashContainer.remove(); + } + } }, 5000); } diff --git a/app/views/visits/index.html.erb b/app/views/visits/index.html.erb index 3fd03b64..e27dbac2 100644 --- a/app/views/visits/index.html.erb +++ b/app/views/visits/index.html.erb @@ -27,7 +27,7 @@
-