feat: Unify user and super admin credentials (#3830)

Fixes: #3061, #3489
This commit is contained in:
Sojan Jose 2022-01-25 16:58:49 -08:00 committed by GitHub
parent 23965fbaa3
commit 34e8ad9dc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 303 additions and 233 deletions

View file

@ -2,7 +2,7 @@
class AccountBuilder
include CustomExceptions::Account
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password]
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin]
def perform
if @user.nil?
@ -65,6 +65,7 @@ class AccountBuilder
password: user_password,
password_confirmation: user_password,
name: @user_full_name)
@user.type = 'SuperAdmin' if @super_admin
@user.confirm if @confirmed
@user.save!
end

View file

@ -10,6 +10,7 @@ class Installation::OnboardingController < ApplicationController
user_full_name: onboarding_params.dig(:user, :name),
email: onboarding_params.dig(:user, :email),
user_password: params.dig(:user, :password),
super_admin: true,
confirmed: true
).perform
rescue StandardError => e

View file

@ -8,7 +8,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
def create
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
sign_in(@super_admin, scope: :super_admin)
sign_in(:super_admin, @super_admin)
flash.discard
redirect_to super_admin_users_path
end

View file

@ -1,44 +0,0 @@
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

@ -33,12 +33,15 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
# 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
def resource_params
permitted_params = super
permitted_params.delete(:password) if permitted_params[:password].blank?
permitted_params
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
def find_resource(param)
super.becomes(User)
end
end

View file

@ -1,75 +0,0 @@
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,
password: Field::Password,
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
].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
password
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { 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

@ -30,6 +30,7 @@ class UserDashboard < Administrate::BaseDashboard
created_at: Field::DateTime,
updated_at: Field::DateTime,
pubsub_token: Field::String,
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
accounts: CountField
}.freeze
@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard
name
email
accounts
type
].freeze
# SHOW_PAGE_ATTRIBUTES
@ -53,10 +55,12 @@ class UserDashboard < Administrate::BaseDashboard
avatar_url
unconfirmed_email
name
type
display_name
email
created_at
updated_at
confirmed_at
account_users
].freeze
@ -68,6 +72,8 @@ class UserDashboard < Administrate::BaseDashboard
display_name
email
password
confirmed_at
type
].freeze
# COLLECTION_FILTERS

View file

@ -12,7 +12,7 @@ module Avatarable
def avatar_url
return url_for(avatar.representation(resize: '250x250')) if avatar.attached? && avatar.representable?
if [User, Contact].include?(self.class) && email.present?
if [SuperAdmin, User, Contact].include?(self.class) && email.present?
hash = Digest::MD5.hexdigest(email)
return "https://www.gravatar.com/avatar/#{hash}?d=404"
end

View file

@ -1,25 +1,41 @@
# == Schema Information
#
# Table name: super_admins
# Table name: users
#
# 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
# id :integer not null, primary key
# availability :integer default("online")
# confirmation_sent_at :datetime
# confirmation_token :string
# confirmed_at :datetime
# current_sign_in_at :datetime
# current_sign_in_ip :string
# custom_attributes :jsonb
# display_name :string
# email :string
# encrypted_password :string default(""), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string
# name :string not null
# provider :string default("email"), not null
# pubsub_token :string
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# tokens :json
# type :string
# ui_settings :jsonb
# uid :string default(""), not null
# unconfirmed_email :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_super_admins_on_email (email) UNIQUE
# index_users_on_email (email)
# index_users_on_pubsub_token (pubsub_token) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_uid_and_provider (uid,provider) UNIQUE
#
class SuperAdmin < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :trackable, :rememberable, :validatable, :password_has_required_content
class SuperAdmin < User
end

View file

@ -23,6 +23,7 @@
# reset_password_token :string
# sign_in_count :integer default(0), not null
# tokens :json
# type :string
# ui_settings :jsonb
# uid :string default(""), not null
# unconfirmed_email :string

View file

