Merge pull request #130 from Freika/admin_flag

Admin flag
This commit is contained in:
Evgenii Burmakin 2024-07-19 21:16:52 +02:00 committed by GitHub
commit db5f6ee9e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 202 additions and 72 deletions

View file

@ -1 +1 @@
0.9.2 0.9.3

View file

@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.9.3] — 2024-07-19
### Added
- Admin flag to the database. Now not only the first user in the system can create new users, but also users with the admin flag set to true. This will make easier introduction of more admin functions in the future.
### Fixed
- Route hover distance is now being rendered in kilometers, not in meters, if route distance is more than 1 km.
---
## [0.9.2] — 2024-07-19 ## [0.9.2] — 2024-07-19
### Fixed ### Fixed

File diff suppressed because one or more lines are too long

View file

@ -13,10 +13,10 @@ class ApplicationController < ActionController::Base
@unread_notifications ||= Notification.where(user: current_user).unread @unread_notifications ||= Notification.where(user: current_user).unread
end end
def authenticate_first_user! def authenticate_admin!
return if current_user == User.first return if current_user.admin?
redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :unauthorized redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other
end end
def authenticate_api_key def authenticate_api_key

View file

@ -2,7 +2,7 @@
class Settings::BackgroundJobsController < ApplicationController class Settings::BackgroundJobsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :authenticate_first_user! before_action :authenticate_admin!
def index def index
@queues = Sidekiq::Queue.all @queues = Sidekiq::Queue.all

View file

@ -2,7 +2,7 @@
class Settings::UsersController < ApplicationController class Settings::UsersController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :authenticate_first_user! before_action :authenticate_admin!
def create def create
@user = User.new( @user = User.new(
@ -12,7 +12,8 @@ class Settings::UsersController < ApplicationController
) )
if @user.save if @user.save
redirect_to settings_url, notice: "User was successfully created, email is #{@user.email}, password is \"password\"." redirect_to settings_url,
notice: "User was successfully created, email is #{@user.email}, password is \"password\"."
else else
redirect_to settings_url, notice: 'User could not be created.', status: :unprocessable_entity redirect_to settings_url, notice: 'User could not be created.', status: :unprocessable_entity
end end

View file

@ -244,7 +244,7 @@ export default class extends Controller {
<b>Start:</b> ${firstTimestamp}<br> <b>Start:</b> ${firstTimestamp}<br>
<b>End:</b> ${lastTimestamp}<br> <b>End:</b> ${lastTimestamp}<br>
<b>Duration:</b> ${timeOnRoute}<br> <b>Duration:</b> ${timeOnRoute}<br>
<b>Distance:</b> ${Math.round(distance)}m<br> <b>Distance:</b> ${this.formatDistance(distance)}<br>
`; `;
if (isDebugMode) { if (isDebugMode) {
@ -335,4 +335,12 @@ export default class extends Controller {
}) })
).addTo(map); ).addTo(map);
} }
formatDistance(distance) {
if (distance >= 1000) {
return (distance / 1000).toFixed(2) + ' km';
} else {
return distance.toFixed(0) + ' meters';
}
}
} }

View file

@ -85,6 +85,9 @@
<details> <details>
<summary> <summary>
<%= "#{current_user.email}" %> <%= "#{current_user.email}" %>
<% if current_user.admin? %>
<span class='tooltip tooltip-bottom' data-tip="You're an admin, Harry!">⭐️</span>
<% end %>
</summary> </summary>
<ul class="p-2 bg-base-100 rounded-t-none z-10"> <ul class="p-2 bg-base-100 rounded-t-none z-10">
<li><%= link_to 'Account', edit_user_registration_path %></li> <li><%= link_to 'Account', edit_user_registration_path %></li>

View file

@ -5,7 +5,15 @@ require 'sidekiq/web'
Rails.application.routes.draw do Rails.application.routes.draw do
mount Rswag::Api::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs'
mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Ui::Engine => '/api-docs'
mount Sidekiq::Web => '/sidekiq' authenticate :user, ->(u) { u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
# We want to return a nice error message if the user is not authorized to access Sidekiq
match '/sidekiq' => redirect { |_, request|
request.flash[:error] = 'You are not authorized to perform this action.'
'/'
}, via: :get
resources :settings, only: :index resources :settings, only: :index
namespace :settings do namespace :settings do

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class MakeFirstUserAdmin < ActiveRecord::Migration[7.1]
def up
user = User.first
user.update!(admin: true)
end
def down
user = User.first
user.update!(admin: false)
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAdminToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :admin, :boolean, default: false
end
end

3
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_07_12_141303) do ActiveRecord::Schema[7.1].define(version: 2024_07_13_103051) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -150,6 +150,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_12_141303) do
t.string "api_key", default: "", null: false t.string "api_key", default: "", null: false
t.string "theme", default: "dark", null: false t.string "theme", default: "dark", null: false
t.jsonb "settings", default: {"fog_of_war_meters"=>"200", "meters_between_routes"=>"1000", "minutes_between_routes"=>"60"} t.jsonb "settings", default: {"fog_of_war_meters"=>"200", "meters_between_routes"=>"1000", "minutes_between_routes"=>"60"}
t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :user do factory :user do
sequence :email do |n| sequence :email do |n|
@ -5,5 +7,9 @@ FactoryBot.define do
end end
password { SecureRandom.hex(8) } password { SecureRandom.hex(8) }
trait :admin do
admin { true }
end
end end
end end

