refinerycms-authentication 0.9.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/app/controllers/admin/users_controller.rb +90 -0
- data/app/controllers/passwords_controller.rb +43 -0
- data/app/controllers/registrations_controller.rb +67 -0
- data/app/controllers/sessions_controller.rb +23 -0
- data/app/helpers/sessions_helper.rb +2 -0
- data/app/helpers/users_helper.rb +2 -0
- data/app/mailers/user_mailer.rb +20 -0
- data/app/models/role.rb +16 -0
- data/app/models/roles_users.rb +6 -0
- data/app/models/user.rb +60 -0
- data/app/models/user_plugin.rb +5 -0
- data/app/views/admin/users/_form.html.erb +92 -0
- data/app/views/admin/users/_user.html.erb +19 -0
- data/app/views/admin/users/_users.html.erb +4 -0
- data/app/views/admin/users/edit.html.erb +1 -0
- data/app/views/admin/users/index.html.erb +12 -0
- data/app/views/admin/users/new.html.erb +1 -0
- data/app/views/layouts/login.html.erb +21 -0
- data/app/views/passwords/edit.html.erb +31 -0
- data/app/views/passwords/new.html.erb +18 -0
- data/app/views/registrations/new.html.erb +41 -0
- data/app/views/sessions/new.html.erb +29 -0
- data/app/views/user_mailer/reset_notification.html.erb +12 -0
- data/app/views/user_mailer/reset_notification.text.plain.erb +7 -0
- data/config/locales/cs.yml +75 -0
- data/config/locales/da.yml +72 -0
- data/config/locales/de.yml +72 -0
- data/config/locales/el.yml +72 -0
- data/config/locales/en.yml +72 -0
- data/config/locales/es.yml +100 -0
- data/config/locales/fr.yml +72 -0
- data/config/locales/it.yml +97 -0
- data/config/locales/lolcat.yml +55 -0
- data/config/locales/lt.yml +55 -0
- data/config/locales/lv.yml +72 -0
- data/config/locales/nb.yml +72 -0
- data/config/locales/nl.yml +70 -0
- data/config/locales/pl.yml +100 -0
- data/config/locales/pt-BR.yml +68 -0
- data/config/locales/rs.yml +72 -0
- data/config/locales/ru.yml +97 -0
- data/config/locales/sl.yml +61 -0
- data/config/locales/sv.yml +64 -0
- data/config/locales/vi.yml +72 -0
- data/config/locales/zh-CN.yml +72 -0
- data/config/locales/zh-TW.yml +72 -0
- data/config/routes.rb +31 -0
- data/db/migrate/20100913234705_create_refinerycms_authentication_schema.rb +43 -0
- data/db/migrate/20100929035252_add_missing_indexes_to_roles_users.rb +11 -0
- data/db/migrate/20101206013505_change_to_devise_users_table.rb +27 -0
- data/db/migrate/20110106184757_add_remember_created_at_to_users.rb +9 -0
- data/features/lost_password.feature +49 -0
- data/features/manage_users.feature +61 -0
- data/features/step_definitions/lost_password.rb +8 -0
- data/features/step_definitions/user_steps.rb +36 -0
- data/features/support/factories.rb +18 -0
- data/features/support/paths.rb +24 -0
- data/lib/authenticated_system.rb +29 -0
- data/lib/gemspec.rb +34 -0
- data/lib/generators/refinerycms_authentication_generator.rb +8 -0
- data/lib/refinerycms-authentication.rb +47 -0
- data/license.md +21 -0
- data/readme.md +17 -0
- data/refinerycms-authentication.gemspec +112 -0
- data/spec/models/user_spec.rb +159 -0
- metadata +144 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
class CreateRefinerycmsAuthenticationSchema < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
# Postgres apparently requires the roles_users table to exist before creating the roles table.
|
4
|
+
create_table ::RolesUsers.table_name, :id => false, :force => true do |t|
|
5
|
+
t.integer "user_id"
|
6
|
+
t.integer "role_id"
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table ::Role.table_name, :force => true do |t|
|
10
|
+
t.string "title"
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table ::UserPlugin.table_name, :force => true do |t|
|
14
|
+
t.integer "user_id"
|
15
|
+
t.string "name"
|
16
|
+
t.integer "position"
|
17
|
+
end
|
18
|
+
|
19
|
+
add_index ::UserPlugin.table_name, ["name"], :name => "index_#{::UserPlugin.table_name}_on_title"
|
20
|
+
add_index ::UserPlugin.table_name, ["user_id", "name"], :name => "index_unique_#{::UserPlugin.table_name}", :unique => true
|
21
|
+
|
22
|
+
create_table ::User.table_name, :force => true do |t|
|
23
|
+
t.string "login", :null => false
|
24
|
+
t.string "email", :null => false
|
25
|
+
t.string "crypted_password", :null => false
|
26
|
+
t.string "password_salt", :null => false
|
27
|
+
t.string "persistence_token"
|
28
|
+
t.datetime "created_at"
|
29
|
+
t.datetime "updated_at"
|
30
|
+
t.string "perishable_token"
|
31
|
+
end
|
32
|
+
|
33
|
+
add_index ::User.table_name, ["id"], :name => "index_#{::User.table_name}_on_id"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.down
|
37
|
+
[::User].reject{|m|
|
38
|
+
!(defined?(m) and m.respond_to?(:table_name))
|
39
|
+
}.each do |model|
|
40
|
+
drop_table model.table_name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AddMissingIndexesToRolesUsers < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_index ::RolesUsers.table_name, [:role_id, :user_id]
|
4
|
+
add_index ::RolesUsers.table_name, [:user_id, :role_id]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
remove_index ::RolesUsers.table_name, :column => [:role_id, :user_id]
|
9
|
+
remove_index ::RolesUsers.table_name, :column => [:user_id, :role_id]
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ChangeToDeviseUsersTable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column ::User.table_name, :current_sign_in_at, :datetime
|
4
|
+
add_column ::User.table_name, :last_sign_in_at, :datetime
|
5
|
+
add_column ::User.table_name, :current_sign_in_ip, :string
|
6
|
+
add_column ::User.table_name, :last_sign_in_ip, :string
|
7
|
+
add_column ::User.table_name, :sign_in_count, :integer
|
8
|
+
add_column ::User.table_name, :remember_token, :string
|
9
|
+
add_column ::User.table_name, :reset_password_token, :string
|
10
|
+
|
11
|
+
rename_column ::User.table_name, :crypted_password, :encrypted_password
|
12
|
+
rename_column ::User.table_name, :login, :username
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
remove_column ::User.table_name, :current_sign_in_at
|
17
|
+
remove_column ::User.table_name, :last_sign_in_at
|
18
|
+
remove_column ::User.table_name, :current_sign_in_ip
|
19
|
+
remove_column ::User.table_name, :last_sign_in_ip
|
20
|
+
remove_column ::User.table_name, :sign_in_count
|
21
|
+
remove_column ::User.table_name, :remember_token
|
22
|
+
remove_column ::User.table_name, :reset_password_token
|
23
|
+
|
24
|
+
rename_column ::User.table_name, :encrypted_password, :crypted_password
|
25
|
+
rename_column ::User.table_name, :username, :login
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
@refinerycms @users @users-password
|
2
|
+
Feature: Lost Password
|
3
|
+
In order to restore my password
|
4
|
+
As a lost soul
|
5
|
+
I want to reset my password
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given A Refinery user exists
|
9
|
+
|
10
|
+
@users-password-forgot
|
11
|
+
Scenario: Forgot Password page (no email entered)
|
12
|
+
And I am on the forgot password page
|
13
|
+
When I press "Reset password"
|
14
|
+
Then I should see "You did not enter an email address."
|
15
|
+
|
16
|
+
@users-password-forgot
|
17
|
+
Scenario: Forgot Password page (non existing email entered)
|
18
|
+
Given I am on the forgot password page
|
19
|
+
And I have a user with email "green@cukes.com"
|
20
|
+
When I fill in "user_email" with "none@cukes.com"
|
21
|
+
And I press "Reset password"
|
22
|
+
Then I should see "Sorry, 'none@cukes.com' isn't associated with any accounts."
|
23
|
+
And I should see "Are you sure you typed the correct email address?"
|
24
|
+
|
25
|
+
@users-password-forgot
|
26
|
+
Scenario: Forgot Password page (existing email entered)
|
27
|
+
Given I am on the forgot password page
|
28
|
+
And I have a user with email "green@cukes.com"
|
29
|
+
When I fill in "user_email" with "green@cukes.com"
|
30
|
+
And I press "Reset password"
|
31
|
+
Then I should see "An email has been sent to you with a link to reset your password."
|
32
|
+
|
33
|
+
@users-password-reset
|
34
|
+
Scenario: Reset password page (invalid reset_code)
|
35
|
+
Given I am not requesting password reset
|
36
|
+
When I go to the reset password page
|
37
|
+
Then I should be on the forgot password page
|
38
|
+
And I should see "We're sorry, but this reset code has expired or is invalid."
|
39
|
+
And I should see "If you are having issues try copying and pasting the URL from your email into your browser or restarting the reset password process."
|
40
|
+
|
41
|
+
@users-password-reset
|
42
|
+
Scenario: Reset password page (valid reset_code)
|
43
|
+
Given I am requesting password reset
|
44
|
+
When I go to the reset password page
|
45
|
+
And I fill in "Password" with "cukes"
|
46
|
+
And I fill in "Password confirmation" with "cukes"
|
47
|
+
And I press "Reset password"
|
48
|
+
Then I should be on the admin root
|
49
|
+
And I should see "Password reset successfully for"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
@refinerycms @users @users-manage
|
2
|
+
Feature: Manage Users
|
3
|
+
In order to control who can access my website's backend
|
4
|
+
As an administrator
|
5
|
+
I want to create and manage users
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given I have no users
|
9
|
+
|
10
|
+
Scenario: When there are no users, you are invited to create a user
|
11
|
+
When I go to the home page
|
12
|
+
Then I should see "There are no users yet, so we'll set you up first."
|
13
|
+
|
14
|
+
@users-add @add
|
15
|
+
Scenario: When there are no users, you can create a user
|
16
|
+
When I go to the home page
|
17
|
+
And I follow "Continue..."
|
18
|
+
And I should see "Fill out your details below so that we can get you started."
|
19
|
+
And I fill in "Username" with "cucumber"
|
20
|
+
And I fill in "Email" with "green@cucumber.com"
|
21
|
+
And I fill in "Password" with "greenandjuicy"
|
22
|
+
And I fill in "Password confirmation" with "greenandjuicy"
|
23
|
+
And I press "Sign up"
|
24
|
+
Then I should see "Welcome to Refinery, cucumber."
|
25
|
+
And I should see "Latest Activity"
|
26
|
+
And I should have 1 user
|
27
|
+
|
28
|
+
@users-list @list
|
29
|
+
Scenario: User List
|
30
|
+
Given I have a user named "steven"
|
31
|
+
And I am a logged in refinery user
|
32
|
+
When I go to the list of users
|
33
|
+
Then I should see "steven"
|
34
|
+
|
35
|
+
@users-add @add
|
36
|
+
Scenario: Create User
|
37
|
+
Given I have a user named "steven"
|
38
|
+
And I am a logged in refinery user
|
39
|
+
When I go to the list of users
|
40
|
+
And I follow "Add new user"
|
41
|
+
And I fill in "Username" with "cucumber"
|
42
|
+
And I fill in "Email" with "green@cucumber.com"
|
43
|
+
And I fill in "Password" with "greenandjuicy"
|
44
|
+
And I fill in "Password confirmation" with "greenandjuicy"
|
45
|
+
And I press "Save"
|
46
|
+
Then I should be on the list of users
|
47
|
+
And I should see "cucumber was successfully added."
|
48
|
+
And I should see "cucumber (green@cucumber.com)"
|
49
|
+
|
50
|
+
@users-edit @edit
|
51
|
+
Scenario: Edit User
|
52
|
+
Given I have a user named "steven"
|
53
|
+
And I am a logged in refinery user
|
54
|
+
When I go to the list of users
|
55
|
+
And I follow "Edit this user"
|
56
|
+
And I fill in "Username" with "cucumber"
|
57
|
+
And I fill in "Email" with "green@cucumber.com"
|
58
|
+
And I press "Save"
|
59
|
+
Then I should be on the list of users
|
60
|
+
And I should see "cucumber was successfully updated."
|
61
|
+
And I should see "cucumber (green@cucumber.com)"
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Given /^I have a user with email "(.*)"$/ do |email|
|
2
|
+
Factory(:refinery_user, :email => email)
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^I am (not )?requesting password reset$/ do |action|
|
6
|
+
@user = Factory(:refinery_user, :updated_at => 11.minutes.ago)
|
7
|
+
@user.send(:generate_reset_password_token!) if action.nil?
|
8
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
def login
|
2
|
+
visit new_user_session_path
|
3
|
+
fill_in("user_login", :with => @user.email)
|
4
|
+
fill_in("user_password", :with => 'greenandjuicy')
|
5
|
+
click_button("submit_button")
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /^I am a logged in refinery user$/ do
|
9
|
+
@user ||= Factory(:refinery_user)
|
10
|
+
login
|
11
|
+
end
|
12
|
+
|
13
|
+
Given /^I am a logged in customer$/ do
|
14
|
+
@user ||= Factory(:user)
|
15
|
+
login
|
16
|
+
end
|
17
|
+
|
18
|
+
Given /^A Refinery user exists$/ do
|
19
|
+
@refinery_user ||= Factory(:refinery_user)
|
20
|
+
end
|
21
|
+
|
22
|
+
Given /^I have a user named "(.*)"$/ do |name|
|
23
|
+
Factory(:user, :username => name)
|
24
|
+
end
|
25
|
+
|
26
|
+
Given /^I have a R|refinery user named "(.*)"$/ do |name|
|
27
|
+
Factory(:refinery_user, :username => name)
|
28
|
+
end
|
29
|
+
|
30
|
+
Given /^I have no users$/ do
|
31
|
+
User.delete_all
|
32
|
+
end
|
33
|
+
|
34
|
+
Then /^I should have ([0-9]+) users?$/ do |count|
|
35
|
+
User.count.should == count.to_i
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
|
3
|
+
Factory.define :user do |u|
|
4
|
+
u.sequence(:username) { |n| "person#{n}" }
|
5
|
+
u.sequence(:email) { |n| "person#{n}@cucumber.com" }
|
6
|
+
u.password "greenandjuicy"
|
7
|
+
u.password_confirmation "greenandjuicy"
|
8
|
+
end
|
9
|
+
|
10
|
+
Factory.define :refinery_user, :parent => :user do |u|
|
11
|
+
u.roles { [ Role[:refinery] ] }
|
12
|
+
|
13
|
+
u.after_create do |user|
|
14
|
+
Refinery::Plugins.registered.each_with_index do |plugin, index|
|
15
|
+
user.plugins.create(:name => plugin.name, :position => index)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module NavigationHelpers
|
2
|
+
module Refinery
|
3
|
+
module Authentication
|
4
|
+
def path_to(page_name)
|
5
|
+
case page_name
|
6
|
+
|
7
|
+
when /the list of users/
|
8
|
+
admin_users_path
|
9
|
+
|
10
|
+
when /the login page/
|
11
|
+
new_user_session_path
|
12
|
+
|
13
|
+
when /the forgot password page/
|
14
|
+
new_user_password_path
|
15
|
+
|
16
|
+
when /the reset password page/
|
17
|
+
edit_user_password_path(:reset_password_token => @user.reset_password_token)
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AuthenticatedSystem
|
2
|
+
protected
|
3
|
+
# Store the URI of the current request in the session.
|
4
|
+
#
|
5
|
+
# We can return to this location by calling #redirect_back_or_default.
|
6
|
+
def store_location
|
7
|
+
session[:return_to] = request.fullpath
|
8
|
+
end
|
9
|
+
|
10
|
+
# Redirect to the URI stored by the most recent store_location call or
|
11
|
+
# to the passed default.
|
12
|
+
def redirect_back_or_default(default)
|
13
|
+
redirect_to(session[:return_to] || default)
|
14
|
+
session[:return_to] = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
#def current_user
|
18
|
+
#current_user
|
19
|
+
#end
|
20
|
+
|
21
|
+
def refinery_user?
|
22
|
+
user_signed_in? && current_user.has_role?(:refinery)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
base.send :helper_method, :current_user, :current_user_session, :user_signed_in?, :refinery_user? if base.respond_to? :helper_method
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/gemspec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
gempath = Pathname.new(File.expand_path('../../', __FILE__))
|
3
|
+
require gempath.join('..', 'base', 'lib', 'base', 'refinery')
|
4
|
+
|
5
|
+
gemspec = <<EOF
|
6
|
+
# DO NOT EDIT THIS FILE DIRECTLY! Instead, use lib/gemspec.rb to generate it.
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = %q{#{gemname = 'refinerycms-authentication'}}
|
10
|
+
s.version = %q{#{::Refinery.version}}
|
11
|
+
s.summary = %q{Authentication engine for Refinery CMS}
|
12
|
+
s.description = %q{The default authentication engine for Refinery CMS}
|
13
|
+
s.date = %q{#{Time.now.strftime('%Y-%m-%d')}}
|
14
|
+
s.email = %q{info@refinerycms.com}
|
15
|
+
s.homepage = %q{http://refinerycms.com}
|
16
|
+
s.rubyforge_project = %q{refinerycms}
|
17
|
+
s.authors = ['Resolve Digital', 'Philip Arndt', 'David Jones', 'Steven Heidel']
|
18
|
+
s.license = %q{MIT}
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
s.executables = %w(#{Pathname.glob(gempath.join('bin/*')).map{|d| d.relative_path_from(gempath)}.sort.join(" ")})
|
21
|
+
|
22
|
+
s.add_dependency 'refinerycms-core', '~> #{::Refinery::Version}'
|
23
|
+
s.add_dependency 'devise', '~> 1.1'
|
24
|
+
|
25
|
+
s.files = [
|
26
|
+
'#{%w( **/{*,.rspec,.gitignore,.yardopts} ).map { |file| Pathname.glob(gempath.join(file)) }.flatten.reject{|f|
|
27
|
+
!f.exist? or f.to_s =~ /\.gem$/ or (f.directory? and f.children.empty?)
|
28
|
+
}.map{|d| d.relative_path_from(gempath)}.uniq.sort.join("',\n '")}'
|
29
|
+
]
|
30
|
+
end
|
31
|
+
EOF
|
32
|
+
|
33
|
+
(gemfile = gempath.join("#{gemname}.gemspec")).open('w') {|f| f.puts(gemspec)}
|
34
|
+
puts `cd #{gempath} && gem build #{gemfile}` if ARGV.any?{|a| a == "BUILD=true"}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'devise'
|
2
|
+
require 'refinerycms-core'
|
3
|
+
# Attach authenticated system methods to the ::Refinery::ApplicationController
|
4
|
+
require File.expand_path('../authenticated_system', __FILE__)
|
5
|
+
[::Refinery::ApplicationController, ::Refinery::ApplicationHelper].each do |c|
|
6
|
+
c.class_eval {
|
7
|
+
include AuthenticatedSystem
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
module Refinery
|
12
|
+
module Authentication
|
13
|
+
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
config.autoload_paths += %W( #{config.root}/lib )
|
16
|
+
|
17
|
+
config.after_initialize do
|
18
|
+
::Refinery::Plugin.register do |plugin|
|
19
|
+
plugin.name = "refinery_users"
|
20
|
+
plugin.version = %q{0.9.9}
|
21
|
+
plugin.menu_match = /(refinery|admin)\/users$/
|
22
|
+
plugin.activity = {
|
23
|
+
:class => User,
|
24
|
+
:title => 'login'
|
25
|
+
}
|
26
|
+
plugin.url = {:controller => "/admin/users"}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :root
|
33
|
+
def root
|
34
|
+
@root ||= Pathname.new(File.expand_path('../../', __FILE__))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_accessor :authentication_login_field
|
41
|
+
def authentication_login_field
|
42
|
+
@authentication_login_field ||= 'login'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
::Refinery.engines << 'authentication'
|
data/license.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2005-2010 [Resolve Digital](http://www.resolvedigital.com)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|