Feature: Introduce Super Admins (#705)

* Feature: Introduce Super Admins

- added new devise model for super user
- added administrate gem
- sample dashboards for users and accounts

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose 2020-05-11 23:07:22 +05:30 committed by GitHub
parent 8859880e55
commit c74b5c21d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 964 additions and 35 deletions

View file

@ -73,10 +73,6 @@ RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info LOG_LEVEL=info
LOG_SIZE=500 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 ### This environment variables are only required if you are setting up social media channels
#facebook #facebook
FB_VERIFY_TOKEN= FB_VERIFY_TOKEN=

View file

@ -49,6 +49,8 @@ gem 'devise_token_auth'
# authorization # authorization
gem 'jwt' gem 'jwt'
gem 'pundit' gem 'pundit'
# super admin
gem 'administrate'
##--- gems for pubsub service ---## ##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/

View file

@ -84,11 +84,24 @@ GEM
activerecord (>= 5.0, < 6.1) activerecord (>= 5.0, < 6.1)
addressable (2.7.0) addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.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) annotate (3.1.1)
activerecord (>= 3.2, < 7.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.0) ast (2.4.0)
attr_extras (6.2.3) attr_extras (6.2.3)
autoprefixer-rails (9.7.6)
execjs
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.310.0) aws-partitions (1.310.0)
aws-sdk-core (3.94.1) aws-sdk-core (3.94.1)
@ -141,6 +154,8 @@ GEM
concurrent-ruby (1.1.6) concurrent-ruby (1.1.6)
connection_pool (2.2.2) connection_pool (2.2.2)
crass (1.0.6) crass (1.0.6)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
declarative (0.0.10) declarative (0.0.10)
declarative-option (0.1.0) declarative-option (0.1.0)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
@ -235,6 +250,10 @@ GEM
jbuilder (2.10.0) jbuilder (2.10.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.4.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 (2.3.0)
json_pure (2.3.0) json_pure (2.3.0)
jwt (2.2.1) jwt (2.2.1)
@ -278,6 +297,8 @@ GEM
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.14.0) minitest (5.14.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
msgpack (1.3.3) msgpack (1.3.3)
multi_json (1.14.1) multi_json (1.14.1)
multi_xml (0.6.0) multi_xml (0.6.0)
@ -406,6 +427,14 @@ GEM
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) 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) scout_apm (2.6.7)
parser parser
scss_lint (0.59.0) scss_lint (0.59.0)
@ -413,6 +442,7 @@ GEM
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (2.3.0) semantic_range (2.3.0)
sentry-raven (3.0.0) sentry-raven (3.0.0)
faraday (>= 1.0) faraday (>= 1.0)
@ -451,6 +481,7 @@ GEM
telephone_number (1.4.6) telephone_number (1.4.6)
thor (0.20.3) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.10)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
@ -505,6 +536,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
action-cable-testing action-cable-testing
acts-as-taggable-on acts-as-taggable-on
administrate
annotate annotate
attr_extras attr_extras
aws-sdk-s3 aws-sdk-s3

View file

@ -1 +1,3 @@
//= link_tree ../images //= link_tree ../images
//= link administrate/application.css
//= link administrate/application.js

View file

@ -2,7 +2,7 @@
class AccountBuilder class AccountBuilder
include CustomExceptions::Account include CustomExceptions::Account
pattr_initialize [:account_name!, :email!] pattr_initialize [:account_name!, :email!, :confirmed!]
def perform def perform
validate_email validate_email
@ -46,6 +46,7 @@ class AccountBuilder
password: password, password: password,
password_confirmation: password, password_confirmation: password,
name: email_to_name(@email)) name: email_to_name(@email))
@user.confirm if @confirmed
if @user.save! if @user.save!
link_user_to_account(@user, @account) link_user_to_account(@user, @account)
@user @user

View file

