Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user management and role assignment functionality #29

Merged
merged 4 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,7 @@ jobs:
run: |
bundler exec rails db:create
bundler exec rails db:migrate
- name: Enhance prepare commands with tailwindcss:build
run: bundle exec rake spec:prepare
- name: Run tests
run: bundler exec rspec
3 changes: 2 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ Metrics/MethodLength:
Max: 20

Style/Documentation:
Enabled: false
Enabled: false

13 changes: 1 addition & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ RUN apt-get update -qq && apt-get install -y \
build-essential \
curl

# Install rbenv
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv && \
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc && \
echo 'eval "$(rbenv init -)"' >> ~/.bashrc && \
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build && \
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc

# Install the specified Ruby version using rbenv
ENV PATH="/root/.rbenv/bin:/root/.rbenv/shims:$PATH"
RUN rbenv install $RUBY_VERSION && rbenv global $RUBY_VERSION

# Set the working directory
WORKDIR /app

Expand All @@ -35,7 +24,7 @@ COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

# Install Gems dependencies
RUN gem install bundler && bundle install
RUN gem install bundler && bundle install --jobs 4 --retry 3

# Copy the application code
COPY . /app
Expand Down
12 changes: 10 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,19 @@ gem 'email_validator'

# Styling
gem 'bootstrap'
gem 'sassc-rails'
gem 'tailwindcss-rails', '~> 2.7'
gem 'sassc', '~> 2.4'
gem 'sassc-rails', '~> 2.1.2'
gem 'tailwindcss-rails', '~> 3.1'

gem 'dotenv-rails'

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

group :test do
gem 'rails-controller-testing'
end

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem 'debug', platforms: %i[mri windows]
Expand All @@ -87,6 +92,9 @@ group :development do
gem 'rubocop-rspec', require: false
gem 'rubocop-rspec_rails', require: false

# To see in what paths rails are looking for translations
gem 'i18n-debug'

# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"

Expand Down
32 changes: 19 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ GEM
activesupport (>= 6.1)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
i18n-debug (1.2.0)
i18n (< 2)
importmap-rails (2.0.2)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
Expand Down Expand Up @@ -217,6 +219,10 @@ GEM
activesupport (= 7.1.5.1)
bundler (>= 1.15.0)
railties (= 7.1.5.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
Expand Down Expand Up @@ -307,18 +313,15 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.1)
tailwindcss-rails (2.7.7)
railties (>= 7.0.0)
tailwindcss-rails (2.7.7-aarch64-linux)
railties (>= 7.0.0)
tailwindcss-rails (2.7.7-arm-linux)
railties (>= 7.0.0)
tailwindcss-rails (2.7.7-arm64-darwin)
railties (>= 7.0.0)
tailwindcss-rails (2.7.7-x86_64-darwin)
railties (>= 7.0.0)
tailwindcss-rails (2.7.7-x86_64-linux)
tailwindcss-rails (3.1.0)
railties (>= 7.0.0)
tailwindcss-ruby
tailwindcss-ruby (3.4.17)
tailwindcss-ruby (3.4.17-aarch64-linux)
tailwindcss-ruby (3.4.17-arm-linux)
tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux)
thor (1.3.2)
tilt (2.4.0)
timeout (0.4.1)
Expand Down Expand Up @@ -361,12 +364,14 @@ DEPENDENCIES
email_validator
factory_bot_rails
faker
i18n-debug
importmap-rails
jbuilder
pg (~> 1.1)
puma (>= 5.0)
pundit
rails (~> 7.1.3, >= 7.1.3.4)
rails-controller-testing
rails-html-sanitizer (>= 1.6.1)
rolify
rspec-rails
Expand All @@ -376,10 +381,11 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
rubocop-rspec_rails
sassc-rails
sassc (~> 2.4)
sassc-rails (~> 2.1.2)
sprockets-rails
stimulus-rails
tailwindcss-rails (~> 2.7)
tailwindcss-rails (~> 3.1)
turbo-rails
tzinfo-data
web-console
Expand Down
10 changes: 0 additions & 10 deletions app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

/*

@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-200;
}
}

*/
90 changes: 90 additions & 0 deletions app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

module Admin
class UsersController < ApplicationController
before_action :set_user, only: %i[show edit update destroy update edit_roles update_roles]
before_action :set_roles_list, only: %i[edit_roles update_roles]
before_action :set_user_params_roles, only: %i[update_roles]

# GET /admin/users or /admin/users.json
def index
@users = policy_scope(User.all)
end

# GET /admin/users/1 or /admin/users/1.json
def show
authorize @user
end

# GET /admin/users/1/edit
def edit
authorize @user
end

# PATCH/PUT /admin/users/1 or /admin/users/1.json
def update
authorize @user

