diff --git a/.env.example b/.env.example index addf9b685..80fec6985 100644 --- a/.env.example +++ b/.env.example @@ -73,10 +73,6 @@ RAILS_LOG_TO_STDOUT=true LOG_LEVEL=info LOG_SIZE=500 -# Credentials to access sidekiq dashboard in production -SIDEKIQ_AUTH_USERNAME= -SIDEKIQ_AUTH_PASSWORD= - ### This environment variables are only required if you are setting up social media channels #facebook FB_VERIFY_TOKEN= diff --git a/Gemfile b/Gemfile index 61b3f4bcd..b9e9c4f4c 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,8 @@ gem 'devise_token_auth' # authorization gem 'jwt' gem 'pundit' +# super admin +gem 'administrate' ##--- gems for pubsub service ---## # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ diff --git a/Gemfile.lock b/Gemfile.lock index 1932d7e72..1659d8cdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,11 +84,24 @@ GEM activerecord (>= 5.0, < 6.1) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) + administrate (0.13.0) + actionpack (>= 4.2) + actionview (>= 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) annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) ast (2.4.0) attr_extras (6.2.3) + autoprefixer-rails (9.7.6) + execjs aws-eventstream (1.1.0) aws-partitions (1.310.0) aws-sdk-core (3.94.1) @@ -141,6 +154,8 @@ GEM concurrent-ruby (1.1.6) connection_pool (2.2.2) crass (1.0.6) + datetime_picker_rails (0.0.7) + momentjs-rails (>= 2.8.1) declarative (0.0.10) declarative-option (0.1.0) descendants_tracker (0.0.4) @@ -235,6 +250,10 @@ GEM jbuilder (2.10.0) activesupport (>= 5.0.0) jmespath (1.4.0) + jquery-rails (4.3.5) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) json (2.3.0) json_pure (2.3.0) jwt (2.2.1) @@ -278,6 +297,8 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.0) + momentjs-rails (2.20.1) + railties (>= 3.1) msgpack (1.3.3) multi_json (1.14.1) multi_xml (0.6.0) @@ -406,6 +427,14 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + sassc (2.3.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt scout_apm (2.6.7) parser scss_lint (0.59.0) @@ -413,6 +442,7 @@ GEM seed_dump (3.3.1) activerecord (>= 4) activesupport (>= 4) + selectize-rails (0.12.6) semantic_range (2.3.0) sentry-raven (3.0.0) faraday (>= 1.0) @@ -451,6 +481,7 @@ GEM telephone_number (1.4.6) thor (0.20.3) thread_safe (0.3.6) + tilt (2.0.10) time_diff (0.3.0) activesupport i18n @@ -505,6 +536,7 @@ PLATFORMS DEPENDENCIES action-cable-testing acts-as-taggable-on + administrate annotate attr_extras aws-sdk-s3 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ac907b367..9b826819b 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1 +1,3 @@ //= link_tree ../images +//= link administrate/application.css +//= link administrate/application.js diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 126eedce0..9c724bd43 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -2,7 +2,7 @@ class AccountBuilder include CustomExceptions::Account - pattr_initialize [:account_name!, :email!] + pattr_initialize [:account_name!, :email!, :confirmed!] def perform validate_email @@ -46,6 +46,7 @@ class AccountBuilder password: password, password_confirmation: password, name: email_to_name(@email)) + @user.confirm if @confirmed if @user.save! link_user_to_account(@user, @account) @user diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts/accounts_controller.rb index 0fd5dc7cf..29e26929b 100644 --- a/app/controllers/api/v1/accounts/accounts_controller.rb +++ b/app/controllers/api/v1/accounts/accounts_controller.rb @@ -16,7 +16,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController def create @user = AccountBuilder.new( account_name: account_params[:account_name], - email: account_params[:email] + email: account_params[:email], + confirmed: confirmed? ).perform if @user send_auth_headers(@user) @@ -40,6 +41,10 @@ class Api::V1::Accounts::AccountsController < Api::BaseController authorize(Account) end + def confirmed? + super_admin? && params[:confirmed] + end + def fetch_account @account = current_user.accounts.find(params[:id]) end diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index e7af9e116..3d6f55674 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -4,17 +4,25 @@ module AccessTokenAuthHelper 'api/v1/accounts/conversations/messages' => ['create'] }.freeze - def authenticate_access_token! + def ensure_access_token token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN] - access_token = AccessToken.find_by(token: token) - render_unauthorized('Invalid Access Token') && return unless access_token + @access_token = AccessToken.find_by(token: token) if token.present? + end - token_owner = access_token.owner - @resource = token_owner + def authenticate_access_token! + ensure_access_token + render_unauthorized('Invalid Access Token') && return if @access_token.blank? + + @resource = @access_token.owner + end + + def super_admin? + @resource.present? && @resource.is_a?(SuperAdmin) end def validate_bot_access_token! return if current_user.is_a?(User) + return if super_admin? return if agent_bot_accessible? render_unauthorized('Access to this endpoint is not authorized for bots') diff --git a/app/controllers/super_admin/access_tokens_controller.rb b/app/controllers/super_admin/access_tokens_controller.rb new file mode 100644 index 000000000..a8a2669c4 --- /dev/null +++ b/app/controllers/super_admin/access_tokens_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::AccessTokensController < SuperAdmin::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 + # super + # send_foo_updated_email(requested_resource) + # 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 + + # The result of this lookup will be available as `requested_resource` + + # 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 + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/accounts_controller.rb b/app/controllers/super_admin/accounts_controller.rb new file mode 100644 index 000000000..4d35fa1d8 --- /dev/null +++ b/app/controllers/super_admin/accounts_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::AccountsController < SuperAdmin::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 + # super + # send_foo_updated_email(requested_resource) + # 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 + + # The result of this lookup will be available as `requested_resource` + + # 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 + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/application_controller.rb b/app/controllers/super_admin/application_controller.rb new file mode 100644 index 000000000..463ad30e6 --- /dev/null +++ b/app/controllers/super_admin/application_controller.rb @@ -0,0 +1,16 @@ +# All Administrate controllers inherit from this +# `Administrate::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. +class SuperAdmin::ApplicationController < Administrate::ApplicationController + # authenticiation done via devise : SuperAdmin Model + before_action :authenticate_super_admin! + + # 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 diff --git a/app/controllers/super_admin/devise/sessions_controller.rb b/app/controllers/super_admin/devise/sessions_controller.rb new file mode 100644 index 000000000..fe046a522 --- /dev/null +++ b/app/controllers/super_admin/devise/sessions_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class SuperAdmin::Devise::SessionsController < Devise::SessionsController + def new + self.resource = resource_class.new(sign_in_params) + end + + def create + return unless valid_credentials? + + sign_in(@super_admin, scope: :super_admin) + flash.discard + redirect_to super_admin_users_path + end + + def destroy + sign_out + flash.discard + redirect_to '/' + end + + private + + def valid_credentials? + @super_admin = SuperAdmin.find_by!(email: params[:super_admin][:email]) + @super_admin.valid_password?(params[:super_admin][:password]) + end +end diff --git a/app/controllers/super_admin/super_admins_controller.rb b/app/controllers/super_admin/super_admins_controller.rb new file mode 100644 index 000000000..16d91a151 --- /dev/null +++ b/app/controllers/super_admin/super_admins_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::SuperAdminsController < SuperAdmin::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 + # super + # send_foo_updated_email(requested_resource) + # 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 + + # The result of this lookup will be available as `requested_resource` + + # 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 + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb new file mode 100644 index 000000000..613670849 --- /dev/null +++ b/app/controllers/super_admin/users_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::UsersController < SuperAdmin::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 + # super + # send_foo_updated_email(requested_resource) + # 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 + + # The result of this lookup will be available as `requested_resource` + + # 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 + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/dashboards/access_token_dashboard.rb b/app/dashboards/access_token_dashboard.rb new file mode 100644 index 000000000..bdc50a7db --- /dev/null +++ b/app/dashboards/access_token_dashboard.rb @@ -0,0 +1,66 @@ +require 'administrate/base_dashboard' + +class AccessTokenDashboard < 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 = { + owner: Field::Polymorphic, + id: Field::Number, + token: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.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[ + owner + id + token + created_at + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + owner + id + token + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + owner + token + ].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) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how access tokens are displayed + # across all pages of the admin dashboard. + # + # def display_resource(access_token) + # "AccessToken ##{access_token.id}" + # end +end diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb new file mode 100644 index 000000000..d80abc199 --- /dev/null +++ b/app/dashboards/account_dashboard.rb @@ -0,0 +1,64 @@ +require 'administrate/base_dashboard' + +class AccountDashboard < 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: Field::Number, + name: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + locale: Field::String.with_options(searchable: false) + }.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[ + name + locale + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + name + created_at + updated_at + locale + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + locale + ].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) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how accounts are displayed + # across all pages of the admin dashboard. + # + # def display_resource(account) + # "Account ##{account.id}" + # end +end diff --git a/app/dashboards/super_admin_dashboard.rb b/app/dashboards/super_admin_dashboard.rb new file mode 100644 index 000000000..4ceab3a17 --- /dev/null +++ b/app/dashboards/super_admin_dashboard.rb @@ -0,0 +1,81 @@ +require 'administrate/base_dashboard' + +class SuperAdminDashboard < 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: Field::Number, + email: Field::String, + access_token: Field::HasOne, + remember_created_at: Field::DateTime, + sign_in_count: Field::Number, + current_sign_in_at: Field::DateTime, + last_sign_in_at: Field::DateTime, + current_sign_in_ip: Field::String.with_options(searchable: false), + last_sign_in_ip: Field::String.with_options(searchable: false), + created_at: Field::DateTime, + updated_at: Field::DateTime + }.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[ + id + email + access_token + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + email + remember_created_at + sign_in_count + current_sign_in_at + last_sign_in_at + current_sign_in_ip + last_sign_in_ip + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + email + remember_created_at + sign_in_count + current_sign_in_at + last_sign_in_at + current_sign_in_ip + last_sign_in_ip + ].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) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how super admins are displayed + # across all pages of the admin dashboard. + # + # def display_resource(super_admin) + # "SuperAdmin ##{super_admin.id}" + # end +end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb new file mode 100644 index 000000000..e8d24eae2 --- /dev/null +++ b/app/dashboards/user_dashboard.rb @@ -0,0 +1,88 @@ +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 = { + account_users: Field::HasMany, + accounts: Field::HasMany, + invitees: Field::HasMany.with_options(class_name: 'User'), + id: Field::Number, + provider: Field::String, + uid: Field::String, + reset_password_token: Field::String, + reset_password_sent_at: Field::DateTime, + remember_created_at: Field::DateTime, + sign_in_count: Field::Number, + current_sign_in_at: Field::DateTime, + last_sign_in_at: Field::DateTime, + current_sign_in_ip: Field::String, + last_sign_in_ip: Field::String, + confirmation_token: Field::String, + confirmed_at: Field::DateTime, + confirmation_sent_at: Field::DateTime, + unconfirmed_email: Field::String, + name: Field::String, + nickname: Field::String, + email: Field::String, + tokens: Field::String.with_options(searchable: false), + created_at: Field::DateTime, + updated_at: Field::DateTime, + pubsub_token: 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[ + name + email + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + accounts + id + unconfirmed_email + name + nickname + email + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + nickname + email + ].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) { 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/javascript/dashboard/assets/scss/super_admin/index.scss b/app/javascript/dashboard/assets/scss/super_admin/index.scss new file mode 100644 index 000000000..c45cf2e22 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/index.scss @@ -0,0 +1,5 @@ +@import '../variables'; + +.superadmin-body { + background: $color-background; +} diff --git a/app/javascript/dashboard/assets/scss/super_admin/pages.scss b/app/javascript/dashboard/assets/scss/super_admin/pages.scss new file mode 100644 index 000000000..91b62d671 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/pages.scss @@ -0,0 +1,13 @@ +@import 'shared/assets/fonts/inter'; +@import '../variables'; + +body { + background-color: $color-background; + font-family: Inter; +} + +.button { + background-color: $color-woot; + border-radius: 1px solid $color-woot; + color: $color-white; +} diff --git a/app/javascript/packs/superadmin.js b/app/javascript/packs/superadmin.js new file mode 100644 index 000000000..90e58bd5e --- /dev/null +++ b/app/javascript/packs/superadmin.js @@ -0,0 +1,2 @@ +import '../dashboard/assets/scss/app.scss'; +import '../dashboard/assets/scss/super_admin/index.scss'; diff --git a/app/javascript/packs/superadmin_pages.js b/app/javascript/packs/superadmin_pages.js new file mode 100644 index 000000000..4870b6f0f --- /dev/null +++ b/app/javascript/packs/superadmin_pages.js @@ -0,0 +1 @@ +import '../dashboard/assets/scss/super_admin/pages.scss'; diff --git a/app/models/super_admin.rb b/app/models/super_admin.rb new file mode 100644 index 000000000..72b1ceb8a --- /dev/null +++ b/app/models/super_admin.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: super_admins +# +# id :bigint not null, primary key +# current_sign_in_at :datetime +# current_sign_in_ip :inet +# email :string default(""), not null +# encrypted_password :string default(""), not null +# last_sign_in_at :datetime +# last_sign_in_ip :inet +# remember_created_at :datetime +# sign_in_count :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_super_admins_on_email (email) UNIQUE +# +class SuperAdmin < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :trackable, :rememberable, :validatable + + include AccessTokenable +end diff --git a/app/views/super_admin/application/_navigation.html.erb b/app/views/super_admin/application/_navigation.html.erb new file mode 100644 index 000000000..348309e7b --- /dev/null +++ b/app/views/super_admin/application/_navigation.html.erb @@ -0,0 +1,27 @@ +<%# +# 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 +%> + +<%= javascript_pack_tag 'superadmin_pages' %> +<%= stylesheet_pack_tag 'superadmin_pages' %> + + + diff --git a/app/views/super_admin/devise/sessions/new.html.erb b/app/views/super_admin/devise/sessions/new.html.erb new file mode 100644 index 000000000..294ee66ee --- /dev/null +++ b/app/views/super_admin/devise/sessions/new.html.erb @@ -0,0 +1,43 @@ + + + + SuperAdmin | Chatwoot + <%= javascript_pack_tag 'superadmin' %> + <%= stylesheet_pack_tag 'superadmin' %> + + +
+
+ +
+
+ <%= form_for(resource, as: resource_name, url: '/super_admin/sign_in', html: { class: 'login-box column align-self-top'}) do |f| %> + + <% end %> + +
+
+
+ + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ce0010637..80eb40433 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -6,7 +6,7 @@ Devise.setup do |config| # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` as its `secret_key` # by default. You can change it below and use your own secret key. - # config.secret_key = 'dff4665a082305d28b485d1d763d0d3e52e2577220eaa551836862a3dbca1aade309fe7ceed35180ac494cbc27bd2f5f84d45e4d19530598d1bd899dcbb115e1' + # config.secret_key = 'dff4665a082305d28b485d1d763d0d3e52e2577220eaa551836862a3dbca1aade309fe7ceed35180ac494cbc27bd2f5f84d45e1' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -220,15 +220,15 @@ Devise.setup do |config| # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. - # config.scoped_views = false + config.scoped_views = true # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). - # config.default_scope = :user + config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true + config.sign_out_all_scopes = true # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like diff --git a/config/routes.rb b/config/routes.rb index 50331d25a..35f162e0d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,20 +168,20 @@ Rails.application.routes.draw do # Internal Monitoring Routes require 'sidekiq/web' - scope :monitoring do - # Sidekiq should use basic auth in production environment - if Rails.env.production? - Sidekiq::Web.use Rack::Auth::Basic do |username, password| - ENV['SIDEKIQ_AUTH_USERNAME'] && - ENV['SIDEKIQ_AUTH_PASSWORD'] && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), - ::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_USERNAME'])) && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), - ::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_PASSWORD'])) - end - end + devise_for :super_admins, path: 'super_admin', controllers: { sessions: 'super_admin/devise/sessions' } + devise_scope :super_admin do + get 'super_admin/logout', to: 'super_admin/devise/sessions#destroy' + namespace :super_admin do + resources :users + resources :accounts + resources :super_admins + resources :access_tokens - mount Sidekiq::Web, at: '/sidekiq' + root to: 'users#index' + end + authenticated :super_admin do + mount Sidekiq::Web => '/monitoring/sidekiq' + end end # --------------------------------------------------------------------- diff --git a/db/migrate/20200410145519_devise_create_super_admins.rb b/db/migrate/20200410145519_devise_create_super_admins.rb new file mode 100644 index 000000000..4a74847dc --- /dev/null +++ b/db/migrate/20200410145519_devise_create_super_admins.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class DeviseCreateSuperAdmins < ActiveRecord::Migration[6.0] + def change + return if ActiveRecord::Base.connection.table_exists? 'super_admins' + + create_table :super_admins do |t| + ## Database authenticatable + t.string :email, null: false, default: '' + t.string :encrypted_password, null: false, default: '' + + ## Recoverable + # t.string :reset_password_token + # t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.inet :current_sign_in_ip + t.inet :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.timestamps null: false + end + + add_index :super_admins, :email, unique: true + # add_index :super_admins, :reset_password_token, unique: true + # add_index :super_admins, :confirmation_token, unique: true + # add_index :super_admins, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7fab7d066..6965c2d03 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -327,6 +327,20 @@ ActiveRecord::Schema.define(version: 2020_05_10_112339) do t.boolean "payment_source_added", default: false end + create_table "super_admins", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.inet "current_sign_in_ip" + t.inet "last_sign_in_ip" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_super_admins_on_email", unique: true + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb index 70b6953a3..955cf1e72 100644 --- a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb @@ -2,9 +2,10 @@ require 'rails_helper' RSpec.describe 'Accounts API', type: :request do describe 'POST /api/v1/accounts' do + let(:email) { Faker::Internet.email } + context 'when posting to accounts with correct parameters' do let(:account_builder) { double } - let(:email) { Faker::Internet.email } let(:account) { create(:account) } let(:user) { create(:user, email: email, account: account) } @@ -22,7 +23,7 @@ RSpec.describe 'Accounts API', type: :request do params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params) + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) expect(account_builder).to have_received(:perform) expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') end @@ -36,16 +37,45 @@ RSpec.describe 'Accounts API', type: :request do params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params) + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) + expect(account_builder).to have_received(:perform) + expect(response).to have_http_status(:forbidden) + expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) + end + + it 'ignores confirmed param when called with out super admin token' do + allow(account_builder).to receive(:perform).and_return(nil) + + params = { account_name: 'test', email: email, confirmed: true } + + post api_v1_accounts_url, + params: params, + as: :json + + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) expect(account_builder).to have_received(:perform) expect(response).to have_http_status(:forbidden) expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) end end - context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do - let(:email) { Faker::Internet.email } + context 'when called with super admin token' do + let(:super_admin) { create(:super_admin) } + it 'calls account builder with confirmed true when confirmed param is passed' do + params = { account_name: 'test', email: email, confirmed: true } + + post api_v1_accounts_url, + params: params, + headers: { api_access_token: super_admin.access_token.token }, + as: :json + + expect(User.find_by(email: email).confirmed?).to eq(true) + expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') + end + end + + context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do it 'responds 404 on requests' do params = { account_name: 'test', email: email } ENV['ENABLE_ACCOUNT_SIGNUP'] = 'false' @@ -60,8 +90,6 @@ RSpec.describe 'Accounts API', type: :request do end context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do - let(:email) { Faker::Internet.email } - it 'does not respond 404 on requests' do params = { account_name: 'test', email: email } ENV['ENABLE_ACCOUNT_SIGNUP'] = 'api_only' diff --git a/spec/controllers/super_admin/access_tokens_controller_spec.rb b/spec/controllers/super_admin/access_tokens_controller_spec.rb new file mode 100644 index 000000000..c1f38ea23 --- /dev/null +++ b/spec/controllers/super_admin/access_tokens_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin access tokens API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/access_tokens' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + it 'shows the list of access tokens' do + sign_in super_admin + get '/super_admin/access_tokens' + expect(response).to have_http_status(:success) + expect(response.body).to include('New access token') + expect(response.body).to include(super_admin.access_token.token) + end + end + end +end diff --git a/spec/controllers/super_admin/accounts_controller_spec.rb b/spec/controllers/super_admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f4da5ad3d --- /dev/null +++ b/spec/controllers/super_admin/accounts_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin accounts API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/accounts' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get '/super_admin/accounts' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated user' do + let!(:account) { create(:account) } + + it 'shows the list of accounts' do + sign_in super_admin + get '/super_admin/accounts' + expect(response).to have_http_status(:success) + expect(response.body).to include('New account') + expect(response.body).to include(account.name) + end + end + end +end diff --git a/spec/controllers/super_admin/super_admins_controller_spec.rb b/spec/controllers/super_admin/super_admins_controller_spec.rb new file mode 100644 index 000000000..0255b520c --- /dev/null +++ b/spec/controllers/super_admin/super_admins_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin super admins API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/users' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/super_admins' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + it 'shows the list of super admins' do + sign_in super_admin + get '/super_admin/super_admins' + expect(response).to have_http_status(:success) + expect(response.body).to include('New super admin') + expect(response.body).to include(super_admin.email) + end + end + end +end diff --git a/spec/controllers/super_admin/users_controller_spec.rb b/spec/controllers/super_admin/users_controller_spec.rb new file mode 100644 index 000000000..7a1385213 --- /dev/null +++ b/spec/controllers/super_admin/users_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin Users API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/users' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + let!(:user) { create(:user) } + + it 'shows the list of users' do + sign_in super_admin + get '/super_admin' + expect(response).to have_http_status(:success) + expect(response.body).to include('New user') + expect(response.body).to include(user.name) + end + end + end +end diff --git a/spec/controllers/super_admin_controller_spec.rb b/spec/controllers/super_admin_controller_spec.rb new file mode 100644 index 000000000..ea0697dd9 --- /dev/null +++ b/spec/controllers/super_admin_controller_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'request to /super_admin' do + context 'when the super admin is unauthenticated' do + it 'redirects to signin page' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + expect(response.body).to include('sign_in') + end + + it 'signs super admin in and out' do + sign_in super_admin + get '/super_admin' + expect(response).to have_http_status(:success) + expect(response.body).to include('New user') + + sign_out super_admin + get '/super_admin' + expect(response).to have_http_status(:redirect) + end + end + end + + describe 'request to /super_admin/sidekiq' do + context 'when the super admin is unauthenticated' do + it 'redirects to signin page' do + get '/monitoring/sidekiq' + expect(response).to have_http_status(:not_found) + expect(response.body).to include('sign_in') + end + + it 'signs super admin in and out' do + sign_in super_admin + get '/monitoring/sidekiq' + expect(response).to have_http_status(:success) + + sign_out super_admin + get '/monitoring/sidekiq' + expect(response).to have_http_status(:not_found) + end + end + end +end diff --git a/spec/factories/super_admins.rb b/spec/factories/super_admins.rb new file mode 100644 index 000000000..88e488f68 --- /dev/null +++ b/spec/factories/super_admins.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :super_admin do + email { "admin@#{SecureRandom.uuid}.com" } + password { 'password' } + end +end diff --git a/spec/models/super_admin_spec.rb b/spec/models/super_admin_spec.rb new file mode 100644 index 000000000..e9a03b361 --- /dev/null +++ b/spec/models/super_admin_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SuperAdmin, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0c0d89421..ef39ac0ef 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -61,6 +61,8 @@ RSpec.configure do |config| config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::IntegrationHelpers, type: :request end Shoulda::Matchers.configure do |config|