@ -16,7 +16,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
def create def create
@user = AccountBuilder.new( @user = AccountBuilder.new(
account_name: account_params[:account_name], account_name: account_params[:account_name],
email: account_params[:email] email: account_params[:email],
confirmed: confirmed?
).perform ).perform
if @user if @user
send_auth_headers(@user) send_auth_headers(@user)
@ -40,6 +41,10 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
authorize(Account) authorize(Account)
end end
def confirmed?
super_admin? && params[:confirmed]
end
def fetch_account def fetch_account
@account = current_user.accounts.find(params[:id]) @account = current_user.accounts.find(params[:id])
end end

View file

@ -4,17 +4,25 @@ module AccessTokenAuthHelper
'api/v1/accounts/conversations/messages' => ['create'] 'api/v1/accounts/conversations/messages' => ['create']
}.freeze }.freeze
def authenticate_access_token! def ensure_access_token
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN] token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
access_token = AccessToken.find_by(token: token) @access_token = AccessToken.find_by(token: token) if token.present?
render_unauthorized('Invalid Access Token') && return unless access_token end
token_owner = access_token.owner def authenticate_access_token!
@resource = token_owner 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 end
def validate_bot_access_token! def validate_bot_access_token!
return if current_user.is_a?(User) return if current_user.is_a?(User)
return if super_admin?
return if agent_bot_accessible? return if agent_bot_accessible?
render_unauthorized('Access to this endpoint is not authorized for bots') render_unauthorized('Access to this endpoint is not authorized for bots')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
@import '../variables';
.superadmin-body {
background: $color-background;
}

View file

@ -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;
}

View file

@ -0,0 +1,2 @@
import '../dashboard/assets/scss/app.scss';
import '../dashboard/assets/scss/super_admin/index.scss';

View file

@ -0,0 +1 @@
import '../dashboard/assets/scss/super_admin/pages.scss';

27
app/models/super_admin.rb Normal file
View file

@ -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

View file

@ -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' %>
<nav class="navigation" role="navigation">
<%= link_to "Back to app", root_url, class: "button button--alt" %>
<%= link_to "Logout", super_admin_logout_url , class: "button button--alt" %>
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
<%= link_to(
display_resource_name(resource),
[namespace, resource_index_route_key(resource)],
class: "navigation__link navigation__link--#{nav_link_state(resource)}"
) if valid_action? :index, resource %>
<% end %>
<%= link_to "Sidekiq", sidekiq_web_url , class: "button" %>
</nav>

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>SuperAdmin | Chatwoot</title>
<%= javascript_pack_tag 'superadmin' %>
<%= stylesheet_pack_tag 'superadmin' %>
</head>
<body data-gr-c-s-loaded="true">
<div id="app" class="superadmin-body app-wrapper app-root">
<div class="medium column login">
<div class="text-center medium-12 login__hero align-self-top">
<h2 class="hero__title">
Howdy, admin 👋
</h2>
</div>
<div class="row align-center">
<div class="small-12 medium-4 column">
<%= form_for(resource, as: resource_name, url: '/super_admin/sign_in', html: { class: 'login-box column align-self-top'}) do |f| %>
<div class="column log-in-form">
<label>
Email
<%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email eg: someone@example.com" %>
</label>
<label>
Password
<%= f.password_field :password, autocomplete: "current-password", placeholder: "Password" %>
</label>
<p>
<%= f.check_box :remember_me %> Remember me
</p>
<button type="submit" class="button nice large expanded">
Login
</button>
</div>
<% end %>
<div class="column text-center sigin__footer">
© Chatwoot
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -6,7 +6,7 @@ Devise.setup do |config|
# confirmation, reset password and unlock tokens in the database. # confirmation, reset password and unlock tokens in the database.
# Devise will use the `secret_key_base` as its `secret_key` # Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own 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 # ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer, # 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 # 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 # "users/sessions/new". It's turned off by default because it's slower if you
# are using only default views. # 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 # Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user). # 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 # 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. # 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 # ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like # Lists the formats that should be treated as navigational. Formats like