@ -0,0 +1,26 @@
<%#
# BelongsTo Form Partial
This partial renders an input element for belongs_to relationships.
By default, the input is a collection select box
that displays all possible records to associate with.
## Local variables:
- `f`:
A Rails form generator, used to help create the appropriate input fields.
- `field`:
An instance of [Administrate::Field::BelongsTo][1].
Contains helper methods for displaying a collection select box.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/BelongsTo
%>
<div class="field-unit__label">
<%= f.label field.permitted_attribute %>
</div>
<div class="field-unit__field">
<%= f.select(field.permitted_attribute,
options_for_select(field.associated_resource_options, field.selected_option),
include_blank: field.include_blank_option) %>
</div>

View file

@ -0,0 +1,32 @@
<%#
# BelongsTo Index Partial
This partial renders a belongs_to relationship,
to be displayed on a resource's index page.
By default, the relationship is rendered as a link to the associated object.
## Local variables:
- `field`:
An instance of [Administrate::Field::BelongsTo][1].
A wrapper around the belongs_to relationship pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/BelongsTo
%>
<% if field.data %>
<% if field.data.is_a? User %>
<%= link_to(
field.display_associated_resource,
super_admin_user_path(field.data),
) %>
<% elsif valid_action?(:show, field.associated_class) %>
<%= link_to(
field.display_associated_resource,
[namespace, field.data],
) %>
<% else %>
<%= field.display_associated_resource %>
<% end %>
<% end %>

View file

@ -0,0 +1,27 @@
<%#
# BelongsTo Show Partial
This partial renders a belongs_to relationship,
to be displayed on a resource's show page.
By default, the relationship is rendered as a link to the associated object.
## Local variables:
- `field`:
An instance of [Administrate::Field::BelongsTo][1].
A wrapper around the belongs_to relationship pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/BelongsTo
%>
<% if field.data %>
<% if valid_action?(:show, field.associated_class) %>
<%= link_to(
field.display_associated_resource,
[namespace, field.data],
) %>
<% else %>
<%= field.display_associated_resource %>
<% end %>
<% end %>

View file

@ -0,0 +1,29 @@
<%#
# Polymorphic Form Partial
This partial renders an input element for polymorphic relationships.
## Local variables:
- `f`:
A Rails form generator, used to help create the appropriate input fields.
- `field`:
An instance of [Administrate::Field::Polymorphic][1].
A wrapper around the polymorphic belongs_to relationship
pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Polymorphic
%>
<%= f.fields_for field.attribute do |pf| %>
<div class="field-unit__label">
<%= pf.label :value, field.name.humanize %>
</div>
<div class="field-unit__field">
<%= pf.hidden_field(:type, value: field.class.name) %>
<%= pf.select(:value) do %>
<%= grouped_options_for_select(field.associated_resource_grouped_options, field.selected_global_id, prompt: true) %>
<% end %>
</div>
<% end %>

View file

@ -0,0 +1,31 @@
<%#
# Polymorphic Index Partial
This partial renders a polymorphic relationship,
to be displayed on a resource's index page.
By default, the relationship is rendered as a link to the associated object.
## Local variables:
- `field`:
An instance of [Administrate::Field::Polymorphic][1].
A wrapper around the polymorphic belongs_to relationship
pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Polymorphic
%>
<% if field.data %>
<% if field.data.is_a? User %>
<%= link_to(
"User ##{field.data.id}",
super_admin_user_path(field.data)
) %>
<% else %>
<%= link_to(
field.display_associated_resource,
[namespace, field.data]
) %>
<% end %>
<% end %>

View file

@ -0,0 +1,28 @@
<%#
# Polymorphic Show Partial
This partial renders a polymorphic relationship,
to be displayed on a resource's show page.
By default, the relationship is rendered as a link to the associated object.
## Local variables:
- `field`:
An instance of [Administrate::Field::Polymorphic][1].
A wrapper around the polymorphic belongs_to relationship
pulled from the database.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Polymorphic
%>
<% if field.data %>
<% if valid_action?(:show, field.data.class) %>
<%= link_to(
field.display_associated_resource,
[namespace, field.data],
) %>
<% else %>
<%= field.display_associated_resource %>
<% end %>
<% end %>

