Merge pull request #896 from Freika/feature/user-features-access

Feature/user features access
This commit is contained in:
Evgenii Burmakin 2025-02-25 00:21:35 +01:00 committed by GitHub
commit 0d703521ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 260 additions and 3 deletions

View file

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# 0.24.2 - 2025-02-24
## Added
- Status field to the User model. Inactive users are now being restricted from accessing some of the functionality, which is mostly about writing data to the database. Reading is remaining unrestricted.
## Fixed
- Fixed a bug where non-admin users could not import Immich and Photoprism geolocation data.

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::Overland::BatchesController < ApiController
before_action :authenticate_active_api_user!, only: %i[create]
def create
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::Owntracks::PointsController < ApiController
before_action :authenticate_active_api_user!, only: %i[create]
def create
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::PointsController < ApiController
before_action :authenticate_active_api_user!, only: %i[create update destroy]
def index
start_at = params[:start_at]&.to_datetime&.to_i
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::SettingsController < ApiController
before_action :authenticate_active_api_user!, only: %i[update]
def index
render json: {
settings: current_api_user.settings,

View file

@ -12,6 +12,12 @@ class ApiController < ApplicationController
true
end
def authenticate_active_api_user!
render json: { error: 'User is not active' }, status: :unauthorized unless current_api_user&.active?
true
end
def current_api_user
@current_api_user ||= User.find_by(api_key:)
end

View file

@ -25,6 +25,12 @@ class ApplicationController < ActionController::Base
redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other
end
def authenticate_active_user!
return if current_user&.active?
redirect_to root_path, notice: 'Your account is not active.', status: :see_other
end
private
def set_self_hosted_status

View file

@ -2,6 +2,7 @@
class ImportsController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_active_user!, only: %i[new create]
before_action :set_import, only: %i[show destroy]
def index

View file

@ -2,7 +2,7 @@
class SettingsController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_active_user!, only: %i[update]
def index; end
def update

View file

@ -2,6 +2,7 @@
class StatsController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_active_user!, only: %i[update update_all]
def index
@stats = current_user.stats.group_by(&:year).sort.reverse

View file

@ -2,6 +2,7 @@
class TripsController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_active_user!, only: %i[new create]
before_action :set_trip, only: %i[show edit update destroy]
before_action :set_coordinates, only: %i[show edit]

View file

@ -8,6 +8,7 @@ class VisitSuggestingJob < ApplicationJob
users = user_ids.any? ? User.where(id: user_ids) : User.all
users.find_each do |user|
next unless user.active?
next if user.tracked_points.empty?
Visits::Suggest.new(user, start_at:, end_at:).call

View file

@ -16,6 +16,7 @@ class User < ApplicationRecord
has_many :trips, dependent: :destroy
after_create :create_api_key
after_commit :activate, on: :create, if: -> { DawarichSettings.self_hosted? }
before_save :sanitize_input
validates :email, presence: true
@ -24,6 +25,8 @@ class User < ApplicationRecord
attribute :admin, :boolean, default: false
enum :status, { inactive: 0, active: 1 }
def safe_settings
Users::SafeSettings.new(settings)
end
@ -104,6 +107,10 @@ class User < ApplicationRecord
save
end
def activate
update(status: :active)
end
def sanitize_input
settings['immich_url']&.gsub!(%r{/+\z}, '')
settings['photoprism_url']&.gsub!(%r{/+\z}, '')

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddStatusToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :status, :integer, default: 0
end
end

1
db/schema.rb generated
View file

@ -224,6 +224,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_21_194509) do
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "status", default: 0
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

View file

@ -6,6 +6,8 @@ FactoryBot.define do
"user#{n}@example.com"
end
status { :active }
password { SecureRandom.hex(8) }
settings do

View file

@ -30,5 +30,17 @@ RSpec.describe VisitSuggestingJob, type: :job do
expect(Visits::Suggest).to have_received(:new)
end
end
context 'when user is inactive' do
before do
users.first.update(status: :inactive)
end
it 'does not suggest visits' do
subject
expect(Visits::Suggest).not_to have_received(:new)
end
end
end
end

View file

@ -16,6 +16,10 @@ RSpec.describe User, type: :model do
it { is_expected.to have_many(:trips).dependent(:destroy) }
end
describe 'enums' do
it { is_expected.to define_enum_for(:status).with_values(inactive: 0, active: 1) }
end
describe 'callbacks' do
describe '#create_api_key' do
let(:user) { create(:user) }
@ -24,6 +28,33 @@ RSpec.describe User, type: :model do
expect(user.api_key).to be_present
end
end
describe '#activate' do
context 'when self-hosted' do
let!(:user) { create(:user, status: :inactive) }
before do
allow(DawarichSettings).to receive(:self_hosted?).and_return(true)
end
it 'activates user after creation' do
expect(user.active?).to be_truthy
end
end
context 'when not self-hosted' do
let!(:user) { create(:user, status: :inactive) }
before do
stub_const('SELF_HOSTED', false)
allow(DawarichSettings).to receive(:self_hosted?).and_return(false)
end
xit 'does not activate user' do
expect(user.active?).to be_falsey
end
end
end
end
describe 'methods' do

