diff --git a/Gemfile b/Gemfile index 3e0a2ae..1e0adf9 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,7 @@ group :development, :test do gem 'dotenv', '>= 3.0' gem 'factory_bot_rails' gem 'rspec-rails' + gem 'pry' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 86a6282..eceff8b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,7 @@ GEM xpath (~> 3.2) childprocess (5.1.0) logger (~> 1.5) + coderay (1.1.3) concurrent-ruby (1.3.4) connection_pool (2.4.1) crack (1.0.0) @@ -210,6 +211,7 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + method_source (1.1.0) mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.25.1) @@ -272,6 +274,9 @@ GEM pgcli-rails (0.9.1) railties (>= 4.2.0) prism (1.2.0) + pry (0.15.0) + coderay (~> 1.1) + method_source (~> 1.0) psych (5.2.0) stringio public_suffix (6.0.1) @@ -533,6 +538,7 @@ DEPENDENCIES pagy pg (~> 1.5) pgcli-rails + pry puma (>= 5.0) pundit (~> 2.4) rails (~> 7.2.0) diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index fe04678..6072b52 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -3,5 +3,15 @@ class BaseController < ApplicationController layout 'admin' before_action :authenticate_user! + + private + + def policy_scope(scope, policy_scope_class: nil, replace_parent_scope: false) + super(replace_parent_scope ? scope : [:admin, scope], policy_scope_class: policy_scope_class) + end + + def authorize(record, query = nil, policy_class: nil, replace_parent_scope: false) + super(replace_parent_scope ? record : [:admin, record], query, policy_class: policy_class) + end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index aa4d189..bffb034 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,8 +1,10 @@ module Admin class UsersController < BaseController + before_action :base_scope before_action :set_user, only: %i[show edit update destroy] def index + authorize(User.all) @pagy, @users = pagy(User.order(created_at: :desc)) end @@ -10,6 +12,7 @@ def show; end def new @user = User.new + authorize(@user) end def edit; end @@ -17,8 +20,10 @@ def edit; end def create @user = User.new(user_params) + authorize(@user) + if @user.save - redirect_to admin_users_path(@user), notice: 'User was successfully created.' + redirect_to admin_user_path(@user), notice: 'User was successfully created.' else render :new end @@ -26,7 +31,7 @@ def create def update if @user.update(user_params) - redirect_to admin_users_path(@user), notice: 'User was successfully updated.' + redirect_to admin_user_path(@user), notice: 'User was successfully updated.' else render :edit end @@ -39,13 +44,17 @@ def destroy private + def base_scope + policy_scope(User) + end + def set_user @user = User.find(params[:id]) authorize(@user) end def user_params - params.require(:user).permit(policy(@user).permitted_attributes) + params.require(:user).permit(policy([:admin, @user]).permitted_attributes) end end end diff --git a/app/policies/admin/base_policy.rb b/app/policies/admin/base_policy.rb new file mode 100644 index 0000000..11aee50 --- /dev/null +++ b/app/policies/admin/base_policy.rb @@ -0,0 +1,29 @@ +module Admin + class BasePolicy < ApplicationPolicy + def index? + admin? + end + + def show? + admin? + end + + def create? + admin? + end + + def update? + admin? + end + + def destroy? + admin? + end + + private + + def admin? + @user.admin? + end + end +end diff --git a/app/policies/admin/user_policy.rb b/app/policies/admin/user_policy.rb new file mode 100644 index 0000000..ed55d9c --- /dev/null +++ b/app/policies/admin/user_policy.rb @@ -0,0 +1,40 @@ +module Admin + class UserPolicy < BasePolicy + def permitted_attributes + [ + :email, + :password, + :password_confirmation, + { + role_ids: [] + } + ] + end + + def index? + admin? + end + + def show? + admin? + end + + def create? + admin? + end + + def update? + admin? + end + + def destroy? + admin? + end + + class Scope < Scope + def resolve + scope.order(created_at: :desc) + end + end + end +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index be644fe..4b2f30c 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -43,7 +43,7 @@ def initialize(user, scope) end def resolve - raise NoMethodError, "You must define #resolve in #{self.class}" + @user.nil? ? @scope.none : @scope.all end private diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb deleted file mode 100644 index c94c763..0000000 --- a/app/policies/user_policy.rb +++ /dev/null @@ -1,38 +0,0 @@ -class UserPolicy < ApplicationPolicy - def permitted_attributes - [ - :email, - :password, - :password_confirmation, - { - role_ids: [] - } - ] - end - - def index? - admin? - end - - def show? - admin? - end - - def create? - admin? - end - - def update? - admin? - end - - def destroy? - admin? - end - - class Scope < Scope - def resolve - scope.order(created_at: :desc) - end - end -end diff --git a/app/views/admin/users/_form.html.slim b/app/views/admin/users/_form.html.slim new file mode 100644 index 0000000..aad5fc6 --- /dev/null +++ b/app/views/admin/users/_form.html.slim @@ -0,0 +1,10 @@ += simple_form_for(user, url: admin_user_path(user, format: :html)) do |f| + = f.error_notification + .form-inputs.mt-4 + = f.input :email, + required: true, + label: false, + placeholder: "Email", + input_html: { autocomplete: "email" } + .form-actions + = f.button :submit, "Save", class: "w-full" diff --git a/app/views/admin/users/edit.html.slim b/app/views/admin/users/edit.html.slim new file mode 100644 index 0000000..ab07098 --- /dev/null +++ b/app/views/admin/users/edit.html.slim @@ -0,0 +1,4 @@ +.container.mx-auto.px-4 + h2.mt-8.text-2xl.font-bold.leading-9.tracking-tight.text-primary + | Edit User + = render partial: 'form', locals: { user: @user } diff --git a/app/views/admin/users/new.html.slim b/app/views/admin/users/new.html.slim new file mode 100644 index 0000000..28a1bcc --- /dev/null +++ b/app/views/admin/users/new.html.slim @@ -0,0 +1,4 @@ +.container.mx-auto.px-4 + h2.mt-8.text-2xl.font-bold.leading-9.tracking-tight.text-primary + | New User + = render partial: 'form', locals: { user: @user } diff --git a/app/views/admin/users/show.html.slim b/app/views/admin/users/show.html.slim new file mode 100644 index 0000000..6f32ed4 --- /dev/null +++ b/app/views/admin/users/show.html.slim @@ -0,0 +1,7 @@ +.container.mx-auto.px-4 + h2.mt-8.text-2xl.font-bold.leading-9.tracking-tight.text-primary + | Show User + h5 + = "Email: #{@user.email}" + h5 + = "Roles: #{@user.roles_name.map(&:titleize).join(', ')}" diff --git a/app/views/layouts/_admin_header.html.slim b/app/views/layouts/_admin_header.html.slim deleted file mode 100644 index 756b042..0000000 --- a/app/views/layouts/_admin_header.html.slim +++ /dev/null @@ -1,2 +0,0 @@ -nav class="bg-white border-gray-200 dark:bg-gray-900" - | Add Admin Header here... diff --git a/app/views/layouts/admin.html.slim b/app/views/layouts/admin.html.slim index 0d07ff8..ade34a5 100644 --- a/app/views/layouts/admin.html.slim +++ b/app/views/layouts/admin.html.slim @@ -16,5 +16,5 @@ html[lang="en"] = vite_client_tag = vite_javascript_tag "admin", "data-turbo-track": "reload" body data-controller="icons" - = render "layouts/admin_header" + = render "layouts/admin/header" = yield diff --git a/app/views/layouts/admin/_header.html.slim b/app/views/layouts/admin/_header.html.slim new file mode 100644 index 0000000..a89ffd9 --- /dev/null +++ b/app/views/layouts/admin/_header.html.slim @@ -0,0 +1,39 @@ +div class='bg-base-100 text-base-content sticky top-0 z-30 flex h-16 w-full justify-center bg-opacity-90 backdrop-blur transition-shadow duration-100 [transform:translate3d(0,0,0)]' + .navbar.w-full + .flex-none + label.btn.btn-square.btn-ghost.drawer-button[for='my-drawer'] + svg.inline-block.w-6.h-6.stroke-current[xmlns='http://www.w3.org/2000/svg' fill='none' viewbox='0 0 24 24'] + path[stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6h16M4 12h16M4 18h16'] + .flex-none.px-2.mx-2 + span.text-lg.font-bold + a.flex-0.btn.btn-ghost.px-2[href='/' aria-current='page' aria-label='Homepage'] + svg[width='32' height='32' viewbox='0 0 415 415' xmlns='http://www.w3.org/2000/svg'] + rect[x='82.5' y='290' width='250' height='125' rx='62.5' fill='#1AD1A5'] + circle[cx='207.5' cy='135' r='130' fill='black' fill-opacity='.3'] + circle[cx='207.5' cy='135' r='125' fill='white'] + circle[cx='207.5' cy='135' r='56' fill='#FF9903'] + div class='font-title inline-flex text-lg md:text-2xl' + span.lowercase + | daisy + span class='uppercase text-[#1AD1A5]' + | UI + div class='form-control hidden md:flex' + div class='flex justify-center flex-1 px-2 mx-2' + .dropdown.dropdown-end[title='Change Theme'] + .m-1.normal-case[tabindex='0'] + = render 'shared/switch_theme' + .flex-none.gap-2 + .dropdown.dropdown-end + div class='btn btn-ghost btn-circle avatar' tabindex='0' + div class='w-10 rounded-full' + img alt='Avatar' src='https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp' + ul class='mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52' tabindex='0' + li + a.justify-between + | Profile + span.badge + | New + li + a Settings + li + = button_to 'Log out', destroy_user_session_path, method: :delete, data: { turbo: false }