View file

@ -56,9 +56,14 @@ as defined by the routes in the `admin/` namespace
</li>
</ul>
<ul class="logout">
<li class="navigation__link">
<i class="ion ion-log-out"></i>
<%= link_to "Logout", super_admin_logout_url %>
</li>
<li class="navigation__link">
<i class="ion ion-android-contacts"></i>
<%= link_to "Agent Dashboard", '/' %>
</li>
</ul>
</div>

View file

@ -57,13 +57,13 @@ to display a collection of resources in an HTML table.
<tr class="js-table-row"
tabindex="0"
<% if valid_action? :show, collection_presenter.resource_name %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource])}) %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource.becomes(User)])}) %>
<% end %>
>
<% collection_presenter.attributes_for(resource).each do |attribute| %>
<td class="cell-data cell-data--<%= attribute.html_class %>">
<% if show_action? :show, resource -%>
<a href="<%= polymorphic_path([namespace, resource]) -%>"
<a href="<%= polymorphic_path([namespace, resource.becomes(User)]) -%>"
class="action-show"
>
<%= render_field attribute %>
@ -75,7 +75,7 @@ to display a collection of resources in an HTML table.
<% if valid_action? :edit, collection_presenter.resource_name %>
<td><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource],
[:edit, namespace, resource.becomes(User)],
class: "action-edit",
) if show_action? :edit, resource%></td>
<% end %>
@ -83,7 +83,7 @@ to display a collection of resources in an HTML table.
<% if valid_action? :destroy, collection_presenter.resource_name %>
<td><%= link_to(
t("administrate.actions.destroy"),
[namespace, resource],
[namespace, resource.becomes(User)],
class: "text-color-red",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }

View file

@ -72,6 +72,15 @@ class Rack::Attack
end
end
throttle('super_admin_login/email', limit: 20, period: 5.minutes) do |req|
if req.path == '/super_admin/sign_in' && req.post?
# NOTE: This line used to throw ArgumentError /rails/action_mailbox/sendgrid/inbound_emails : invalid byte sequence in UTF-8
# Hence placed in the if block
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
email.to_s.downcase.gsub(/\s+/, '')
end
end
throttle('reset_password/email', limit: 5, period: 1.hour) do |req|
if req.path == '/auth/password' && req.post?
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence

View file

@ -303,7 +303,6 @@ Rails.application.routes.draw do
# order of resources affect the order of sidebar navigation in super admin
resources :accounts
resources :users, only: [:index, :new, :create, :show, :edit, :update]
resources :super_admins
resources :access_tokens, only: [:index, :show]
resources :installation_configs, only: [:index, :new, :create, :show, :edit, :update]

View file

@ -0,0 +1,42 @@
class AddTypeToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :type, :string
migrate_existing_super_admins
drop_table :super_admins do |t|
t.string :email, null: false, default: ''
t.string :encrypted_password, null: false, default: ''
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.timestamps null: false
end
end
private
def old_super_admins
ActiveRecord::Base.connection.execute('SELECT * from super_admins').to_a
end
def create_user_account_for_super_admin(super_admin)
u = User.new(email: super_admin['email'], name: "SuperUser #{super_admin['id']}", encrypted_password: super_admin['encrypted_password'],
confirmed_at: DateTime.now, type: 'SuperAdmin')
u.save(validate: false)
end
def migrate_existing_super_admins
old_super_admins.each do |super_admin|
user = User.find_by(email: super_admin['email'])
if user.present?
user.update(type: 'SuperAdmin')
else
Rails.logger.debug { "User with email #{super_admin['email']} not found" }
create_user_account_for_super_admin(super_admin)
end
end
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_01_11_223630) do
ActiveRecord::Schema.define(version: 2022_01_21_055444) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@ -745,6 +745,7 @@ ActiveRecord::Schema.define(version: 2022_01_11_223630) do
t.integer "availability", default: 0
t.jsonb "ui_settings", default: {}
t.jsonb "custom_attributes", default: {}
t.string "type"
t.index ["email"], name: "index_users_on_email"
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