View file

@ -91,11 +91,10 @@ RSpec.describe User, type: :model do
describe '#total_reverse_geocoded' do describe '#total_reverse_geocoded' do
subject { user.total_reverse_geocoded } subject { user.total_reverse_geocoded }
let(:import) { create(:import, user:) }
let!(:reverse_geocoded_point) do let!(:reverse_geocoded_point) do
create(:point, country: 'Country', city: 'City', geodata: { some: 'data' }, import:) create(:point, country: 'Country', city: 'City', geodata: { some: 'data' }, user:)
end end
let!(:not_reverse_geocoded_point) { create(:point, country: 'Country', city: 'City', import:) } let!(:not_reverse_geocoded_point) { create(:point, country: 'Country', city: 'City', user:) }
it 'returns number of reverse geocoded points' do it 'returns number of reverse geocoded points' do
expect(subject).to eq(1) expect(subject).to eq(1)

View file

@ -17,53 +17,62 @@ RSpec.describe '/settings/background_jobs', type: :request do
end end
context 'when user is authenticated' do context 'when user is authenticated' do
let(:user) { create(:user) } before { sign_in create(:user) }
before do context 'when user is not an admin' do
sign_in user it 'redirects to root page' do
end
describe 'GET /index' do
it 'renders a successful response' do
get settings_background_jobs_url 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 end
end end
describe 'POST /create' do context 'when user is an admin' do
let(:params) { { job_name: 'start_reverse_geocoding' } } before { sign_in create(:user, :admin) }
context 'with valid parameters' do describe 'GET /index' do
it 'enqueues a new job' do it 'renders a successful response' do
expect do get settings_background_jobs_url
post settings_background_jobs_url, params:
end.to have_enqueued_job(EnqueueReverseGeocodingJob) 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(EnqueueReverseGeocodingJob)
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 end
it 'redirects to the created settings_background_job' do it 'redirects to the settings_background_jobs list' do
post(settings_background_jobs_url, params:) delete settings_background_job_url('queue_name')
expect(response).to redirect_to(settings_background_jobs_url) expect(response).to redirect_to(settings_background_jobs_url)
end 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)
expect(queue).to receive(:clear)
delete settings_background_job_url('queue_name')
end
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 end

View file

@ -3,41 +3,61 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe '/settings/users', type: :request do RSpec.describe '/settings/users', type: :request do
before do let(:valid_attributes) { { email: 'user@domain.com' } }
sign_in create(:user)
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(new_user_session_url)
end
end end
describe 'POST /create' do context 'when user is authenticated' do
context 'with valid parameters' do context 'when user is not an admin' do
let(:valid_attributes) { { email: 'user@domain.com' } } before { sign_in create(:user) }
it 'creates a new User' do it 'redirects to root page' do
expect do
post settings_users_url, params: { user: valid_attributes }
end.to change(User, :count).by(1)
end
it 'redirects to the created settings_user' do
post settings_users_url, params: { user: valid_attributes } post settings_users_url, params: { user: valid_attributes }
expect(response).to redirect_to(settings_url) expect(response).to redirect_to(root_url)
expect(flash[:notice]).to eq("User was successfully created, email is #{valid_attributes[:email]}, password is \"password\".")
end end
end end
context 'with invalid parameters' do context 'when user is an admin' do
let(:invalid_attributes) { { email: nil } } before { sign_in create(:user, :admin) }
it 'does not create a new User' do describe 'POST /create' do
expect do context 'with valid parameters' do
post settings_users_url, params: { user: invalid_attributes } it 'creates a new User' do
end.to change(User, :count).by(0) expect do
end post settings_users_url, params: { user: valid_attributes }
end.to change(User, :count).by(1)
end
it 'renders a response with 422 status (i.e. to display the "new" template)' do it 'redirects to the created settings_user' do
post settings_users_url, params: { user: invalid_attributes } post settings_users_url, params: { user: valid_attributes }
expect(response).to have_http_status(:unprocessable_entity) expect(response).to redirect_to(settings_url)
expect(flash[:notice]).to eq("User was successfully created, email is #{valid_attributes[:email]}, password is \"password\".")
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 end
end end
end end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
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
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