authkit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/FEATURES.md +73 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +168 -0
- data/Rakefile +60 -0
- data/authkit.gemspec +27 -0
- data/config/database.yml.example +19 -0
- data/lib/authkit.rb +5 -0
- data/lib/authkit/engine.rb +7 -0
- data/lib/authkit/version.rb +3 -0
- data/lib/generators/authkit/USAGE +18 -0
- data/lib/generators/authkit/install_generator.rb +113 -0
- data/lib/generators/authkit/templates/app/controllers/application_controller.rb +94 -0
- data/lib/generators/authkit/templates/app/controllers/email_confirmation_controller.rb +25 -0
- data/lib/generators/authkit/templates/app/controllers/password_change_controller.rb +29 -0
- data/lib/generators/authkit/templates/app/controllers/password_reset_controller.rb +29 -0
- data/lib/generators/authkit/templates/app/controllers/sessions_controller.rb +35 -0
- data/lib/generators/authkit/templates/app/controllers/users_controller.rb +89 -0
- data/lib/generators/authkit/templates/app/models/user.rb +170 -0
- data/lib/generators/authkit/templates/app/views/password_change/show.html.erb +16 -0
- data/lib/generators/authkit/templates/app/views/password_reset/show.html.erb +12 -0
- data/lib/generators/authkit/templates/app/views/sessions/new.html.erb +13 -0
- data/lib/generators/authkit/templates/app/views/users/edit.html.erb +58 -0
- data/lib/generators/authkit/templates/app/views/users/new.html.erb +58 -0
- data/lib/generators/authkit/templates/db/migrate/add_authkit_fields_to_users.rb +110 -0
- data/lib/generators/authkit/templates/db/migrate/create_users.rb +17 -0
- data/lib/generators/authkit/templates/lib/email_format_validator.rb +11 -0
- data/lib/generators/authkit/templates/spec/controllers/application_controller_spec.rb +188 -0
- data/lib/generators/authkit/templates/spec/controllers/email_confirmation_controller_spec.rb +80 -0
- data/lib/generators/authkit/templates/spec/controllers/password_change_controller_spec.rb +98 -0
- data/lib/generators/authkit/templates/spec/controllers/password_reset_controller_spec.rb +87 -0
- data/lib/generators/authkit/templates/spec/controllers/sessions_controller_spec.rb +111 -0
- data/lib/generators/authkit/templates/spec/controllers/users_controller_spec.rb +195 -0
- data/lib/generators/authkit/templates/spec/models/user_spec.rb +268 -0
- data/spec/spec_helper.rb +16 -0
- metadata +165 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
<h1>Please enter a new password</h1>
|
2
|
+
|
3
|
+
<%= form_tag(password_change_path, method: "post") do %>
|
4
|
+
<%= hidden_field_tag :token, params[:token] %>
|
5
|
+
|
6
|
+
<div class="field">
|
7
|
+
<%= label_tag "password" %>
|
8
|
+
<%= password_field_tag "password" %>
|
9
|
+
</div>
|
10
|
+
<div class="field">
|
11
|
+
<%= label_tag "password_confirmation" %>
|
12
|
+
<%= password_field_tag "password_confirmation" %>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<%= submit_tag "Change" %>
|
16
|
+
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<h1>Reset your password</h1>
|
2
|
+
|
3
|
+
<%= form_tag(password_reset_path, method: "post") do %>
|
4
|
+
<%= hidden_field_tag :token, params[:token] %>
|
5
|
+
|
6
|
+
<div class="field">
|
7
|
+
<%= label_tag "email", "Email or username" %>
|
8
|
+
<%= text_field_tag "email" %>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<%= submit_tag "Reset" %>
|
12
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<h1>Sign In</h1>
|
2
|
+
|
3
|
+
<%= form_tag(login_path, method: "post") do %>
|
4
|
+
<div class="field">
|
5
|
+
<%= label_tag "email", "Email or username" %>
|
6
|
+
<%= text_field_tag "email" %>
|
7
|
+
</div>
|
8
|
+
<div class="field">
|
9
|
+
<%= label_tag "password" %>
|
10
|
+
<%= password_field_tag "password" %>
|
11
|
+
</div>
|
12
|
+
<%= submit_tag "Login" %>
|
13
|
+
<% end %>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<h1>Account</h1>
|
2
|
+
|
3
|
+
<% if @user.errors.any? %>
|
4
|
+
<div id="error_explanation">
|
5
|
+
<div class="alert alert-error">
|
6
|
+
The form contains <%= pluralize(@user.errors.count, "error") %>.
|
7
|
+
</div>
|
8
|
+
<ul>
|
9
|
+
<% @user.errors.full_messages.each do |msg| %>
|
10
|
+
<li>* <%= msg %></li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
13
|
+
</div>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<%= form_for @user do |f| %>
|
17
|
+
<div class="field">
|
18
|
+
<%= f.label "first_name" %>
|
19
|
+
<%= f.text_field "first_name" %>
|
20
|
+
</div>
|
21
|
+
<div class="field">
|
22
|
+
<%= f.label "last_name" %>
|
23
|
+
<%= f.text_field "last_name" %>
|
24
|
+
</div>
|
25
|
+
<div class="field">
|
26
|
+
<%= f.label "bio" %>
|
27
|
+
<%= f.text_area "bio" %>
|
28
|
+
</div>
|
29
|
+
<div class="field">
|
30
|
+
<%= f.label "website" %>
|
31
|
+
<%= f.text_field "website" %>
|
32
|
+
</div>
|
33
|
+
<div class="field">
|
34
|
+
<%= f.label "phone_number" %>
|
35
|
+
<%= f.text_field "phone_number" %>
|
36
|
+
</div>
|
37
|
+
<div class="field">
|
38
|
+
<%= f.label "phone_number" %>
|
39
|
+
<%= f.time_zone_select('time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)") %>
|
40
|
+
</div>
|
41
|
+
<div class="field">
|
42
|
+
<%= f.label "email" %>
|
43
|
+
<%= f.text_field "confirmation_email" %>
|
44
|
+
</div>
|
45
|
+
<div class="field">
|
46
|
+
<%= f.label "username" %>
|
47
|
+
<%= f.text_field "username" %>
|
48
|
+
</div>
|
49
|
+
<div class="field">
|
50
|
+
<%= f.label "password" %>
|
51
|
+
<%= f.password_field "password" %>
|
52
|
+
</div>
|
53
|
+
<div class="field">
|
54
|
+
<%= f.label "password_confirmation" %>
|
55
|
+
<%= f.password_field "password_confirmation" %>
|
56
|
+
</div>
|
57
|
+
<%= f.submit "Save" %>
|
58
|
+
<% end %>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<h1>Sign Up</h1>
|
2
|
+
|
3
|
+
<% if @user.errors.any? %>
|
4
|
+
<div id="error_explanation">
|
5
|
+
<div class="alert alert-error">
|
6
|
+
The form contains <%= pluralize(@user.errors.count, "error") %>.
|
7
|
+
</div>
|
8
|
+
<ul>
|
9
|
+
<% @user.errors.full_messages.each do |msg| %>
|
10
|
+
<li>* <%= msg %></li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
13
|
+
</div>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<%= form_for @user do |f| %>
|
17
|
+
<div class="field">
|
18
|
+
<%= f.label "first_name" %>
|
19
|
+
<%= f.text_field "first_name" %>
|
20
|
+
</div>
|
21
|
+
<div class="field">
|
22
|
+
<%= f.label "last_name" %>
|
23
|
+
<%= f.text_field "last_name" %>
|
24
|
+
</div>
|
25
|
+
<div class="field">
|
26
|
+
<%= f.label "bio" %>
|
27
|
+
<%= f.text_area "bio" %>
|
28
|
+
</div>
|
29
|
+
<div class="field">
|
30
|
+
<%= f.label "website" %>
|
31
|
+
<%= f.text_field "website" %>
|
32
|
+
</div>
|
33
|
+
<div class="field">
|
34
|
+
<%= f.label "phone_number" %>
|
35
|
+
<%= f.text_field "phone_number" %>
|
36
|
+
</div>
|
37
|
+
<div class="field">
|
38
|
+
<%= f.label "phone_number" %>
|
39
|
+
<%= f.time_zone_select('time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)") %>
|
40
|
+
</div>
|
41
|
+
<div class="field">
|
42
|
+
<%= f.label "email" %>
|
43
|
+
<%= f.text_field "email" %>
|
44
|
+
</div>
|
45
|
+
<div class="field">
|
46
|
+
<%= f.label "username" %>
|
47
|
+
<%= f.text_field "username" %>
|
48
|
+
</div>
|
49
|
+
<div class="field">
|
50
|
+
<%= f.label "password" %>
|
51
|
+
<%= f.text_field "password" %>
|
52
|
+
</div>
|
53
|
+
<div class="field">
|
54
|
+
<%= f.label "password_confirmation" %>
|
55
|
+
<%= f.text_field "password_confirmation" %>
|
56
|
+
</div>
|
57
|
+
<%= f.submit "Sign up" %>
|
58
|
+
<% end %>
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Generated by Authkit.
|
2
|
+
#
|
3
|
+
# Add fields to the users table for managing authentication.
|
4
|
+
#
|
5
|
+
class AddAuthkitFieldsToUsers < ActiveRecord::Migration
|
6
|
+
def self.up
|
7
|
+
add_column :users, :email, :string, :default => "", :null => false
|
8
|
+
add_column :users, :password_digest, :string, :default => "", :null => false
|
9
|
+
add_column :users, :username, :string, :limit => 64
|
10
|
+
|
11
|
+
add_column :users, :time_zone, :string, :default => "Eastern Time (US & Canada)"
|
12
|
+
add_column :users, :first_name, :string
|
13
|
+
add_column :users, :last_name, :string
|
14
|
+
add_column :users, :bio, :text
|
15
|
+
add_column :users, :website, :string
|
16
|
+
add_column :users, :phone_number, :string
|
17
|
+
|
18
|
+
# One time password key for two-factor auth
|
19
|
+
add_column :users, :otp_secret_key, :string
|
20
|
+
|
21
|
+
# Tracking
|
22
|
+
add_column :users, :sign_in_count, :integer, :default => 0
|
23
|
+
add_column :users, :current_sign_in_at, :datetime
|
24
|
+
add_column :users, :last_sign_in_at, :datetime
|
25
|
+
add_column :users, :current_sign_in_ip, :string
|
26
|
+
add_column :users, :last_sign_in_ip, :string
|
27
|
+
|
28
|
+
# Analytics
|
29
|
+
add_column :users, :original_source, :string
|
30
|
+
add_column :users, :session_source, :string
|
31
|
+
add_column :users, :first_visit_at, :datetime
|
32
|
+
add_column :users, :last_visit_at, :datetime
|
33
|
+
|
34
|
+
# Forgot password / Password reset
|
35
|
+
add_column :users, :reset_password_token, :string
|
36
|
+
add_column :users, :reset_password_token_created_at, :datetime
|
37
|
+
|
38
|
+
# Remember
|
39
|
+
add_column :users, :remember_token, :string
|
40
|
+
add_column :users, :remember_token_created_at, :datetime
|
41
|
+
|
42
|
+
# Confirmation
|
43
|
+
add_column :users, :confirmation_email, :string
|
44
|
+
add_column :users, :confirmation_token, :string
|
45
|
+
add_column :users, :confirmation_token_created_at, :string
|
46
|
+
|
47
|
+
# Lockout
|
48
|
+
add_column :users, :failed_attempts, :integer, :default => 0
|
49
|
+
add_column :users, :locked_at, :datetime
|
50
|
+
add_column :users, :unlock_token, :string
|
51
|
+
add_column :users, :unlock_token_created_at, :datetime
|
52
|
+
|
53
|
+
# Make sure the validations are enforced
|
54
|
+
add_index :users, :email, :unique => true
|
55
|
+
add_index :users, :username, :unique => true
|
56
|
+
add_index :users, :reset_password_token, :unique => true
|
57
|
+
add_index :users, :remember_token, :unique => true
|
58
|
+
add_index :users, :confirmation_token, :unique => true
|
59
|
+
add_index :users, :unlock_token, :unique => true
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.down
|
64
|
+
drop_column :users, :email
|
65
|
+
drop_column :users, :password_digest
|
66
|
+
drop_column :users, :username
|
67
|
+
|
68
|
+
drop_column :users, :time_zone
|
69
|
+
drop_column :users, :first_name
|
70
|
+
drop_column :users, :last_name
|
71
|
+
drop_column :users, :bio
|
72
|
+
drop_column :users, :website
|
73
|
+
drop_column :users, :phone_number
|
74
|
+
|
75
|
+
drop_column :users, :otp_secret_key
|
76
|
+
|
77
|
+
# Tracking
|
78
|
+
drop_column :users, :sign_in_count
|
79
|
+
drop_column :users, :current_sign_in_at
|
80
|
+
drop_column :users, :last_sign_in_at
|
81
|
+
drop_column :users, :current_sign_in_ip
|
82
|
+
drop_column :users, :last_sign_in_ip
|
83
|
+
|
84
|
+
# Analytics
|
85
|
+
drop_column :users, :original_source
|
86
|
+
drop_column :users, :session_source
|
87
|
+
drop_column :users, :first_visit_at
|
88
|
+
drop_column :users, :last_visit_at
|
89
|
+
|
90
|
+
# Forgot password / Password reset
|
91
|
+
drop_column :users, :reset_password_token
|
92
|
+
drop_column :users, :reset_password_created_at
|
93
|
+
|
94
|
+
# Remember
|
95
|
+
drop_column :users, :remember_token
|
96
|
+
drop_column :users, :remember_token_created_at
|
97
|
+
|
98
|
+
# Confirmation
|
99
|
+
drop_column :users, :confirmation_email
|
100
|
+
drop_column :users, :confirmation_token
|
101
|
+
drop_column :users, :confirmation_token_created_at
|
102
|
+
|
103
|
+
# Lockout
|
104
|
+
drop_column :users, :failed_attempts
|
105
|
+
drop_column :users, :locked_at
|
106
|
+
drop_column :users, :unlock_token
|
107
|
+
drop_column :users, :unlock_token_created_at
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Generated by Authkit.
|
2
|
+
#
|
3
|
+
# Create a users table for managing authentication. Fields to handle
|
4
|
+
# the authentication are created in the AddAuthkitFieldsToUsers
|
5
|
+
# migration.
|
6
|
+
#
|
7
|
+
class CreateUsers < ActiveRecord::Migration
|
8
|
+
def self.up
|
9
|
+
create_table :users do |t|
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :users
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class EmailFormatValidator < ActiveModel::EachValidator
|
2
|
+
def validate_each(object, attribute, value)
|
3
|
+
unless EmailFormatValidator::is_valid_email(value)
|
4
|
+
object.errors[attribute] << (options[:message] || "is not a valid email address")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.is_valid_email(value)
|
9
|
+
value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ApplicationController do
|
4
|
+
let(:user_params) { { email: "test@example.com", username: "test", password: "example", password_confirmation: "example" } }
|
5
|
+
let(:user) { User.new(user_params) }
|
6
|
+
let(:logged_in_session) { { user_id: "1" } }
|
7
|
+
let(:unknown_session) { { user_id: "2" } }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
User.stub(:find_by).with("1").and_return(user)
|
11
|
+
end
|
12
|
+
|
13
|
+
controller do
|
14
|
+
before_filter :require_login, only: [:index]
|
15
|
+
before_filter :require_token, only: [:show]
|
16
|
+
|
17
|
+
def new
|
18
|
+
head :ok
|
19
|
+
end
|
20
|
+
|
21
|
+
def index
|
22
|
+
head :ok
|
23
|
+
end
|
24
|
+
|
25
|
+
def show
|
26
|
+
head :ok
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "current_user" do
|
31
|
+
it "returns nil if there is no current user" do
|
32
|
+
get :new
|
33
|
+
controller.send(:current_user).should be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "does not perform multiple finds" do
|
37
|
+
User.should_receive(:find_by)
|
38
|
+
get :new, {}, unknown_session
|
39
|
+
controller.send(:current_user).should be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "finds the current user in the session" do
|
43
|
+
get :new, {}, logged_in_session
|
44
|
+
controller.send(:current_user).should == user
|
45
|
+
end
|
46
|
+
|
47
|
+
it "finds the current user from the remember cookie" do
|
48
|
+
# Need to sign the cookie
|
49
|
+
request.env["action_dispatch.secret_token"] = "SECRET"
|
50
|
+
verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
|
51
|
+
request.cookies[:remember] = verifier.generate("TOKEN")
|
52
|
+
User.should_receive(:user_from_remember_token).with("TOKEN").and_return(user)
|
53
|
+
get :index
|
54
|
+
controller.send(:current_user).should == user
|
55
|
+
end
|
56
|
+
|
57
|
+
it "sets the time zone" do
|
58
|
+
user.should_receive(:time_zone).and_return("Pacific Time (US & Canada)")
|
59
|
+
get :index, {}, logged_in_session
|
60
|
+
Time.zone.name.should == "Pacific Time (US & Canada)"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "has a logged in helper method" do
|
64
|
+
get :new, {}, logged_in_session
|
65
|
+
controller.should be_logged_in
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "tracking" do
|
70
|
+
it "does not allow tracking if there is a do not track header" do
|
71
|
+
request.headers["DNT"] = "1"
|
72
|
+
get :new
|
73
|
+
controller.send(:allow_tracking?).should == false
|
74
|
+
end
|
75
|
+
|
76
|
+
it "allows tracking if there is no do not track header" do
|
77
|
+
get :new
|
78
|
+
controller.send(:allow_tracking?).should == true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "when requiring a user" do
|
83
|
+
it "allows access if there is a user" do
|
84
|
+
get :index, {}, logged_in_session
|
85
|
+
response.should be_success
|
86
|
+
end
|
87
|
+
|
88
|
+
it "stores the return path" do
|
89
|
+
get :index, {}
|
90
|
+
session[:return_url].should == "/anonymous"
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "when responding to html" do
|
94
|
+
it "sets the flash message" do
|
95
|
+
get :index, {}
|
96
|
+
flash.should_not be_empty
|
97
|
+
end
|
98
|
+
|
99
|
+
it "redirecs the user to login" do
|
100
|
+
get :index, {}
|
101
|
+
response.should be_redirect
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "when responding to json" do
|
106
|
+
it "returns a forbidden status" do
|
107
|
+
get :index, {format: :json}
|
108
|
+
response.code.should == "403"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "tokens" do
|
114
|
+
it "requires a user token" do
|
115
|
+
User.should_receive(:user_from_token).with('testtoken').and_return(user)
|
116
|
+
get 'show', {id: '1', token: 'testtoken'}
|
117
|
+
end
|
118
|
+
|
119
|
+
it "returns an error if there is no user token" do
|
120
|
+
User.should_receive(:user_from_token).with('testtoken').and_return(nil)
|
121
|
+
controller.should_receive(:deny_user)
|
122
|
+
get 'show', {id: '1', token: 'testtoken'}
|
123
|
+
end
|
124
|
+
|
125
|
+
it "verifies the token" do
|
126
|
+
request.env["action_dispatch.secret_token"] = "SECRET"
|
127
|
+
verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
|
128
|
+
token = verifier.generate("TOKEN")
|
129
|
+
User.should_receive(:user_from_token).with(token).and_return(user)
|
130
|
+
get 'show', {id: '1', token: token}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "login" do
|
135
|
+
it "tracks the login" do
|
136
|
+
get :new
|
137
|
+
user.should_receive(:track_sign_in)
|
138
|
+
controller.send(:login, user)
|
139
|
+
end
|
140
|
+
|
141
|
+
it "remembers the user using a token and cookie" do
|
142
|
+
get :new
|
143
|
+
controller.should_receive(:set_remember_cookie)
|
144
|
+
user.should_receive(:set_token).with(:remember_token).and_return(:true)
|
145
|
+
controller.send(:login, user)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "resets the session" do
|
149
|
+
get :new
|
150
|
+
controller.should_receive(:reset_session)
|
151
|
+
controller.send(:login, user)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "logout" do
|
156
|
+
it "resets the session" do
|
157
|
+
get :index, {}, logged_in_session
|
158
|
+
controller.should_receive(:reset_session)
|
159
|
+
controller.send(:logout)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "logs the user out" do
|
163
|
+
get :index, {}, logged_in_session
|
164
|
+
controller.send(:logout)
|
165
|
+
controller.send(:current_user).should be_nil
|
166
|
+
end
|
167
|
+
|
168
|
+
it "clears the remember token" do
|
169
|
+
get :index, {}, logged_in_session
|
170
|
+
user.should_receive(:clear_remember_token).and_return(:true)
|
171
|
+
controller.send(:logout)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
it "sets the remember cookie" do
|
176
|
+
request.env["action_dispatch.secret_token"] = "SECRET"
|
177
|
+
get :new
|
178
|
+
controller.send(:login, user)
|
179
|
+
cookies.permanent.signed[:remember].should == user.remember_token
|
180
|
+
end
|
181
|
+
|
182
|
+
it "redirects to a stored session location if present" do
|
183
|
+
get :new, {}, {return_url: "/return"}
|
184
|
+
controller.should_receive(:redirect_to).with("/return").and_return(true)
|
185
|
+
controller.send(:redirect_back_or_default)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|