View file

@ -31,6 +31,18 @@ RSpec.describe 'Api::V1::Overland::Batches', type: :request do
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
end.to have_enqueued_job(Overland::BatchCreatingJob)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns http unauthorized' do
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end

View file

@ -31,6 +31,18 @@ RSpec.describe 'Api::V1::Owntracks::Points', type: :request do
end.to have_enqueued_job(Owntracks::PointCreatingJob)
end
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns http unauthorized' do
post api_v1_owntracks_points_path(api_key: user.api_key), params: params
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end

View file

@ -119,4 +119,66 @@ RSpec.describe 'Api::V1::Points', type: :request do
end
end
end
describe 'POST /create' do
it 'returns a successful response' do
post "/api/v1/points?api_key=#{user.api_key}", params: { point: { latitude: 1.0, longitude: 1.0 } }
expect(response).to have_http_status(:success)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns an unauthorized response' do
post "/api/v1/points?api_key=#{user.api_key}", params: { point: { latitude: 1.0, longitude: 1.0 } }
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT /update' do
it 'returns a successful response' do
put "/api/v1/points/#{points.first.id}?api_key=#{user.api_key}",
params: { point: { latitude: 1.0, longitude: 1.1 } }
expect(response).to have_http_status(:success)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns an unauthorized response' do
put "/api/v1/points/#{points.first.id}?api_key=#{user.api_key}",
params: { point: { latitude: 1.0, longitude: 1.1 } }
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /destroy' do
it 'returns a successful response' do
delete "/api/v1/points/#{points.first.id}?api_key=#{user.api_key}"
expect(response).to have_http_status(:success)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns an unauthorized response' do
delete "/api/v1/points/#{points.first.id}?api_key=#{user.api_key}"
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View file

@ -25,6 +25,18 @@ RSpec.describe 'Api::V1::Settings', type: :request do
expect(response.parsed_body['settings']['route_opacity'].to_f).to eq(0.3)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns http unauthorized' do
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 0.3 } }
expect(response).to have_http_status(:unauthorized)
end
end
end
context 'with invalid request' do

View file

@ -82,6 +82,19 @@ RSpec.describe 'Settings', type: :request do
expect(user.reload.settings).to eq(params[:settings])
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'redirects to the root path' do
patch '/settings', params: params
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('Your account is not active.')
end
end
end
describe 'GET /settings/users' do

View file

@ -69,6 +69,19 @@ RSpec.describe '/stats', type: :request do
end
end
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns an unauthorized response' do
put update_year_month_stats_url(year: '2024', month: '1')
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('Your account is not active.')
end
end
end
describe 'PUT /update_all' do
@ -83,6 +96,19 @@ RSpec.describe '/stats', type: :request do
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 2)
expect(Stats::CalculatingJob).to_not have_been_enqueued.with(user.id, 2024, 3)
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'returns an unauthorized response' do
put update_all_stats_url
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('Your account is not active.')
end
end
end
end
end

View file

@ -53,6 +53,19 @@ RSpec.describe '/trips', type: :request do
expect(response).to be_successful
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'redirects to the root path' do
get new_trip_url
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('Your account is not active.')
end
end
end
describe 'GET /edit' do
@ -77,6 +90,19 @@ RSpec.describe '/trips', type: :request do
post trips_url, params: { trip: valid_attributes }
expect(response).to redirect_to(trip_url(Trip.last))
end
context 'when user is inactive' do
before do
user.update(status: :inactive)
end
it 'redirects to the root path' do
post trips_url, params: { trip: valid_attributes }
expect(response).to redirect_to(root_path)
expect(flash[:notice]).to eq('Your account is not active.')
end
end
end
context 'with invalid parameters' do

View file

@ -78,6 +78,9 @@ describe 'Points API', type: :request do
coordinates: [-122.40530871, 37.74430413]
},
properties: {
battery_state: 'full',
battery_level: 0.7,
wifi: 'dawarich_home',
timestamp: '2025-01-17T21:03:01Z',
horizontal_accuracy: 5,
vertical_accuracy: -1,
@ -92,7 +95,7 @@ describe 'Points API', type: :request do
}
]
}
tags 'Batches'
tags 'Points'
consumes 'application/json'
parameter name: :locations, in: :body, schema: {
type: :object,

View file

@ -828,7 +828,7 @@ paths:
post:
summary: Creates a batch of points
tags:
- Batches
- Points
parameters:
- name: api_key
in: query
@ -918,6 +918,9 @@ paths:
- -122.40530871
- 37.74430413
properties:
battery_state: full
battery_level: 0.7
wifi: dawarich_home
timestamp: '2025-01-17T21:03:01Z'
horizontal_accuracy: 5
vertical_accuracy: -1