respond_to do |format|
if @user.update(user_params)
format.html { redirect_to admin_user_url(@user), notice: t('.success') }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end

# DELETE /admin/users/1 or /admin/users/1.json
def destroy
authorize @user
@user.destroy!

respond_to do |format|
format.html { redirect_to admin_users_path, status: :see_other, notice: t('.success') }
format.json { head :no_content }
end
end

# GET /admin/users/1/edit_roles or /admin/users/1/edit_roles.json
def edit_roles
authorize @user
end

# POST /admin/users/1/update_roles or /admin/users/1/update_roles.json
def update_roles
authorize @user

respond_to do |format|
if @user.update_roles?(@user_params_roles)
format.html { redirect_to admin_user_url(@user), notice: t('.success') }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit_roles, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end

private

# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end

def set_roles_list
@roles = Role.distinct.pluck(:name).map { |role| [role.capitalize, role] }
end

def set_user_params_roles
@user_params_roles = user_params[:roles].compact_blank
end

# Only allow a list of trusted parameters through.
def user_params
params.require(:user).permit(:first_name, :last_name, :email, roles: [])
end
end
end
11 changes: 11 additions & 0 deletions app/helpers/users_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module UsersHelper
def get_user_roles_list_as_string(user)
user.user_roles_names.map { |name| User::Roles::ROLES_MAP[name.to_sym] }.join(', ')
end

def get_user_role_ids_list(user)
user.user_roles_names
end
end
79 changes: 72 additions & 7 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class User < ApplicationRecord
include ActionView::Helpers::TranslationHelper

rolify
resourcify
after_commit :assign_default_role, on: :create
Expand All @@ -15,40 +17,96 @@ class User < ApplicationRecord
has_many :measurements, dependent: :destroy
has_and_belongs_to_many :roles, join_table: :users_roles # rubocop:disable Rails/HasAndBelongsToMany

accepts_nested_attributes_for :roles

validates :email, presence: true, uniqueness: true, email: true

def full_name
"#{first_name} #{last_name}"
end

def user_roles_names
roles.pluck(:name)
end

def full_access_roles_can?
has_any_role?(*User::Roles::FULL_ACCESS_ROLES)
has_any_role?(*Roles::FULL_ACCESS_ROLES)
end

def all_roles_can?
has_any_role?(*User::Roles::ROLES)
has_any_role?(*Roles::ROLES)
end

def admin?
has_role?(User::Roles::ADMIN)
has_role?(Roles::ADMIN)
end

def doctor?
has_role?(User::Roles::DOCTOR)
has_role?(Roles::DOCTOR)
end

def health_coach?
has_role?(User::Roles::HEALTH_COACH)
has_role?(Roles::HEALTH_COACH)
end

def user?
has_role?(User::Roles::USER)
has_role?(Roles::USER)
end

def update_roles?(new_roles)
User.transaction do
synchronize_roles(new_roles)

raise ActiveRecord::Rollback unless errors.empty?
rescue StandardError => e
handle_unexpected_transaction_error(e)

raise ActiveRecord::Rollback
end
are_errors_empty = errors.empty?

unless are_errors_empty
Rails.logger.error("Role update failed for Account ID #{id}: #{errors.full_messages.join(', ')}")
end

are_errors_empty
end

private

def assign_default_role
add_role(User::Roles::USER) if roles.blank?
add_role(Roles::USER) if roles.blank?
end

def synchronize_roles(new_roles)
return unless validate_roles(new_roles)

current_roles = roles.pluck(:name)

roles_to_remove = current_roles - new_roles
roles_to_remove.each do |role|
remove_role(role) || errors.add(:base, t('errors.messages.remove_role_failure', role: 'role'))
end

roles_to_add = new_roles - current_roles
roles_to_add.each do |role|
add_role(role) || errors.add(:base, t('errors.messages.add_role_failure', role: 'role'))
end
end

def validate_roles(new_roles)
valid_roles = Role.pluck(:name)
invalid_roles = new_roles - valid_roles
unless invalid_roles.empty?
errors.add(:base, t('errors.messages.invalid_roles_detected', invalid_roles: invalid_roles.join(', ')))
end

invalid_roles.empty?
end

def handle_unexpected_transaction_error(error)
errors.add(:base, t('errors.messages.unexpected_error'))
Rails.logger.error("Unexpected error while synchronizing roles: #{error.message}")
end

class Roles < User
Expand All @@ -59,5 +117,12 @@ class Roles < User

ROLES = [ADMIN, DOCTOR, HEALTH_COACH, USER].freeze
FULL_ACCESS_ROLES = [ADMIN, DOCTOR, HEALTH_COACH].freeze

ROLES_MAP = {
ADMIN => 'Admin',
DOCTOR => 'Doctor',
HEALTH_COACH => 'Health coach',
USER => 'User'
}.freeze
end
end
Loading
Loading