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
37 changed files with 964 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

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>