View file

@ -10,7 +10,6 @@ end
## Seeds for Local Development
unless Rails.env.production?
SuperAdmin.create!(email: 'john@acme.inc', password: 'Password1!')
account = Account.create!(
name: 'Acme Inc'
@ -20,7 +19,7 @@ unless Rails.env.production?
name: 'Acme Org'
)
user = User.new(name: 'John', email: 'john@acme.inc', password: 'Password1!')
user = User.new(name: 'John', email: 'john@acme.inc', password: 'Password1!', type: 'SuperAdmin')
user.skip_confirmation!
user.save!

View file

@ -14,7 +14,7 @@ RSpec.describe 'Super Admin access tokens API', type: :request do
context 'when it is an authenticated super admin' do
it 'shows the list of access tokens' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/access_tokens'
expect(response).to have_http_status(:success)
expect(response.body).to include(platform_app.access_token.token)

View file

@ -13,7 +13,7 @@ RSpec.describe 'Super Admin Account Users API', type: :request do
context 'when it is an authenticated super admin' do
it 'shows the account user create page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/account_users/new'
expect(response).to have_http_status(:success)
end

View file

@ -15,7 +15,7 @@ RSpec.describe 'Super Admin accounts API', type: :request do
let!(:account) { create(:account) }
it 'shows the list of accounts' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/accounts'
expect(response).to have_http_status(:success)
expect(response.body).to include('New account')

View file

@ -15,7 +15,7 @@ RSpec.describe 'Super Admin agent-bots API', type: :request do
let!(:agent_bot) { create(:agent_bot) }
it 'shows the list of users' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/agent_bots'
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.name)

View file

@ -15,7 +15,7 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
let!(:config) { create(:installation_config, { name: 'FB_APP_ID', value: 'TESTVALUE' }) }
it 'shows the app_config page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/app_config'
expect(response).to have_http_status(:success)
expect(response.body).to include(config.name)
@ -33,7 +33,7 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
context 'when it is an aunthenticated super admin' do
it 'shows the app_config page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
post '/super_admin/app_config', params: { app_config: { TESTKEY: 'TESTVALUE' } }
expect(response.status).to eq(302)

View file

@ -19,20 +19,20 @@ RSpec.describe 'Super Admin Installation Config API', type: :request do
end
it 'shows the installation_configs create page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/installation_configs/new'
expect(response).to have_http_status(:success)
end
it 'shows the installation_configs edit page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
editable_config = InstallationConfig.editable.first
get "/super_admin/installation_configs/#{editable_config.id}/edit"
expect(response).to have_http_status(:success)
end
it 'shows the installation_configs list page' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/installation_configs'
expect(response).to have_http_status(:success)
expect(response.body).to include(config.name)

View file

@ -15,7 +15,7 @@ RSpec.describe 'Super Admin platform app API', type: :request do
let!(:platform_app) { create(:platform_app) }
it 'shows the list of users' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/platform_apps'
expect(response).to have_http_status(:success)
expect(response.body).to include(platform_app.name)

View file

@ -1,24 +0,0 @@
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

@ -15,7 +15,7 @@ RSpec.describe 'Super Admin Users API', type: :request do
let!(:user) { create(:user) }
it 'shows the list of users' do
sign_in super_admin
sign_in(super_admin, scope: :super_admin)
get '/super_admin/users'
expect(response).to have_http_status(:success)
expect(response.body).to include('New user')

View file

@ -1,46 +0,0 @@
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('Dashboard')
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

@ -1,6 +1,9 @@
FactoryBot.define do
factory :super_admin do
name { Faker::Name.name }
email { "admin@#{SecureRandom.uuid}.com" }
password { 'Password1!' }
type { 'SuperAdmin' }
confirmed_at { Time.zone.now }
end
end