diff --git a/Gemfile b/Gemfile index d4f8525e..f37ec3c3 100644 --- a/Gemfile +++ b/Gemfile @@ -28,8 +28,12 @@ gem 'jbuilder', '~> 2.7' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false +# Authentication gem 'devise', git: 'https://github.com/plataformatec/devise' +# Administration panel +gem "administrate", git: "https://github.com/thoughtbot/administrate.git" + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 80c449b6..53f92aa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,6 +9,23 @@ GIT responders warden (~> 1.2.3) +GIT + remote: https://github.com/thoughtbot/administrate.git + revision: fcf46ae4e6989ceb1b091791fec754a67da0f32c + specs: + administrate (0.11.0) + actionpack (>= 4.2) + actionview (>= 4.2) + activejob (>= 4.2) + activerecord (>= 4.2) + autoprefixer-rails (>= 6.0) + datetime_picker_rails (~> 0.0.7) + jquery-rails (>= 4.0) + kaminari (>= 1.0) + momentjs-rails (~> 2.8) + sassc-rails (~> 2.1) + selectize-rails (~> 0.6) + GEM remote: https://rubygems.org/ specs: @@ -69,6 +86,8 @@ GEM zeitwerk (~> 2.1, >= 2.1.8) addressable (2.6.0) public_suffix (>= 2.0.2, < 4.0) + autoprefixer-rails (9.6.1) + execjs bcrypt (3.1.13) bindex (0.8.1) bootsnap (1.4.4) @@ -87,8 +106,11 @@ GEM rake (< 13.0) concurrent-ruby (1.1.5) crass (1.0.4) + datetime_picker_rails (0.0.7) + momentjs-rails (>= 2.8.1) diff-lcs (1.3) erubi (1.8.0) + execjs (2.7.0) factory_bot (5.0.2) activesupport (>= 4.2.0) factory_bot_rails (5.0.2) @@ -101,6 +123,22 @@ GEM concurrent-ruby (~> 1.0) jbuilder (2.9.1) activesupport (>= 4.2.0) + jquery-rails (4.3.5) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + kaminari (1.1.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.1.1) + kaminari-activerecord (= 1.1.1) + kaminari-core (= 1.1.1) + kaminari-actionview (1.1.1) + actionview + kaminari-core (= 1.1.1) + kaminari-activerecord (1.1.1) + activerecord + kaminari-core (= 1.1.1) + kaminari-core (1.1.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -117,6 +155,8 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.11.3) + momentjs-rails (2.20.1) + railties (>= 3.1) msgpack (1.3.1) nio4r (2.4.0) nokogiri (1.10.4) @@ -194,6 +234,15 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + sassc (2.1.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + selectize-rails (0.12.6) selenium-webdriver (3.142.3) childprocess (>= 0.5, < 2.0) rubyzip (~> 1.2, >= 1.2.2) @@ -242,6 +291,7 @@ PLATFORMS ruby DEPENDENCIES + administrate! bootsnap (>= 1.4.2) byebug capybara (>= 2.15) diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb new file mode 100644 index 00000000..65f65649 --- /dev/null +++ b/app/controllers/admin/application_controller.rb @@ -0,0 +1,31 @@ +# All Administrate controllers inherit from this `Admin::ApplicationController`, +# making it the ideal place to put authentication logic or other +# before_actions. +# +# If you want to add pagination or other controller-level concerns, +# you're free to overwrite the RESTful controller actions. +module Admin + class ApplicationController < Administrate::ApplicationController + before_action :authenticate_admin + + def authenticate_admin + unless user_signed_in? + flash[:alert] = "You must be logged in to access this page." + redirect_to new_user_session_path + return + end + + unless current_user.moderator? || current_user.admin? + flash[:alert] = "You do not have the privilegies to access this page." + redirect_to root_path + return + end + end + + # Override this value to specify the number of elements to display at a time + # on index pages. Defaults to 20. + # def records_per_page + # params[:per_page] || 20 + # end + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 00000000..21b1ffb5 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,88 @@ +module Admin + class UsersController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # foo = Foo.find(params[:id]) + # foo.update(params[:foo]) + # send_foo_updated_email + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information + + def authenticate_admin + super # apply the generic rules for authentication in the admin panel... + + # ...plus this one + unless current_user.admin? + flash[:alert] = "You do not have the privilegies to access this page." + redirect_to root_path + return + end + end + + # overwrite default create + def create + user = User.new(user_params) + user.skip_confirmation! # automatically confirm user email + + if user.save + flash[:notice] = translate_with_resource("create.success") + redirect_to admin_user_path(user) + else + render :new, locals: { + page: Administrate::Page::Form.new(dashboard, user), + } + end + end + + # overwrite default update + def update + user = User.find(params[:user][:id]) + + if params[:user][:password].empty? + user.assign_attributes(user_params.except(:password)) + else + user.assign_attributes(user_params) + end + + user.skip_reconfirmation! # automatically reconfirm user email + + if user.save + flash[:notice] = translate_with_resource("update.success") + redirect_to admin_user_path(user) + else + render :new, locals: { + page: Administrate::Page::Form.new(dashboard, user), + } + end + end + + private + + def user_params + params.require(:user).permit(:full_name, :email, :role, :password) + end + end +end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb new file mode 100644 index 00000000..98a2eaac --- /dev/null +++ b/app/dashboards/user_dashboard.rb @@ -0,0 +1,83 @@ +require "administrate/base_dashboard" + +class UserDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: IdField, + email: Field::String, + password: Field::Password, + encrypted_password: Field::String, + reset_password_token: Field::String, + reset_password_sent_at: Field::DateTime, + remember_created_at: Field::DateTime, + confirmation_token: Field::String, + confirmed_at: Field::DateTime, + confirmation_sent_at: Field::DateTime, + unconfirmed_email: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + role: RoleField, + full_name: Field::String, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + full_name + email + role + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + full_name + email + role + password + created_at + updated_at + confirmed_at + confirmation_sent_at + unconfirmed_email + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + id + full_name + email + role + password + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how users are displayed + # across all pages of the admin dashboard. + # + # def display_resource(user) + # "User ##{user.id}" + # end +end diff --git a/app/fields/id_field.rb b/app/fields/id_field.rb new file mode 100644 index 00000000..c155d4ff --- /dev/null +++ b/app/fields/id_field.rb @@ -0,0 +1,7 @@ +require "administrate/field/base" + +class IdField < Administrate::Field::Base + def to_s + data.to_s + end +end diff --git a/app/fields/role_field.rb b/app/fields/role_field.rb new file mode 100644 index 00000000..0c5e804c --- /dev/null +++ b/app/fields/role_field.rb @@ -0,0 +1,13 @@ +require "administrate/field/base" + +class RoleField < Administrate::Field::Base + def to_s + data.to_s + end + + def select_field_values(form_builder) + form_builder.object.class.public_send(attribute.to_s.pluralize).keys.map do |v| + [v.titleize, v] + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 376c45c7..f1ea4246 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,8 @@ class User < ApplicationRecord + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable, + :confirmable + enum role: [:user, :moderator, :admin] after_initialize :set_default_role, if: :new_record? @@ -12,8 +16,4 @@ class User < ApplicationRecord gravatar_id = Digest::MD5::hexdigest(email.downcase) "https://secure.gravatar.com/avatar/#{gravatar_id}" end - - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable, - :confirmable end diff --git a/app/views/admin/application/_navigation.html.erb b/app/views/admin/application/_navigation.html.erb new file mode 100644 index 00000000..8a97ca38 --- /dev/null +++ b/app/views/admin/application/_navigation.html.erb @@ -0,0 +1,19 @@ +<%# +# Navigation + +This partial is used to display the navigation in Administrate. +By default, the navigation contains navigation links +for all resources in the admin dashboard, +as defined by the routes in the `admin/` namespace +%> + + diff --git a/app/views/fields/id_field/_form.html.erb b/app/views/fields/id_field/_form.html.erb new file mode 100644 index 00000000..bb387ecd --- /dev/null +++ b/app/views/fields/id_field/_form.html.erb @@ -0,0 +1 @@ +<%= f.text_field field.attribute, hidden: :true %> diff --git a/app/views/fields/id_field/_index.html.erb b/app/views/fields/id_field/_index.html.erb new file mode 100644 index 00000000..6d9dbc90 --- /dev/null +++ b/app/views/fields/id_field/_index.html.erb @@ -0,0 +1 @@ +<%= field.to_s %> diff --git a/app/views/fields/id_field/_show.html.erb b/app/views/fields/id_field/_show.html.erb new file mode 100644 index 00000000..6d9dbc90 --- /dev/null +++ b/app/views/fields/id_field/_show.html.erb @@ -0,0 +1 @@ +<%= field.to_s %> diff --git a/app/views/fields/role_field/_form.html.erb b/app/views/fields/role_field/_form.html.erb new file mode 100644 index 00000000..0e9be501 --- /dev/null +++ b/app/views/fields/role_field/_form.html.erb @@ -0,0 +1,6 @@ +
+ <%= f.label field.attribute %> +
+
+ <%= f.select field.attribute, field.select_field_values(f) %> +
\ No newline at end of file diff --git a/app/views/fields/role_field/_index.html.erb b/app/views/fields/role_field/_index.html.erb new file mode 100644 index 00000000..1430540f --- /dev/null +++ b/app/views/fields/role_field/_index.html.erb @@ -0,0 +1 @@ +<%= field.to_s.titleize %> diff --git a/app/views/fields/role_field/_show.html.erb b/app/views/fields/role_field/_show.html.erb new file mode 100644 index 00000000..1430540f --- /dev/null +++ b/app/views/fields/role_field/_show.html.erb @@ -0,0 +1 @@ +<%= field.to_s.titleize %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index a36beeb0..7d073846 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -8,6 +8,11 @@