View file

@ -168,20 +168,20 @@ Rails.application.routes.draw do
# Internal Monitoring Routes # Internal Monitoring Routes
require 'sidekiq/web' require 'sidekiq/web'
scope :monitoring do devise_for :super_admins, path: 'super_admin', controllers: { sessions: 'super_admin/devise/sessions' }
# Sidekiq should use basic auth in production environment devise_scope :super_admin do
if Rails.env.production? get 'super_admin/logout', to: 'super_admin/devise/sessions#destroy'
Sidekiq::Web.use Rack::Auth::Basic do |username, password| namespace :super_admin do
ENV['SIDEKIQ_AUTH_USERNAME'] && resources :users
ENV['SIDEKIQ_AUTH_PASSWORD'] && resources :accounts
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), resources :super_admins
::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_USERNAME'])) && resources :access_tokens
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password),
::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_PASSWORD']))
end
end
mount Sidekiq::Web, at: '/sidekiq' root to: 'users#index'
end
authenticated :super_admin do
mount Sidekiq::Web => '/monitoring/sidekiq'
end
end end
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View file

@ -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

View file

@ -327,6 +327,20 @@ ActiveRecord::Schema.define(version: 2020_05_10_112339) do
t.boolean "payment_source_added", default: false t.boolean "payment_source_added", default: false
end 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| create_table "taggings", id: :serial, force: :cascade do |t|
t.integer "tag_id" t.integer "tag_id"
t.string "taggable_type" t.string "taggable_type"

View file

@ -2,9 +2,10 @@ require 'rails_helper'
RSpec.describe 'Accounts API', type: :request do RSpec.describe 'Accounts API', type: :request do
describe 'POST /api/v1/accounts' do describe 'POST /api/v1/accounts' do
let(:email) { Faker::Internet.email }
context 'when posting to accounts with correct parameters' do context 'when posting to accounts with correct parameters' do
let(:account_builder) { double } let(:account_builder) { double }
let(:email) { Faker::Internet.email }
let(:account) { create(:account) } let(:account) { create(:account) }
let(:user) { create(:user, email: email, account: account) } let(:user) { create(:user, email: email, account: account) }
@ -22,7 +23,7 @@ RSpec.describe 'Accounts API', type: :request do
params: params, params: params,
as: :json 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(account_builder).to have_received(:perform)
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
end end
@ -36,16 +37,45 @@ RSpec.describe 'Accounts API', type: :request do
params: params, params: params,
as: :json 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(account_builder).to have_received(:perform)
expect(response).to have_http_status(:forbidden) expect(response).to have_http_status(:forbidden)
expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json)
end end
end end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do context 'when called with super admin token' do
let(:email) { Faker::Internet.email } 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 it 'responds 404 on requests' do
params = { account_name: 'test', email: email } params = { account_name: 'test', email: email }
ENV['ENABLE_ACCOUNT_SIGNUP'] = 'false' ENV['ENABLE_ACCOUNT_SIGNUP'] = 'false'
@ -60,8 +90,6 @@ RSpec.describe 'Accounts API', type: :request do
end end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do 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 it 'does not respond 404 on requests' do
params = { account_name: 'test', email: email } params = { account_name: 'test', email: email }
ENV['ENABLE_ACCOUNT_SIGNUP'] = 'api_only' ENV['ENABLE_ACCOUNT_SIGNUP'] = 'api_only'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :super_admin do
email { "admin@#{SecureRandom.uuid}.com" }
password { 'password' }
end
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe SuperAdmin, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -61,6 +61,8 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace! config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via: # arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name") # config.filter_gems_from_backtrace("gem name")
config.include Devise::Test::IntegrationHelpers, type: :request
end end
Shoulda::Matchers.configure do |config| Shoulda::Matchers.configure do |config|