auther 4.1.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/README.md +70 -43
- data/app/assets/stylesheets/auther/application.scss +3 -1
- data/app/assets/stylesheets/auther/auther.scss +98 -9
- data/app/controllers/auther/base_controller.rb +26 -21
- data/app/controllers/auther/session_controller.rb +10 -7
- data/app/models/auther/account.rb +2 -1
- data/app/presenters/auther/account.rb +10 -0
- data/app/views/auther/session/new.html.slim +19 -37
- data/app/views/layouts/auther/auth.html.slim +0 -2
- data/bin/rails +4 -7
- data/lib/auther/authenticator.rb +14 -18
- data/lib/auther/cipher.rb +2 -3
- data/lib/auther/engine.rb +3 -22
- data/lib/auther/gatekeeper.rb +18 -15
- data/lib/auther/identity.rb +3 -2
- data/lib/auther/keymaster.rb +5 -4
- data/lib/auther/null_logger.rb +6 -6
- data/lib/auther/settings.rb +14 -7
- data/lib/auther/tasks/rspec.rake +6 -0
- data/lib/auther/tasks/rubocop.rake +6 -0
- data/lib/generators/auther/install/install_generator.rb +1 -0
- data/vendor/assets/stylesheets/bitters/_base.scss +10 -0
- data/vendor/assets/stylesheets/bitters/_buttons.scss +35 -0
- data/vendor/assets/stylesheets/bitters/_forms.scss +90 -0
- data/vendor/assets/stylesheets/bitters/_grid-settings.scss +14 -0
- data/vendor/assets/stylesheets/bitters/_typography.scss +49 -0
- data/vendor/assets/stylesheets/bitters/_variables.scss +42 -0
- metadata +53 -20
- metadata.gz.sig +0 -0
- data/app/assets/javascripts/auther/application.js +0 -5
- data/app/assets/stylesheets/auther/foundation_and_overrides.scss +0 -1191
- data/app/helpers/auther/foundation_helper.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: afbf9e0daac1c015150d4c65cdb4db78c533df92
|
4
|
+
data.tar.gz: ddc7c7aee85af6ce9320701d5f3a701f19936e77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e16402c17bb22ca4cf6f7d94bd9f95bc59293639e264fbec6a8c8cb2b11a307558361dd8b784c4b4fa5acf5e2f80b62a0ed7630f7ac464f3c39d3d67883f06cc
|
7
|
+
data.tar.gz: deffffbbff5597864342a75314fa5e03d1d08ce982cc274bf1468a65320e0d9269392afb27daedafb7562d198616fa56e6b612ec1161052ccd53a283319346b6
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/README.md
CHANGED
@@ -1,36 +1,67 @@
|
|
1
|
-
#
|
1
|
+
# Auther
|
2
2
|
|
3
|
-
[![Gem Version](https://badge.fury.io/rb/auther.
|
4
|
-
[![Code Climate GPA](https://codeclimate.com/github/bkuhlmann/auther.
|
5
|
-
[![Code Climate Coverage](https://codeclimate.com/github/bkuhlmann/auther/coverage.
|
6
|
-
[![Gemnasium Status](https://gemnasium.com/bkuhlmann/auther.
|
7
|
-
[![Travis CI Status](https://secure.travis-ci.org/bkuhlmann/auther.
|
8
|
-
[![
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/auther.svg)](http://badge.fury.io/rb/auther)
|
4
|
+
[![Code Climate GPA](https://codeclimate.com/github/bkuhlmann/auther.svg)](https://codeclimate.com/github/bkuhlmann/auther)
|
5
|
+
[![Code Climate Coverage](https://codeclimate.com/github/bkuhlmann/auther/coverage.svg)](https://codeclimate.com/github/bkuhlmann/auther)
|
6
|
+
[![Gemnasium Status](https://gemnasium.com/bkuhlmann/auther.svg)](https://gemnasium.com/bkuhlmann/auther)
|
7
|
+
[![Travis CI Status](https://secure.travis-ci.org/bkuhlmann/auther.svg)](http://travis-ci.org/bkuhlmann/auther)
|
8
|
+
[![Patreon](https://img.shields.io/badge/patreon-donate-brightgreen.svg)](https://www.patreon.com/bkuhlmann)
|
9
9
|
|
10
10
|
Provides simple, form-based authentication for apps that need security but don't want to deal with the clunky UI
|
11
11
|
of HTTP Basic Authentication or something as heavyweight as [Devise](https://github.com/plataformatec/devise). It
|
12
12
|
doesn't require a database and is compatible with password managers like [1Password](https://agilebits.com/onepassword)
|
13
13
|
making for a pleasent user experience.
|
14
14
|
|
15
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
16
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
17
|
+
# Table of Contents
|
18
|
+
|
19
|
+
- [Features](#features)
|
20
|
+
- [Requirements](#requirements)
|
21
|
+
- [Setup](#setup)
|
22
|
+
- [Usage](#usage)
|
23
|
+
- [Initializer](#initializer)
|
24
|
+
- [Routes](#routes)
|
25
|
+
- [Model](#model)
|
26
|
+
- [Presenter](#presenter)
|
27
|
+
- [View](#view)
|
28
|
+
- [Controller](#controller)
|
29
|
+
- [Logging](#logging)
|
30
|
+
- [Troubleshooting](#troubleshooting)
|
31
|
+
- [Tests](#tests)
|
32
|
+
- [Code of Conduct](#code-of-conduct)
|
33
|
+
- [Contributions](#contributions)
|
34
|
+
- [License](#license)
|
35
|
+
- [History](#history)
|
36
|
+
- [Credits](#credits)
|
37
|
+
|
38
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
39
|
+
|
15
40
|
# Features
|
16
41
|
|
17
|
-
-
|
42
|
+
- Supports form-based authentication compatible with password managers like
|
43
|
+
[1Password](https://agilebits.com/onepassword).
|
44
|
+
|
45
|
+
[![Screenshot - Large Valid](doc/screenshots/large-valid.png)](https://github.com/bkuhlmann/auther)
|
46
|
+
[![Screenshot - Large Invalid](doc/screenshots/large-invalid.png)](https://github.com/bkuhlmann/auther)
|
47
|
+
|
48
|
+
- Supports mobile layouts and small screens:
|
18
49
|
|
19
|
-
[![Screenshot -
|
20
|
-
[![Screenshot -
|
50
|
+
[![Screenshot - Mobile Valid](doc/screenshots/mobile-valid.png)](https://github.com/bkuhlmann/auther)
|
51
|
+
[![Screenshot - Mobile Invalid](doc/screenshots/mobile-invalid.png)](https://github.com/bkuhlmann/auther)
|
21
52
|
|
22
|
-
-
|
23
|
-
|
24
|
-
-
|
25
|
-
-
|
26
|
-
-
|
27
|
-
-
|
28
|
-
-
|
53
|
+
- Uses [Bourbon](http://bourbon.io), [Neat](http://neat.bourbon.io), and [Bitters](http://bitters.bourbon.io) for
|
54
|
+
lightweight styling.
|
55
|
+
- Uses encrypted account credentials to keep sensitive information secure.
|
56
|
+
- Supports multiple accounts with account specific blacklists.
|
57
|
+
- Supports customizable routes, models, presenters, views, controllers, and loggers.
|
58
|
+
- Provides a generator for easy install and setup within an existing project.
|
59
|
+
- Provides auto-redirection to requested path for verified credentials.
|
29
60
|
|
30
61
|
# Requirements
|
31
62
|
|
32
63
|
0. [MRI 2.x.x](http://www.ruby-lang.org).
|
33
|
-
0. [Ruby on Rails 4.
|
64
|
+
0. [Ruby on Rails 4.2.x](http://rubyonrails.org).
|
34
65
|
|
35
66
|
# Setup
|
36
67
|
|
@@ -56,8 +87,7 @@ Run the generator to configure and initialize your application:
|
|
56
87
|
|
57
88
|
# Usage
|
58
89
|
|
59
|
-
Assuming you are using the
|
60
|
-
settings:
|
90
|
+
Assuming you are using the [dotenv](https://github.com/bkeepers/dotenv) gem, add the following to your `.env` settings:
|
61
91
|
|
62
92
|
AUTHER_SECRET=66is2tB4EbekG74DPGRmyQkdtZkQyNWZY6yeeNsmQ4Rpu42esdnP9X6puxpKfs64Gy2ghPu6QGTKsvQ73wXuDyWzDr
|
63
93
|
AUTHER_ADMIN_LOGIN=aHdMWUhiVGRyVHBPMmhTRWNRR082MFhNdVFkL2ZaSGpvY2VoVS90dGRpRT0tLXFBWWZDRkJ4aDR3Qy9aamNOeU1JekE9PQ==--bf077a68a8e654ed9e480851c9597dae57ec34b8
|
@@ -72,8 +102,6 @@ Use these credentials to login:
|
|
72
102
|
- Login: test@test.com
|
73
103
|
- Password: password
|
74
104
|
|
75
|
-
# Customization
|
76
|
-
|
77
105
|
## Initializer
|
78
106
|
|
79
107
|
The initializer (installed during setup) can be found here:
|
@@ -95,7 +123,7 @@ The initializer comes installed with the following settings:
|
|
95
123
|
**IMPORTANT**: The encrypted secret, login, and password credentials used in the `.env` setup above must be re-encrypted
|
96
124
|
before deploying to production! To encrypt/decrypt account credentials, launch a rails console and run the following:
|
97
125
|
|
98
|
-
# Best if more than 150 characters and gibberish to read. Must be
|
126
|
+
# Best if more than 150 characters and gibberish to read. Must be equal to what is defined in the `auther_settings`.
|
99
127
|
cipher = Auther::Cipher.new "vuKrwD9XWoYuv@s99?tR(9VqryiL,KV{W7wFnejUa4QcVBP+D{2rD4JfuD(mXgA=$tNK4Pfn#NeGs3o3TZ3CqNc^Qb"
|
100
128
|
|
101
129
|
# Do this to encrypt an unecrypted value.
|
@@ -110,12 +138,12 @@ The initializer can be customized as follows:
|
|
110
138
|
- *label* - Optional. The page label (what would appear above the form). Default: "Authorization".
|
111
139
|
- *secret* - Required. The secret passphrase used to encrypt/decrypt account credentials.
|
112
140
|
- *accounts* - Required. The array of accounts with different or similar access to the application.
|
113
|
-
- *name* - Required. The account name
|
141
|
+
- *name* - Required. The account name that uniquely identifies the account.
|
114
142
|
- *encrypted_login* - Required. The encrypted account login.
|
115
143
|
- *encrypted_password* - Required. The encrypted account password.
|
116
144
|
- *paths* - Required. The array of blacklisted paths for which only this account has access to.
|
117
145
|
- *authorized_url* - Optional. The URL to redirect to upon successful authorization. Authorized redirection works
|
118
|
-
|
146
|
+
in the order defined:
|
119
147
|
0. The blacklisted path (if requested prior to authorization but now authorized).
|
120
148
|
0. The authorized URL (if defined and the blacklisted path wasn't requested).
|
121
149
|
0. The root path (if none of the above).
|
@@ -124,7 +152,7 @@ The initializer can be customized as follows:
|
|
124
152
|
0. The deauthorized URL (if defined).
|
125
153
|
0. The auth URL.
|
126
154
|
- *auth_url* - Optional. The URL to redirect to when enforcing authentication. Default: “/login”.
|
127
|
-
- *logger* - Optional. The logger used to log path/account authorization messages. Default: Auther::NullLogger
|
155
|
+
- *logger* - Optional. The logger used to log path/account authorization messages. Default: `Auther::NullLogger`.
|
128
156
|
|
129
157
|
## Routes
|
130
158
|
|
@@ -140,7 +168,7 @@ The routes can be customized as follows (installed, by default, via the install
|
|
140
168
|
|
141
169
|
The [Auther::Account](app/models/auther/account.rb) is a plain old Ruby object that uses ActiveModel validations to aid
|
142
170
|
in attribute validation. This model could potentially be replaced with a database-backed object (would require
|
143
|
-
controller customization)...but you
|
171
|
+
controller customization)...but you should question if you have outgrown the use of this gem and need a different
|
144
172
|
solution altogether if it comes to that.
|
145
173
|
|
146
174
|
## Presenter
|
@@ -155,8 +183,8 @@ default Auther::SessionController implementation is sufficient):
|
|
155
183
|
|
156
184
|
app/views/auther/session/new.html
|
157
185
|
|
158
|
-
The form uses `@
|
159
|
-
|
186
|
+
The form uses `@account` instance variable which is an instance of the Auther::Presenter::Account presenter (as
|
187
|
+
mentioned above). The form can be stylized by attaching new styles to the .authorization class (see
|
160
188
|
[auther.scss](app/assets/stylesheets/auther/auther.scss) for details).
|
161
189
|
|
162
190
|
## Controller
|
@@ -170,8 +198,8 @@ you add a controller to your app that inherits from the Auther::BaseController.
|
|
170
198
|
layout "example"
|
171
199
|
end
|
172
200
|
|
173
|
-
This allows
|
174
|
-
Auther::BaseController for additional details or the Auther::SessionController for default implementation.
|
201
|
+
This allows customization of session controller behavior to serve any special business needs. See the
|
202
|
+
`Auther::BaseController` for additional details or the `Auther::SessionController` for default implementation.
|
175
203
|
|
176
204
|
## Logging
|
177
205
|
|
@@ -189,24 +217,23 @@ Auther settings:
|
|
189
217
|
- Account authentication pass/fail.
|
190
218
|
- Account and path authorization pass/fail.
|
191
219
|
|
192
|
-
|
193
|
-
|
194
|
-
To test, run:
|
195
|
-
|
196
|
-
bundle exec rspec spec
|
197
|
-
|
198
|
-
# Troubleshooting
|
220
|
+
## Troubleshooting
|
199
221
|
|
200
222
|
- If upgrading Rails, changing the cookie/session settings, generating a new secret base key, etc. this might
|
201
223
|
cause Auther authentication to fail. Make sure to clear your browser cookies in this situation or use Google
|
202
224
|
Chrome (incognito mode) to verify.
|
225
|
+
- If the authentication view/form looks broken (stylewise) this could be due to custom
|
226
|
+
`ActionView::Base.field_error_proc` settings defined by your app (usually via an initializer). Auther uses this
|
227
|
+
configuration `ActionView::Base.field_error_proc = proc { |html_tag, _| html_tag.html_safe }` so that no additional
|
228
|
+
markup is added to the DOM when errors are raised. If you have customized this to something else, you might want to
|
229
|
+
read the usage documentation (mentioned above) to rebuild the authentication view/form for your specific business
|
230
|
+
needs.
|
203
231
|
|
204
|
-
#
|
232
|
+
# Tests
|
233
|
+
|
234
|
+
To test, run:
|
205
235
|
|
206
|
-
|
207
|
-
support beyond what this engine can provide.
|
208
|
-
- [Devise](https://github.com/plataformatec/devise) - For complex situations where you need persisted user objects,
|
209
|
-
email support, social media support, and much more.
|
236
|
+
bundle exec rake
|
210
237
|
|
211
238
|
# Code of Conduct
|
212
239
|
|
@@ -1,11 +1,100 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
$auther-red: #F04124;
|
2
|
+
$auther-white: #FFFFFF;
|
3
|
+
$mobile: new-breakpoint(max-width 480px 4);
|
4
|
+
|
5
|
+
.auther-page {
|
6
|
+
@include row;
|
7
|
+
}
|
8
|
+
|
9
|
+
.auther-content {
|
10
|
+
@include outer-container;
|
11
|
+
}
|
12
|
+
|
13
|
+
.auther-credentials {
|
14
|
+
@include span-columns(6);
|
15
|
+
@include shift(2);
|
16
|
+
@include media($mobile) {
|
17
|
+
@include shift(0);
|
18
|
+
@include span-columns(4);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
.auther-title {
|
23
|
+
font-size: 2em;
|
24
|
+
font-weight: 400;
|
25
|
+
margin: 2em 0 1em 0;
|
26
|
+
text-align: center;
|
27
|
+
@include media($mobile) {
|
28
|
+
margin-top: 1em;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
.auther-form-section {
|
33
|
+
@include span-columns(6 of 6);
|
34
|
+
}
|
35
|
+
|
36
|
+
%auther-form-label {
|
37
|
+
@include span-columns(2 of 6);
|
38
|
+
font-weight: 300;
|
39
|
+
text-align: right;
|
40
|
+
@include media($mobile) {
|
41
|
+
@include span-columns(4 of 4);
|
42
|
+
@include omega();
|
43
|
+
text-align: center;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
.auther-form-label {
|
48
|
+
@extend %auther-form-label;
|
49
|
+
}
|
50
|
+
|
51
|
+
.auther-form-select-label {
|
52
|
+
@extend %auther-form-label;
|
53
|
+
}
|
54
|
+
|
55
|
+
.auther-form-select {
|
56
|
+
@include fill-parent;
|
57
|
+
}
|
58
|
+
|
59
|
+
.auther-form-group {
|
60
|
+
@include span-columns(4 of 6);
|
61
|
+
@include omega();
|
62
|
+
@include media($mobile) {
|
63
|
+
@include span-columns(4 of 4);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
.auther-button {
|
68
|
+
@include shift(1.3);
|
69
|
+
@include span-columns(4 of 6);
|
70
|
+
@include media($mobile) {
|
71
|
+
@include shift(0);
|
72
|
+
@include span-columns(4 of 4);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
.auther-error-message {
|
77
|
+
background: $auther-red;
|
78
|
+
color: $auther-white;
|
79
|
+
display: none;
|
80
|
+
padding: 0.3em 0.5em;
|
81
|
+
}
|
82
|
+
|
83
|
+
.auther-error {
|
84
|
+
.auther-form-label {
|
85
|
+
color: $auther-red;
|
86
|
+
}
|
87
|
+
|
88
|
+
.auther-form-group {
|
89
|
+
margin-bottom: 0.75em;
|
90
|
+
}
|
91
|
+
|
92
|
+
.auther-form-input {
|
93
|
+
border-color: $auther-red;
|
94
|
+
margin-bottom: 0;
|
95
|
+
}
|
96
|
+
|
97
|
+
.auther-error-message {
|
98
|
+
display: block;
|
10
99
|
}
|
11
100
|
}
|
@@ -1,35 +1,40 @@
|
|
1
1
|
module Auther
|
2
|
+
# Abstract controller for session management.
|
2
3
|
class BaseController < ActionController::Base
|
3
4
|
def show
|
4
5
|
redirect_to settings.auth_url
|
5
6
|
end
|
6
7
|
|
7
8
|
def new
|
8
|
-
@
|
9
|
+
@account = Auther::Presenter::Account.new
|
9
10
|
end
|
10
11
|
|
11
12
|
def create
|
12
|
-
@
|
13
|
-
|
14
|
-
authenticator = Auther::Authenticator.new settings.secret,
|
13
|
+
@account = Auther::Presenter::Account.new account_params
|
14
|
+
account = Auther::Account.new settings.find_account(@account.name)
|
15
|
+
authenticator = Auther::Authenticator.new settings.secret, account, @account
|
15
16
|
|
16
17
|
if authenticator.authenticated?
|
17
|
-
store_credentials
|
18
|
-
redirect_to authorized_url(
|
18
|
+
store_credentials account
|
19
|
+
redirect_to authorized_url(account)
|
19
20
|
else
|
20
|
-
remove_credentials
|
21
|
+
remove_credentials account
|
21
22
|
render template: new_template_path
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
26
|
def destroy
|
26
|
-
|
27
|
-
remove_credentials
|
28
|
-
redirect_to deauthorized_url(
|
27
|
+
account = Auther::Account.new settings.find_account(params[:name])
|
28
|
+
remove_credentials account
|
29
|
+
redirect_to deauthorized_url(account)
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
32
33
|
|
34
|
+
def account_params
|
35
|
+
params.require(:account).permit(:name, :login, :password)
|
36
|
+
end
|
37
|
+
|
33
38
|
def settings
|
34
39
|
Auther::Settings.new Rails.application.config.auther_settings
|
35
40
|
end
|
@@ -50,25 +55,25 @@ module Auther
|
|
50
55
|
end
|
51
56
|
|
52
57
|
def new_template_path
|
53
|
-
|
58
|
+
fail NotImplementedError, "The method, #new_template_path, is not implemented."
|
54
59
|
end
|
55
60
|
|
56
|
-
def authorized_url
|
57
|
-
session["auther_redirect_url"] ||
|
61
|
+
def authorized_url account
|
62
|
+
session["auther_redirect_url"] || account.authorized_url || "/"
|
58
63
|
end
|
59
64
|
|
60
|
-
def deauthorized_url
|
61
|
-
|
65
|
+
def deauthorized_url account
|
66
|
+
account.deauthorized_url || settings.auth_url
|
62
67
|
end
|
63
68
|
|
64
|
-
def store_credentials
|
65
|
-
keymaster = Auther::Keymaster.new
|
66
|
-
session[keymaster.login_key] =
|
67
|
-
session[keymaster.password_key] =
|
69
|
+
def store_credentials account
|
70
|
+
keymaster = Auther::Keymaster.new account.name
|
71
|
+
session[keymaster.login_key] = account.encrypted_login
|
72
|
+
session[keymaster.password_key] = account.encrypted_password
|
68
73
|
end
|
69
74
|
|
70
|
-
def remove_credentials
|
71
|
-
keymaster = Auther::Keymaster.new
|
75
|
+
def remove_credentials account
|
76
|
+
keymaster = Auther::Keymaster.new account.name
|
72
77
|
session.delete keymaster.login_key
|
73
78
|
session.delete keymaster.password_key
|
74
79
|
end
|
@@ -1,11 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Auther
|
2
|
+
# Default implementation for session management.
|
3
|
+
class SessionController < BaseController
|
4
|
+
layout "auther/auth"
|
5
|
+
before_filter :load_title, :load_label
|
6
|
+
before_filter :load_account_options, only: [:new, :create]
|
5
7
|
|
6
|
-
|
8
|
+
private
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
def new_template_path
|
11
|
+
"auther/session/new"
|
12
|
+
end
|
10
13
|
end
|
11
14
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require "active_model"
|
2
2
|
|
3
3
|
module Auther
|
4
|
+
# Represents an authenticatable account.
|
4
5
|
class Account
|
5
6
|
include ActiveModel::Validations
|
6
7
|
|
7
8
|
attr_accessor :name, :encrypted_login, :encrypted_password, :paths, :authorized_url, :deauthorized_url
|
8
9
|
|
9
10
|
validates :name, :encrypted_login, :encrypted_password, presence: true
|
10
|
-
validates :paths, presence: {unless:
|
11
|
+
validates :paths, presence: {unless: ->(account) { account.paths.is_a? Array }, message: "must be an array"}
|
11
12
|
|
12
13
|
def initialize options = {}
|
13
14
|
@name = options.fetch :name, nil
|