mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Merge pull request #896 from Freika/feature/user-features-access
Feature/user features access
This commit is contained in:
commit
0d703521ee
27 changed files with 260 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class SettingsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
before_action :authenticate_active_user!, only: %i[update]
|
||||
def index; end
|
||||
|
||||
def update
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}, '')
|
||||
|
|
|
|||
7
db/migrate/20250219195822_add_status_to_users.rb
Normal file
7
db/migrate/20250219195822_add_status_to_users.rb
Normal 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
1
db/schema.rb
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ FactoryBot.define do
|
|||
"user#{n}@example.com"
|
||||
end
|
||||
|
||||
status { :active }
|
||||
|
||||
password { SecureRandom.hex(8) }
|
||||
|
||||
settings do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue