passwordless 0.12.0 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +108 -191
- data/Rakefile +7 -7
- data/app/controllers/passwordless/sessions_controller.rb +121 -39
- data/app/mailers/passwordless/mailer.rb +13 -11
- data/app/models/passwordless/session.rb +25 -12
- data/app/views/passwordless/mailer/sign_in.text.erb +1 -0
- data/app/views/passwordless/sessions/new.html.erb +8 -4
- data/app/views/passwordless/sessions/show.html.erb +5 -0
- data/config/locales/en.yml +18 -6
- data/config/routes.rb +0 -4
- data/db/migrate/20171104221735_create_passwordless_sessions.rb +1 -3
- data/lib/generators/passwordless/views_generator.rb +5 -5
- data/lib/passwordless/config.rb +71 -0
- data/lib/passwordless/controller_helpers.rb +32 -70
- data/lib/passwordless/engine.rb +2 -6
- data/lib/passwordless/errors.rb +4 -0
- data/lib/passwordless/router_helpers.rb +24 -10
- data/lib/passwordless/short_token_generator.rb +9 -0
- data/lib/passwordless/test_helpers.rb +19 -9
- data/lib/passwordless/token_digest.rb +18 -0
- data/lib/passwordless/version.rb +1 -1
- data/lib/passwordless.rb +5 -19
- metadata +12 -51
- data/app/views/passwordless/mailer/magic_link.text.erb +0 -1
- data/lib/passwordless/url_safe_base_64_generator.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '09a1ddce2bcfe831bf08b8e0e2e48ce7d8941ea4a44ed9ea0091de3ece68b9f3'
|
4
|
+
data.tar.gz: 35d3d30954025caa77b06a2ff6c51579cbe78135579733cdb92323cd1c140e7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16be37d7458a6749f1df567fa15b7e480913a21a85ec3fab9dfabc737e4ab85308a93f387c663939b863967968e9e26c9e3bdfb1ec1b26e957570a109bd170ea
|
7
|
+
data.tar.gz: 5715a453783257aa2f3065f85e9a2b3a71079fca9519ff7fbc92e0b5dfe491b5b8846f7dfd24e458b15589746be0c5fd6d5996ec7c3feebc2ed9bf6c3aa1836a
|
data/README.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
**NOTE:** Passwordless is currently going through some breaking changes. Be aware that the docs in `master` aren't necessarily the same as for you installed version.
|
2
|
+
|
3
|
+
---
|
4
|
+
|
1
5
|
<p align='center'>
|
2
6
|
<img src='https://s3.brnbw.com/Passwordless-title-gaIVkX0sPg.svg' alt='Passwordless' />
|
3
7
|
<br />
|
@@ -12,37 +16,34 @@ Add authentication to your Rails app without all the icky-ness of passwords.
|
|
12
16
|
|
13
17
|
## Table of Contents
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
19
|
+
<!--toc:start-->
|
20
|
+
|
21
|
+
- [Table of Contents](#table-of-contents)
|
22
|
+
- [Installation](#installation)
|
23
|
+
- [Usage](#usage)
|
24
|
+
- [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
|
25
|
+
- [Providing your own templates](#providing-your-own-templates)
|
26
|
+
- [Registering new users](#registering-new-users)
|
27
|
+
- [URLs and links](#urls-and-links)
|
28
|
+
- [Configuration](#configuration)
|
29
|
+
- [Delivery method](#delivery-method)
|
30
|
+
- [Token generation](#token-generation)
|
31
|
+
- [Timeout and Expiry](#timeout-and-expiry)
|
32
|
+
- [Redirection after sign-in](#redirection-after-sign-in)
|
33
|
+
- [Looking up the user](#looking-up-the-user)
|
34
|
+
- [Claiming tokens](#claiming-tokens)
|
35
|
+
- [Test helpers](#test-helpers)
|
36
|
+
- [Security considerations](#security-considerations)
|
37
|
+
- [Alternatives](#alternatives)
|
38
|
+
- [License](#license)
|
39
|
+
<!--toc:end-->
|
33
40
|
|
34
41
|
## Installation
|
35
42
|
|
36
|
-
Add
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
gem 'passwordless'
|
40
|
-
```
|
41
|
-
|
42
|
-
Install it and copy over the migrations:
|
43
|
+
Add to your bundle and copy over the migrations:
|
43
44
|
|
44
45
|
```sh
|
45
|
-
$ bundle
|
46
|
+
$ bundle add passwordless
|
46
47
|
$ bin/rails passwordless:install:migrations
|
47
48
|
```
|
48
49
|
|
@@ -97,6 +98,7 @@ class ApplicationController < ActionController::Base
|
|
97
98
|
|
98
99
|
def require_user!
|
99
100
|
return if current_user
|
101
|
+
save_passwordless_redirect_location!(User) # <-- optional, see below
|
100
102
|
redirect_to root_path, flash: { error: 'You are not worthy!' }
|
101
103
|
end
|
102
104
|
end
|
@@ -116,30 +118,19 @@ end
|
|
116
118
|
|
117
119
|
### Providing your own templates
|
118
120
|
|
119
|
-
|
121
|
+
To make Passwordless look like your app, override the bundled views by adding your own. You can manually copy the specific views that you need or copy them to your application with `rails generate passwordless:views`.
|
120
122
|
|
121
|
-
|
123
|
+
Passwordless has 2 action views and 1 mailer view:
|
122
124
|
|
123
125
|
```sh
|
124
126
|
# the form where the user inputs their email address
|
125
127
|
app/views/passwordless/sessions/new.html.erb
|
126
|
-
#
|
127
|
-
app/views/passwordless/sessions/
|
128
|
-
# the
|
129
|
-
app/views/passwordless/mailer/
|
130
|
-
```
|
131
|
-
|
132
|
-
If you'd like to let the user know whether or not a record was found, `@resource` is provided to the view. You may override `app/views/passwordless/session/create.html.erb` for example like so:
|
133
|
-
```erb
|
134
|
-
<% if @resource.present? %>
|
135
|
-
<p>User found, check your inbox</p>
|
136
|
-
<% else %>
|
137
|
-
<p>No user found with the provided email address</p>
|
138
|
-
<% end %>
|
128
|
+
# the form where the user inputs their just received token
|
129
|
+
app/views/passwordless/sessions/show.html.erb
|
130
|
+
# the email with the token and magic link
|
131
|
+
app/views/passwordless/mailer/sign_in.text.erb
|
139
132
|
```
|
140
133
|
|
141
|
-
Please note that, from a security standpoint, this is a **bad practice** because you'd be giving information about which users are registered on your system. It is recommended to use a single message similar to the default one: "If we found you in the system, we've sent you an email". The **best practice** is to never expose which emails are registered on your system.
|
142
|
-
|
143
134
|
See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
|
144
135
|
|
145
136
|
### Registering new users
|
@@ -152,13 +143,13 @@ class UsersController < ApplicationController
|
|
152
143
|
# (unless you already have it in your ApplicationController)
|
153
144
|
|
154
145
|
def create
|
155
|
-
@user = User.new
|
146
|
+
@user = User.new(user_params)
|
156
147
|
|
157
148
|
if @user.save
|
158
|
-
sign_in
|
159
|
-
redirect_to
|
149
|
+
sign_in(build_passwordless_session(@user)) # <-- This!
|
150
|
+
redirect_to(@user, flash: { notice: 'Welcome!' })
|
160
151
|
else
|
161
|
-
render
|
152
|
+
render(:new)
|
162
153
|
end
|
163
154
|
end
|
164
155
|
|
@@ -172,137 +163,90 @@ By default, Passwordless uses the resource name given to `passwordless_for` to g
|
|
172
163
|
|
173
164
|
```ruby
|
174
165
|
passwordless_for :users
|
175
|
-
# <%=
|
166
|
+
# <%= users_sign_in_path %> # => /users/sign_in
|
176
167
|
|
177
168
|
passwordless_for :users, at: '/', as: :auth
|
178
|
-
# <%=
|
169
|
+
# <%= auth_sign_in_path %> # => /sign_in
|
179
170
|
```
|
180
171
|
|
181
|
-
Also be sure to
|
172
|
+
Also be sure to
|
173
|
+
[specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
|
182
174
|
|
183
|
-
|
175
|
+
## Configuration
|
184
176
|
|
185
|
-
|
177
|
+
To customize Passwordless, create a file `config/initializers/passwordless.rb`.
|
186
178
|
|
187
|
-
|
179
|
+
The default values are shown below. It's recommended to only include the ones that you specifically want to modify.
|
188
180
|
|
189
181
|
```ruby
|
190
|
-
Passwordless.
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
182
|
+
Passwordless.configure do |config|
|
183
|
+
config.default_from_address = "CHANGE_ME@example.com"
|
184
|
+
config.parent_mailer = "ActionMailer::Base"
|
185
|
+
config.restrict_token_reuse = false # Can a token/link be used multiple times?
|
186
|
+
config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
|
187
|
+
|
188
|
+
config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
|
189
|
+
config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.
|
190
|
+
|
191
|
+
config.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
|
192
|
+
config.redirect_to_response_options = {} # Additional options for redirects.
|
193
|
+
config.success_redirect_path = '/' # After a user successfully signs in
|
194
|
+
config.failure_redirect_path = '/' # After a sign in fails
|
195
|
+
config.sign_out_redirect_path = '/' # After a user signs out
|
196
196
|
end
|
197
197
|
```
|
198
198
|
|
199
|
-
|
199
|
+
### Delivery method
|
200
200
|
|
201
|
-
|
201
|
+
By default, Passwordless sends emails. See [Providing your own templates](#providing-your-own-templates). If you need to customize this further, you can do so in the `after_session_save` callback.
|
202
202
|
|
203
|
-
|
204
|
-
|
205
|
-
However, you can accomplish this with the following snippet of code.
|
203
|
+
In `config/initializers/passwordless.rb`:
|
206
204
|
|
207
205
|
```ruby
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
})
|
213
|
-
session.save!
|
214
|
-
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
215
|
-
```
|
206
|
+
Passwordless.configure do |config|
|
207
|
+
config.after_session_save = lambda do |session, request|
|
208
|
+
# Default behavior is
|
209
|
+
# Passwordless::Mailer.sign_in(session).deliver_now
|
216
210
|
|
217
|
-
You can
|
218
|
-
|
219
|
-
@magic_link = "#{@magic_link}?destination_path=/your-custom-path"
|
220
|
-
```
|
221
|
-
|
222
|
-
### Overrides
|
223
|
-
|
224
|
-
By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
|
225
|
-
|
226
|
-
You can override this and provide your own customer fetcher by defining a class method `fetch_resource_for_passwordless` in your passwordless model. The method will be called with the downcased email and should return an `ActiveRecord` instance of the model.
|
227
|
-
|
228
|
-
Example time:
|
229
|
-
|
230
|
-
Let's say we would like to fetch the record and if it doesn't exist, create automatically.
|
231
|
-
|
232
|
-
```ruby
|
233
|
-
class User < ApplicationRecord
|
234
|
-
def self.fetch_resource_for_passwordless(email)
|
235
|
-
find_or_create_by(email: email)
|
211
|
+
# You can change behavior to do something with session model. For example,
|
212
|
+
# SmsApi.send_sms(session.authenticatable.phone_number, session.token)
|
236
213
|
end
|
237
214
|
end
|
238
215
|
```
|
239
216
|
|
240
|
-
|
241
|
-
|
242
|
-
The following configuration parameters are supported. You can override these for example in `initializers/passwordless.rb`.
|
217
|
+
### Token generation
|
243
218
|
|
244
|
-
|
219
|
+
By default Passwordless generates short, 6-digit, alpha numeric tokens. You can change the generator using `Passwordless.config.token_generator` to something else that responds to `call(session)` eg.:
|
245
220
|
|
246
221
|
```ruby
|
247
|
-
Passwordless.
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
Passwordless.expires_at = lambda { 1.year.from_now } # How long until a passwordless session expires.
|
254
|
-
Passwordless.timeout_at = lambda { 1.hour.from_now } # How long until a magic link expires.
|
255
|
-
|
256
|
-
# redirection session behavior
|
257
|
-
Passwordless.redirect_to_response_options = {} # any allowed response_options for redirect_to can go in here
|
258
|
-
|
259
|
-
# Default redirection paths
|
260
|
-
Passwordless.success_redirect_path = '/' # When a user succeeds in logging in.
|
261
|
-
Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
|
262
|
-
Passwordless.sign_out_redirect_path = '/' # When a user logs out.
|
263
|
-
```
|
264
|
-
|
265
|
-
### Customizing token generation
|
266
|
-
|
267
|
-
By default Passwordless generates tokens using `SecureRandom.urlsafe_base64` but you can change that by setting `Passwordless.token_generator` to something else that responds to `call(session)` eg.:
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
Passwordless.token_generator = -> (session) {
|
271
|
-
"probably-stupid-token-#{session.user_agent}-#{Time.current}"
|
272
|
-
}
|
222
|
+
Passwordless.configure do |config|
|
223
|
+
config.token_generator = lambda do |session|
|
224
|
+
"probably-stupid-token-#{session.user_agent}-#{Time.current}"
|
225
|
+
end
|
226
|
+
end
|
273
227
|
```
|
274
228
|
|
275
|
-
|
229
|
+
Passwordless will keep generating tokens until it finds one that hasn't been used yet. So be sure to use some kind of method where matches are unlikely.
|
276
230
|
|
277
|
-
###
|
231
|
+
### Timeout and Expiry
|
278
232
|
|
279
|
-
|
233
|
+
The _timeout_ is the time by which the generated token and magic link is invalidated. After this the token cannot be used to sign in to your app and the user will need to request a new token.
|
280
234
|
|
281
|
-
|
235
|
+
The _expiry_ is the expiration time of the session of a logged in user. Once this is expired, the user is signed out.
|
282
236
|
|
283
|
-
|
237
|
+
**Note:** Passwordless' session relies on Rails' own session and so will never live longer than that.
|
284
238
|
|
285
|
-
|
286
|
-
|
287
|
-
> Make sure to use a `.call`able object, like a proc or lambda as it will be called everytime a session is created.
|
239
|
+
To configure your Rails session, in `config/initializers/session_store.rb`:
|
288
240
|
|
289
241
|
```ruby
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
#### Session Expiry
|
294
|
-
|
295
|
-
Session expiry is the time when the actual session is itself expired, i.e. users will be logged out and has to sign back in post this expiry time. By default, sessions are valid for `1.year` from the time they are generated. You can override by providing your custom Proc function that returns a datetime object.
|
296
|
-
|
297
|
-
> Make sure to use a `.call`able object, like a proc or lambda as it will be called everytime a session is created.
|
298
|
-
|
299
|
-
```ruby
|
300
|
-
Passwordless.expires_at = lambda { 24.hours.from_now }
|
242
|
+
Rails.application.config.session_store :cookie_store,
|
243
|
+
expire_after: 1.year,
|
244
|
+
# ...
|
301
245
|
```
|
302
246
|
|
303
|
-
###
|
247
|
+
### Redirection after sign-in
|
304
248
|
|
305
|
-
By default Passwordless will redirect back to where the user wanted to go
|
249
|
+
By default Passwordless will redirect back to where the user wanted to go _if_ it knows where that is -- so you'll have to help it. `Passwordless::ControllerHelpers` provide a method:
|
306
250
|
|
307
251
|
```ruby
|
308
252
|
class ApplicationController < ActionController::Base
|
@@ -312,71 +256,45 @@ class ApplicationController < ActionController::Base
|
|
312
256
|
|
313
257
|
def require_user!
|
314
258
|
return if current_user
|
315
|
-
save_passwordless_redirect_location!(User) # <--
|
259
|
+
save_passwordless_redirect_location!(User) # <-- this one!
|
316
260
|
redirect_to root_path, flash: {error: 'You are not worthy!'}
|
317
261
|
end
|
318
262
|
end
|
319
263
|
```
|
320
264
|
|
321
|
-
This can be turned off with `Passwordless.redirect_back_after_sign_in = false
|
322
|
-
|
323
|
-
### Claiming tokens
|
324
|
-
|
325
|
-
Opt-in for marking tokens as `claimed` so they can only be used once.
|
326
|
-
|
327
|
-
config/initializers/passwordless.rb
|
328
|
-
|
329
|
-
```ruby
|
330
|
-
# Default is `false`
|
331
|
-
Passwordless.restrict_token_reuse = true
|
332
|
-
```
|
333
|
-
|
334
|
-
#### Upgrading an existing Rails app to use claim token
|
265
|
+
This can also be turned off with `Passwordless.config.redirect_back_after_sign_in = false`.
|
335
266
|
|
336
|
-
|
267
|
+
### Looking up the user
|
337
268
|
|
338
|
-
|
339
|
-
<summary>Example migration</summary>
|
269
|
+
By default Passwordless uses the `passwordless_with` column to _case insensitively_ fetch the user resource.
|
340
270
|
|
341
|
-
|
342
|
-
bin/rails generate migration add_claimed_at_to_passwordless_sessions
|
343
|
-
```
|
271
|
+
You can override this by defining a class method `fetch_resource_for_passwordless` in your user model. This method will be called with the down-cased, stripped `email` and should return an `ActiveRecord` instance.
|
344
272
|
|
345
273
|
```ruby
|
346
|
-
class
|
347
|
-
def
|
348
|
-
|
274
|
+
class User < ApplicationRecord
|
275
|
+
def self.fetch_resource_for_passwordless(email)
|
276
|
+
find_or_create_by(email: email)
|
349
277
|
end
|
350
278
|
end
|
351
|
-
|
352
279
|
```
|
353
|
-
</details>
|
354
280
|
|
355
|
-
###
|
281
|
+
### Claiming tokens
|
282
|
+
|
283
|
+
By default, a token/magic link **can** be used more than once.
|
356
284
|
|
357
|
-
|
358
|
-
to change the type of `passwordless`' `authenticatable_id` field to match your primary key type (this will also involve dropping and recreating associated indices).
|
285
|
+
To change, in `config/initializers/passwordless.rb`:
|
359
286
|
|
360
|
-
Here is an example migration you can use:
|
361
287
|
```ruby
|
362
|
-
|
363
|
-
|
364
|
-
remove_index :passwordless_sessions, column: [:authenticatable_type, :authenticatable_id] if index_exists? :authenticatable_type, :authenticatable_id
|
365
|
-
remove_column :passwordless_sessions, :authenticatable_id
|
366
|
-
add_column :passwordless_sessions, :authenticatable_id, :uuid
|
367
|
-
add_index :passwordless_sessions, [:authenticatable_type, :authenticatable_id], name: 'authenticatable'
|
368
|
-
end
|
288
|
+
Passwordless.configure do |config|
|
289
|
+
config.restrict_token_reuse = true
|
369
290
|
end
|
370
291
|
```
|
371
292
|
|
372
|
-
|
373
|
-
|
374
|
-
## Testing helpers
|
293
|
+
## Test helpers
|
375
294
|
|
376
295
|
To help with testing, a set of test helpers are provided.
|
377
296
|
|
378
|
-
If you are using RSpec, add the following line to your `spec/rails_helper.rb
|
379
|
-
`spec/spec_helper.rb` if `rails_helper.rb` does not exist:
|
297
|
+
If you are using RSpec, add the following line to your `spec/rails_helper.rb`:
|
380
298
|
|
381
299
|
```ruby
|
382
300
|
require "passwordless/test_helpers"
|
@@ -388,7 +306,6 @@ If you are using TestUnit, add this line to your `test/test_helper.rb`:
|
|
388
306
|
require "passwordless/test_helpers"
|
389
307
|
```
|
390
308
|
|
391
|
-
|
392
309
|
Then in your controller, request, and system tests/specs, you can utilize the following methods:
|
393
310
|
|
394
311
|
```ruby
|
@@ -396,18 +313,18 @@ passwordless_sign_in(user) # signs you in as a user
|
|
396
313
|
passwordless_sign_out # signs out user
|
397
314
|
```
|
398
315
|
|
399
|
-
##
|
316
|
+
## Security considerations
|
400
317
|
|
401
|
-
There's no reason that this approach should be less secure than the usual username/password combo. In fact this is most often a more secure option, as users don't get to choose the
|
318
|
+
There's no reason that this approach should be less secure than the usual username/password combo. In fact this is most often a more secure option, as users don't get to choose the horrible passwords they can't seem to stop using. In a way, this is just the same as having each user go through "Forgot password" on every login.
|
402
319
|
|
403
|
-
But be aware that when everyone authenticates via emails
|
320
|
+
But be aware that when everyone authenticates via emails, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your email delivery provider account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was once compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-stolen-hack/) using this method.
|
404
321
|
|
405
|
-
Ideally you should set up your email provider to not log these mails. And be sure to turn on 2-factor
|
322
|
+
Ideally you should set up your email provider to not log these mails. And be sure to turn on non-SMS 2-factor authentication if your provider supports it.
|
406
323
|
|
407
|
-
|
324
|
+
## Alternatives
|
408
325
|
|
409
326
|
- [OTP JWT](https://github.com/stas/otp-jwt) -- Passwordless JSON Web Tokens
|
410
327
|
|
411
|
-
|
328
|
+
## License
|
412
329
|
|
413
330
|
MIT
|
data/Rakefile
CHANGED
@@ -2,17 +2,19 @@
|
|
2
2
|
|
3
3
|
begin
|
4
4
|
require "bundler/setup"
|
5
|
+
|
5
6
|
rescue LoadError
|
6
|
-
puts
|
7
|
+
puts("You must `gem install bundler` and `bundle install` to run rake tasks")
|
7
8
|
end
|
8
9
|
|
9
10
|
require "yard"
|
11
|
+
|
10
12
|
YARD::Rake::YardocTask.new
|
11
|
-
task
|
13
|
+
task(docs: :yard)
|
12
14
|
|
13
15
|
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
14
|
-
load
|
15
|
-
load
|
16
|
+
load("rails/tasks/engine.rake")
|
17
|
+
load("rails/tasks/statistics.rake")
|
16
18
|
|
17
19
|
require "bundler/gem_tasks"
|
18
20
|
|
@@ -24,6 +26,4 @@ Rake::TestTask.new(:test) do |t|
|
|
24
26
|
t.verbose = false
|
25
27
|
end
|
26
28
|
|
27
|
-
task
|
28
|
-
|
29
|
-
require "standard/rake"
|
29
|
+
task(default: :test)
|