active_authentication 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +83 -6
- data/app/controllers/active_authentication/confirmations_controller.rb +1 -1
- data/app/controllers/active_authentication/magic_links_controller.rb +28 -0
- data/app/controllers/active_authentication/omniauth_callbacks_controller.rb +30 -0
- data/app/controllers/active_authentication/passwords_controller.rb +1 -1
- data/app/controllers/active_authentication/registrations_controller.rb +9 -5
- data/app/controllers/active_authentication/sessions_controller.rb +1 -1
- data/app/controllers/active_authentication/unlocks_controller.rb +1 -1
- data/app/controllers/active_authentication_controller.rb +2 -0
- data/app/mailers/active_authentication/mailer.rb +6 -0
- data/app/views/active_authentication/magic_links/new.html.erb +14 -0
- data/app/views/active_authentication/mailer/magic_link.html.erb +5 -0
- data/app/views/active_authentication/shared/_links.html.erb +10 -0
- data/config/locales/en.yml +22 -0
- data/config/locales/es.yml +22 -0
- data/lib/active_authentication/controller/authenticatable.rb +53 -0
- data/lib/active_authentication/controller/lockable.rb +13 -13
- data/lib/active_authentication/controller/timeoutable.rb +26 -0
- data/lib/active_authentication/controller/trackable.rb +4 -4
- data/lib/active_authentication/controller.rb +1 -40
- data/lib/active_authentication/engine.rb +7 -5
- data/lib/active_authentication/model/magiclinkable.rb +16 -0
- data/lib/active_authentication/model/omniauthable.rb +11 -0
- data/lib/active_authentication/model/timeoutable.rb +15 -0
- data/lib/active_authentication/model.rb +1 -1
- data/lib/active_authentication/routes.rb +9 -0
- data/lib/active_authentication/version.rb +1 -1
- data/lib/active_authentication.rb +20 -0
- data/lib/generators/active_authentication/install/install_generator.rb +5 -3
- data/lib/generators/active_authentication/install/templates/initializer.rb +15 -0
- data/lib/generators/active_authentication/install/templates/migration.rb +8 -8
- data/lib/generators/active_authentication/omniauthable/omniauthable_generator.rb +13 -0
- data/lib/generators/active_authentication/views/views_generator.rb +1 -1
- metadata +20 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f71840fc4d08d8eb9ec344aeb3f779e276b09c9812e5a380d1e3455d4d4f29b
|
4
|
+
data.tar.gz: 602d85a21c8cea03d629ca0abfd3698344cb9560e376745aea5d964a6e9b9064
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4307d0af7c4d2a319f2714c1ada81b60956f5a41e7e4d11f17a917e33bf2d2bfa7eeff3d41d3c7ded2a846af2eec55937230f8db6c44c548529d4f21bd4a8a2
|
7
|
+
data.tar.gz: f6e78e92c1298baa72ddf0be06f2bd90553fac04ece61813b0d3829eacc0861d5028ff73200131a4b8e4df51155253f8cd016a79b426c20f50be5d558535a933
|
data/README.md
CHANGED
@@ -13,15 +13,16 @@ A pure Rails authentication solution.
|
|
13
13
|
* Authenticatable: provides the standard email/password authentication. It's the only concern that can't be turned off.
|
14
14
|
* Confirmable: allows users to confirm their email addresses.
|
15
15
|
* Lockable: locks users after a number of failed sign in attempts.
|
16
|
+
* MagicLinkable: allows users to sign in with a magic link.
|
17
|
+
* Omniauthable: allows users to sign up and sign in using a third party service through Omniauth. Turned off by default.
|
16
18
|
* Recoverable: allows users to reset their password.
|
17
19
|
* Registerable: allows users to sign up and edit their profile.
|
20
|
+
* Timeoutable: expires sessions after a period of inactivity. Turned off by default.
|
18
21
|
* Trackable: tracks users sign in count, timestamps and ip addresses.
|
19
22
|
|
20
23
|
Planned concerns:
|
21
24
|
|
22
|
-
*
|
23
|
-
* Omniauthable: to allow users to sign up and sign in using a third party service through Omniauth.
|
24
|
-
* Timeoutable: to expire sessions after a period of inactivity.
|
25
|
+
* Invitable: to allow users to invite other users.
|
25
26
|
|
26
27
|
## Installation
|
27
28
|
|
@@ -51,7 +52,13 @@ After installing the gem, you need to generate the `User` model. To generate it,
|
|
51
52
|
$ rails generate active_authentication:install
|
52
53
|
```
|
53
54
|
|
54
|
-
This command will generate the `User` model, add the `active_authentication` route, and generate an initializer (`config/initializers/active_authentication.rb`) where you can configure the concerns.
|
55
|
+
This command will generate the `User` model, add the `active_authentication` route, and generate an initializer (`config/initializers/active_authentication.rb`) where you can configure the concerns. By default, this command enables all concerns. If you want to use a subset of the concerns, you can specify them:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
$ rails generate active_authentication:install confirmable
|
59
|
+
```
|
60
|
+
|
61
|
+
In this example, only the confirmable concern will be enabled (along with authenticatable, which can't be turned off).
|
55
62
|
|
56
63
|
You will need to set up the default url options in your `config/environments/development.rb`:
|
57
64
|
|
@@ -69,13 +76,13 @@ If you look at the `User` model (in `app/models/user.rb`), you will notice there
|
|
69
76
|
|
70
77
|
```ruby
|
71
78
|
class User < ApplicationRecord
|
72
|
-
authenticates_with :confirmable, :lockable, :recoverable, :registerable, :trackable
|
79
|
+
authenticates_with :confirmable, :lockable, :recoverable, :registerable, :timeoutable, :trackable
|
73
80
|
end
|
74
81
|
```
|
75
82
|
|
76
83
|
Notice that `:authenticatable` is not in the list. This is because you cannot turn it off.
|
77
84
|
|
78
|
-
By default, all concerns are turned on. But you can turn
|
85
|
+
By default, all concerns are turned on except omniauthable. But you can turn it on by adding it to the list, and similarly, you can turn any concern off by just removing them from the list. If you plan to not use any concerns, you can replace `authenticates_with` with `authenticates`.
|
79
86
|
|
80
87
|
### Filters and helpers
|
81
88
|
|
@@ -91,12 +98,62 @@ Then, to verify if there's an authenticated user, you can use the `user_signed_i
|
|
91
98
|
|
92
99
|
Similarly, you can use `current_user` to access the current authenticated user.
|
93
100
|
|
101
|
+
If you want to close your application entirely, you can add the before action to your application controller, in conjunction with `active_authentication_controller?`, like this:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
before_action :authenticate_user!, unless: :active_authentication_controller?
|
105
|
+
```
|
106
|
+
|
107
|
+
### Omniauthable
|
108
|
+
|
109
|
+
ActiveAuthentication's implementation of OmniAuth allows you to sign in and/or sign up with your third party accounts or sign up with ActiveAuthentication and later connect your third party accounts to ActiveAuthentication's User. To accomplish this, ActiveAuthentication relies on an `Authentication` model which can be created with the `active_authentication:omniauthable` generator.
|
110
|
+
|
111
|
+
To set up the omniauthable concern you must configure your OmniAuth providers as you would do with plain OmniAuth. There's no OmniAuth config in ActiveAuthentication. For example, in `config/initializers/omniauth.rb` you would set the middleware:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
115
|
+
provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"]
|
116
|
+
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
|
117
|
+
# ... and any other omniauth strategies
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
And then you need to run the omniauthable generator to generate the `Authentication` model:
|
122
|
+
|
123
|
+
```bash
|
124
|
+
$ rails g active_authentication:omniauthable
|
125
|
+
```
|
126
|
+
|
127
|
+
The User model has many Authentication models associated, to allow you to connect your user with multiple third party services if required.
|
128
|
+
|
129
|
+
By adding the `:omniauthable` concern to your `User` model, the following routes will be added to your app:
|
130
|
+
|
131
|
+
* `/auth/:provider` to redirect your users to the provider consent screen
|
132
|
+
* `/auth/:provider/callback` to actually sign in/sign up with the given providers
|
133
|
+
|
134
|
+
The sign in and sign up views will show a link to sign in or sign up with each provider you configured if and only if you set the `ActiveAuthentication.omniauth_providers` setting in your ActiveAuthentication initializer.
|
135
|
+
|
94
136
|
## Customization
|
95
137
|
|
96
138
|
### Concerns configuration
|
97
139
|
|
98
140
|
When you run the `active_authentication:install` generator, an initializer will be copied to your app at `config/initializers/active_authentication.rb`. There's a section per concern where you can configure certain aspects of their behavior.
|
99
141
|
|
142
|
+
### Customize registration and profile params
|
143
|
+
|
144
|
+
If you add extra fields to your User model, you will likely want to allow users to fill in those fields upon registration or when editing their profile. By default, only email, password and password confirmation are allowed. To change this behavior, just add these lines to your `config/initializers/active_authentication.rb` file:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
ActiveAuthentication.configure do |config|
|
148
|
+
config.profile_params = ->(controller) {
|
149
|
+
controller.params.require(:user).permit(:first_name, :email, :last_name, :password, :password_confirmation) # first_name and last_name were added in this example
|
150
|
+
}
|
151
|
+
config.registration_params = config.profile_params
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
We believe that the configuration of a gem should be placed in just one place. For this gem, it's the initializer. The `profile_params` and `registration_params` take a lambda and that lambda receives a controller. Why a lambda? and why does it take a controller? We could have allowed the params to be just an array of symbols instead of the whole `params.require.permit` call, but in edge cases you might want to post-process the required params, or call tap, or whatever. And to be able to call `params.require.permit`, you need to run this lambda in the context of the registrations controller. That's why the lambda receives the controller.
|
156
|
+
|
100
157
|
### Views
|
101
158
|
|
102
159
|
The default views are good enough to get you started, but you'll want to customize them sooner than later. To copy the default views into your app, run the following command:
|
@@ -111,6 +168,26 @@ If you're not using all the concerns, you might want to copy only the views you
|
|
111
168
|
$ rails generate active_authentication:views -v sessions
|
112
169
|
```
|
113
170
|
|
171
|
+
### Omniauthable
|
172
|
+
|
173
|
+
By default, ActiveAuthentication stores the `provider`, `uid` and `auth_data` in the `Authentication` model. There are some cases where you want to store, for example, the first name and last name in the `User` model to avoid digging into the `auth_data` hash each time. Or if you have multiple authentications, you might want to pull first and last name on registration and later allow the user to change them. To pull that data from an Authentication object at sign up, you don't really need to change the controller, instead you can add a callback to your Authentication model, like this:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class Authentication < ApplicationRecord
|
177
|
+
before_validation :update_user_attributes, if: ->(auth) { auth.auth_data.present? && auth.user.present? }
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def update_user_attributes
|
182
|
+
first_name, last_name = auth_data.dig("info", "first_name"), auth_data.dig("info", "last_name")
|
183
|
+
|
184
|
+
user.update first_name: first_name, last_name: last_name
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
Note: this example assumes `first_name:string` and `last_name:string` have been added to the User model and are required. Optional first_name and last_name can be handled similarly.
|
190
|
+
|
114
191
|
## Contributing
|
115
192
|
|
116
193
|
You can open an issue or a PR in GitHub.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class ActiveAuthentication::MagicLinksController < ActiveAuthenticationController
|
2
|
+
before_action :require_no_authentication
|
3
|
+
before_action :set_user, only: :show
|
4
|
+
|
5
|
+
def new
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
@user = User.find_by email: params[:email]
|
10
|
+
@user&.send_magic_link
|
11
|
+
|
12
|
+
redirect_to root_path, notice: t(".success")
|
13
|
+
end
|
14
|
+
|
15
|
+
def show
|
16
|
+
sign_in @user
|
17
|
+
|
18
|
+
redirect_to root_url, notice: t(".success")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def set_user
|
24
|
+
@user = User.find_by_token_for :magic_link, params[:token]
|
25
|
+
|
26
|
+
redirect_to root_url, alert: t(".invalid_token") unless @user.present?
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class ActiveAuthentication::OmniauthCallbacksController < ActiveAuthenticationController
|
2
|
+
def create
|
3
|
+
auth = request.env["omniauth.auth"]
|
4
|
+
provider = auth["provider"]
|
5
|
+
|
6
|
+
@authentication = Authentication.find_or_create_by uid: auth["uid"], provider: provider
|
7
|
+
|
8
|
+
@authentication.update auth_data: auth.as_json
|
9
|
+
|
10
|
+
if user_signed_in?
|
11
|
+
if @authentication.user == current_user
|
12
|
+
redirect_to root_path, notice: t(".already_linked", provider: provider)
|
13
|
+
else
|
14
|
+
@authentication.update user: current_user
|
15
|
+
redirect_to root_path, notice: t(".successfully_linked", provider: provider)
|
16
|
+
end
|
17
|
+
elsif @authentication.user.blank?
|
18
|
+
@user = User.find_or_initialize_by email: auth.dig("info", "email")
|
19
|
+
@user.password = SecureRandom.hex if @user.new_record?
|
20
|
+
|
21
|
+
@authentication.update user: @user
|
22
|
+
|
23
|
+
sign_in @authentication.user
|
24
|
+
redirect_to root_path, notice: t(".successfully_signed_up", provider: provider)
|
25
|
+
else
|
26
|
+
sign_in @authentication.user
|
27
|
+
redirect_to root_path, notice: t("active_authentication.sessions.create.success")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class ActiveAuthentication::RegistrationsController <
|
1
|
+
class ActiveAuthentication::RegistrationsController < ActiveAuthenticationController
|
2
2
|
before_action :require_no_authentication, only: [:new, :create]
|
3
3
|
before_action :authenticate_user!, only: [:edit, :update, :destroy]
|
4
4
|
|
@@ -7,7 +7,7 @@ class ActiveAuthentication::RegistrationsController < ApplicationController
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def create
|
10
|
-
@user = User.new
|
10
|
+
@user = User.new registration_params
|
11
11
|
|
12
12
|
if @user.save
|
13
13
|
sign_in @user
|
@@ -21,7 +21,7 @@ class ActiveAuthentication::RegistrationsController < ApplicationController
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def update
|
24
|
-
if current_user.update(
|
24
|
+
if current_user.update(profile_params)
|
25
25
|
redirect_to edit_profile_path, notice: t(".success")
|
26
26
|
else
|
27
27
|
render :edit, status: :unprocessable_entity
|
@@ -36,7 +36,11 @@ class ActiveAuthentication::RegistrationsController < ApplicationController
|
|
36
36
|
|
37
37
|
private
|
38
38
|
|
39
|
-
def
|
40
|
-
|
39
|
+
def profile_params
|
40
|
+
ActiveAuthentication.profile_params.call self
|
41
|
+
end
|
42
|
+
|
43
|
+
def registration_params
|
44
|
+
ActiveAuthentication.registration_params.call self
|
41
45
|
end
|
42
46
|
end
|
@@ -6,6 +6,12 @@ module ActiveAuthentication
|
|
6
6
|
mail to: @user.unconfirmed_email
|
7
7
|
end
|
8
8
|
|
9
|
+
def magic_link
|
10
|
+
@token, @user = params[:token], params[:user]
|
11
|
+
|
12
|
+
mail to: @user.email
|
13
|
+
end
|
14
|
+
|
9
15
|
def password_reset_instructions
|
10
16
|
@token, @user = params[:token], params[:user]
|
11
17
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<h1 class="text-4xl font-bold"><%=t ".send_magic_link" %></h1>
|
2
|
+
|
3
|
+
<%= form_with url: magic_links_path, data: {turbo: false} do |form| %>
|
4
|
+
<div>
|
5
|
+
<%= form.label :email, t("activerecord.attributes.user.email") %>
|
6
|
+
<%= form.email_field :email %>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<div>
|
10
|
+
<%= form.submit t(".send_magic_link") %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<%= render "active_authentication/shared/links" %>
|
@@ -2,10 +2,20 @@
|
|
2
2
|
<p><%= link_to t(".sign_in"), new_session_path %></p>
|
3
3
|
<% end %>
|
4
4
|
|
5
|
+
<% if controller_name != "magic_links" %>
|
6
|
+
<p><%= link_to t(".send_magic_link"), new_magic_link_path %></p>
|
7
|
+
<% end %>
|
8
|
+
|
5
9
|
<% if User.registerable? && controller_name != "registrations" %>
|
6
10
|
<p><%= link_to t(".sign_up"), new_registration_path %></p>
|
7
11
|
<% end %>
|
8
12
|
|
13
|
+
<% if User.omniauthable? && (controller_name == "sessions" || controller_name == "registrations") %>
|
14
|
+
<% ActiveAuthentication.omniauth_providers.each do |provider| %>
|
15
|
+
<p><%= link_to t(".sign_in_or_sign_up_with", provider: provider), omniauth_path(provider) %></p>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
|
9
19
|
<% if User.recoverable? && controller_name != "registrations" && controller_name != "passwords" %>
|
10
20
|
<p><%= link_to t(".reset_password"), new_password_path %></p>
|
11
21
|
<% end %>
|
data/config/locales/en.yml
CHANGED
@@ -14,13 +14,28 @@ en:
|
|
14
14
|
already_signed_in: You are already signed in.
|
15
15
|
form_errors: "%{errors} prohibited this user from being saved:"
|
16
16
|
locked: Your account has been locked after %{count} failed attempts. Unlock instructions will be sent to your email.
|
17
|
+
timedout: Your session expired. Sign in again to continue.
|
17
18
|
unauthenticated: You need to sign in or sign up before continuing.
|
19
|
+
magic_links:
|
20
|
+
create:
|
21
|
+
success: Magic link will be sent to your email.
|
22
|
+
new:
|
23
|
+
send_magic_link: Send magic link
|
24
|
+
set_user:
|
25
|
+
invalid_token: Magic link is invalid.
|
26
|
+
show:
|
27
|
+
success: Signed in successfully.
|
18
28
|
mailer:
|
19
29
|
email_confirmation_instructions:
|
20
30
|
confirm_email: Confirm my account
|
21
31
|
confirm_email_below: 'You can confirm your account email by clicking the link below:'
|
22
32
|
hello: Hello, %{email}!
|
23
33
|
subject: Confirmation instructions
|
34
|
+
magic_link:
|
35
|
+
hello: Hello, %{email}!
|
36
|
+
magic_link_below: 'You can sign in to your account by clicking the link below:'
|
37
|
+
sign_in: Sign in
|
38
|
+
subject: Magic link
|
24
39
|
password_reset_instructions:
|
25
40
|
hello: Hello, %{email}!
|
26
41
|
reset_password: Reset your password
|
@@ -31,6 +46,11 @@ en:
|
|
31
46
|
subject: Unlock instructions
|
32
47
|
unlock: Unlock your account
|
33
48
|
unlock_below: 'You can unlock your account by clicking the link below:'
|
49
|
+
omniauth_callbacks:
|
50
|
+
create:
|
51
|
+
already_linked: Your %{provider} account has already been linked.
|
52
|
+
successfully_linked: Your %{provider} account has been succesfully linked.
|
53
|
+
successfully_signed_up: You have signed up successfully with %{provider}.
|
34
54
|
passwords:
|
35
55
|
create:
|
36
56
|
success: Password reset instructions will be sent to your email.
|
@@ -70,8 +90,10 @@ en:
|
|
70
90
|
links:
|
71
91
|
reset_password: Reset password
|
72
92
|
send_email_confirmation_instructions: Didn't receive confirmation instructions?
|
93
|
+
send_magic_link: Send me a magic link
|
73
94
|
send_unlock_instructions: Didn't receive unlock instructions?
|
74
95
|
sign_in: Sign in
|
96
|
+
sign_in_or_sign_up_with: Sign in or sign up with %{provider}
|
75
97
|
sign_up: Sign up
|
76
98
|
unlocks:
|
77
99
|
create:
|
data/config/locales/es.yml
CHANGED
@@ -14,13 +14,28 @@ es:
|
|
14
14
|
already_signed_in: Ya iniciaste sesión.
|
15
15
|
form_errors: "%{errors} no permitieron guardar este elemento:"
|
16
16
|
locked: Tu cuenta fue bloqueada después de %{count} intentos fallidos. Te enviaremos un email con las instrucciones de desbloqueo.
|
17
|
+
timedout: Tu sesión expiró. Iniciá sesión nuevamente para continuar.
|
17
18
|
unauthenticated: Tenés que iniciar sesión antes de continuar.
|
19
|
+
magic_links:
|
20
|
+
create:
|
21
|
+
success: Te enviaremos un email con un enlace mágico para iniciar sesión.
|
22
|
+
new:
|
23
|
+
send_magic_link: Enviar enlace mágico
|
24
|
+
set_user:
|
25
|
+
invalid_token: El enlace mágico es inválido.
|
26
|
+
show:
|
27
|
+
success: Inicio de sesión existoso.
|
18
28
|
mailer:
|
19
29
|
email_confirmation_instructions:
|
20
30
|
confirm_email: Confirmar mi cuenta
|
21
31
|
confirm_email_below: 'Podés confirmar tu cuenta haciendo click en el siguiente link:'
|
22
32
|
hello: Hola, %{email}!
|
23
33
|
subject: Instrucciones de confirmación
|
34
|
+
magic_link:
|
35
|
+
hello: Hola, %{email}!
|
36
|
+
magic_link_below: 'Podés iniciar sesión haciendo click en el siguiente link:'
|
37
|
+
sign_in: Iniciar sesión
|
38
|
+
subject: Enlace mágico
|
24
39
|
password_reset_instructions:
|
25
40
|
hello: Hola, %{email}!
|
26
41
|
reset_password: Recuperar contraseña
|
@@ -31,6 +46,11 @@ es:
|
|
31
46
|
subject: Instrucciones de desbloqueo
|
32
47
|
unlock: Desbloquear cuenta
|
33
48
|
unlock_below: 'Podés desbloquear tu contraseña haciendo click en el siguiente link:'
|
49
|
+
omniauth_callbacks:
|
50
|
+
create:
|
51
|
+
already_linked: Tu cuenta de %{provider} ya fue enlazada.
|
52
|
+
successfully_linked: Tu cuenta de %{provider} fue enlazada exitosamente.
|
53
|
+
successfully_signed_up: Te registraste exitosamente con %{provider}.
|
34
54
|
passwords:
|
35
55
|
create:
|
36
56
|
success: Te enviaremos un email con las instrucciones de para recuperar tu contraseña.
|
@@ -70,8 +90,10 @@ es:
|
|
70
90
|
links:
|
71
91
|
reset_password: Recuperar contraseña
|
72
92
|
send_email_confirmation_instructions: "¿No recibiste las instrucciones de confirmación?"
|
93
|
+
send_magic_link: Enviarme un enlace mágico
|
73
94
|
send_unlock_instructions: "¿No recibiste las instrucciones de desbloqueo?"
|
74
95
|
sign_in: Iniciar sesión
|
96
|
+
sign_in_or_sign_up_with: Iniciar sesión o crear una cuenta con %{provider}
|
75
97
|
sign_up: Crear una cuenta
|
76
98
|
unlocks:
|
77
99
|
create:
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ActiveAuthentication
|
2
|
+
module Controller
|
3
|
+
module Authenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
helper_method :current_user
|
8
|
+
helper_method :user_signed_in?
|
9
|
+
end
|
10
|
+
|
11
|
+
def active_authentication_controller?
|
12
|
+
is_a? ::ActiveAuthenticationController
|
13
|
+
end
|
14
|
+
|
15
|
+
def authenticate_user!
|
16
|
+
redirect_to new_session_path, alert: t("active_authentication.failure.unauthenticated") unless user_signed_in?
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_user
|
20
|
+
Current.user ||= user_from_session
|
21
|
+
end
|
22
|
+
|
23
|
+
def require_no_authentication
|
24
|
+
redirect_to root_path, alert: t("active_authentication.failure.already_signed_in") if user_signed_in?
|
25
|
+
end
|
26
|
+
|
27
|
+
def sign_in(user)
|
28
|
+
reset_session
|
29
|
+
Current.user = user
|
30
|
+
session[:user_id] = user.id
|
31
|
+
end
|
32
|
+
|
33
|
+
def sign_out
|
34
|
+
reset_session
|
35
|
+
Current.user = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def user_signed_in?
|
39
|
+
current_user.present?
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def scope
|
45
|
+
User
|
46
|
+
end
|
47
|
+
|
48
|
+
def user_from_session
|
49
|
+
User.find_by id: session[:user_id]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -6,24 +6,24 @@ module ActiveAuthentication
|
|
6
6
|
included do
|
7
7
|
set_callback :failed_sign_in, :before, :increment_failed_attempts
|
8
8
|
set_callback :failed_sign_in, :after, :set_alert
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
+
private
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
def increment_failed_attempts
|
14
|
+
user = User.find_by email: params[:email]
|
15
|
+
user&.increment_failed_attempts
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def scope
|
19
|
+
User.unlocked
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
def set_alert
|
23
|
+
user = User.find_by email: params[:email]
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
25
|
+
if user&.locked?
|
26
|
+
flash[:alert] = t "active_authentication.failure.locked", count: user.failed_attempts
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveAuthentication
|
2
|
+
module Controller
|
3
|
+
module Timeoutable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :sign_out_user_if_timedout, if: :user_signed_in?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def sign_out_user_if_timedout
|
13
|
+
last_request_at = session[:last_request_at].yield_self do |timestamp|
|
14
|
+
Time.at(timestamp).utc if timestamp.present?
|
15
|
+
end
|
16
|
+
|
17
|
+
if current_user.timedout?(last_request_at)
|
18
|
+
sign_out
|
19
|
+
redirect_to root_path, alert: t("active_authentication.failure.timedout")
|
20
|
+
end
|
21
|
+
|
22
|
+
session[:last_request_at] = Time.now.utc.to_i
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,48 +1,9 @@
|
|
1
1
|
module ActiveAuthentication
|
2
2
|
module Controller
|
3
3
|
extend ActiveSupport::Concern
|
4
|
-
include ActiveSupport::Callbacks
|
5
4
|
|
6
5
|
included do
|
7
|
-
|
8
|
-
helper_method :user_signed_in?
|
9
|
-
end
|
10
|
-
|
11
|
-
def authenticate_user!
|
12
|
-
redirect_to new_session_path, alert: t("active_authentication.failure.unauthenticated") unless user_signed_in?
|
13
|
-
end
|
14
|
-
|
15
|
-
def current_user
|
16
|
-
Current.user ||= user_from_session
|
17
|
-
end
|
18
|
-
|
19
|
-
def require_no_authentication
|
20
|
-
redirect_to root_path, alert: t("active_authentication.failure.already_signed_in") if user_signed_in?
|
21
|
-
end
|
22
|
-
|
23
|
-
def sign_in(user)
|
24
|
-
reset_session
|
25
|
-
Current.user = user
|
26
|
-
session[:user_id] = user.id
|
27
|
-
end
|
28
|
-
|
29
|
-
def sign_out
|
30
|
-
reset_session
|
31
|
-
Current.user = nil
|
32
|
-
end
|
33
|
-
|
34
|
-
def user_signed_in?
|
35
|
-
current_user.present?
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def scope
|
41
|
-
User
|
42
|
-
end
|
43
|
-
|
44
|
-
def user_from_session
|
45
|
-
User.find_by id: session[:user_id]
|
6
|
+
include Authenticatable
|
46
7
|
end
|
47
8
|
end
|
48
9
|
end
|
@@ -3,14 +3,16 @@ require "active_authentication/model"
|
|
3
3
|
|
4
4
|
module ActiveAuthentication
|
5
5
|
class Engine < ::Rails::Engine
|
6
|
-
initializer
|
7
|
-
ActiveSupport.on_load :action_controller_base do
|
8
|
-
include ActiveAuthentication::Controller
|
9
|
-
end
|
10
|
-
|
6
|
+
initializer "active_authentication.model" do
|
11
7
|
ActiveSupport.on_load :active_record do
|
12
8
|
include ActiveAuthentication::Model
|
13
9
|
end
|
14
10
|
end
|
11
|
+
|
12
|
+
initializer "active_authentication.controller" do
|
13
|
+
ActiveSupport.on_load :action_controller_base do
|
14
|
+
include ActiveAuthentication::Controller
|
15
|
+
end
|
16
|
+
end
|
15
17
|
end
|
16
18
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveAuthentication
|
2
|
+
module Model
|
3
|
+
module Magiclinkable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
generates_token_for :magic_link, expires_in: ActiveAuthentication.magic_link_token_expires_in
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_magic_link
|
11
|
+
token = generate_token_for :magic_link
|
12
|
+
ActiveAuthentication::Mailer.with(token: token, user: self).magic_link.deliver
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveAuthentication
|
2
|
+
module Model
|
3
|
+
module Timeoutable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
ApplicationController.send :include, ActiveAuthentication::Controller::Timeoutable
|
8
|
+
end
|
9
|
+
|
10
|
+
def timedout?(last_request_at)
|
11
|
+
last_request_at && last_request_at <= ActiveAuthentication.timeout_in.ago
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -2,7 +2,7 @@ module ActiveAuthentication
|
|
2
2
|
module Model
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
CONCERNS = %i[authenticatable confirmable lockable recoverable registerable trackable]
|
5
|
+
CONCERNS = %i[authenticatable confirmable lockable magiclinkable omniauthable recoverable registerable timeoutable trackable]
|
6
6
|
|
7
7
|
class_methods do
|
8
8
|
def authenticates_with(*concerns)
|
@@ -22,6 +22,15 @@ module ActionDispatch::Routing
|
|
22
22
|
resources :unlocks, param: :token, only: [:new, :create, :show]
|
23
23
|
end
|
24
24
|
|
25
|
+
def magiclinkable
|
26
|
+
resources :magic_links, param: :token, only: [:new, :create, :show]
|
27
|
+
end
|
28
|
+
|
29
|
+
def omniauthable
|
30
|
+
get "auth/:provider", to: "omniauth_callbacks#pass", as: :omniauth
|
31
|
+
get "auth/:provider/callback", to: "omniauth_callbacks#create", as: :omniauth_callback
|
32
|
+
end
|
33
|
+
|
25
34
|
def registerable
|
26
35
|
resources :registrations, only: [:new, :create]
|
27
36
|
resource :profile, only: [:edit, :update, :destroy], path: :profile, controller: :registrations
|
@@ -10,7 +10,9 @@ module ActiveAuthentication
|
|
10
10
|
autoload :Model, "active_authentication/model"
|
11
11
|
|
12
12
|
module Controller
|
13
|
+
autoload :Authenticatable, "active_authentication/controller/authenticatable"
|
13
14
|
autoload :Lockable, "active_authentication/controller/lockable"
|
15
|
+
autoload :Timeoutable, "active_authentication/controller/timeoutable"
|
14
16
|
autoload :Trackable, "active_authentication/controller/trackable"
|
15
17
|
end
|
16
18
|
|
@@ -18,8 +20,11 @@ module ActiveAuthentication
|
|
18
20
|
autoload :Authenticatable, "active_authentication/model/authenticatable"
|
19
21
|
autoload :Confirmable, "active_authentication/model/confirmable"
|
20
22
|
autoload :Lockable, "active_authentication/model/lockable"
|
23
|
+
autoload :Magiclinkable, "active_authentication/model/magiclinkable"
|
24
|
+
autoload :Omniauthable, "active_authentication/model/omniauthable"
|
21
25
|
autoload :Recoverable, "active_authentication/model/recoverable"
|
22
26
|
autoload :Registerable, "active_authentication/model/registerable"
|
27
|
+
autoload :Timeoutable, "active_authentication/model/timeoutable"
|
23
28
|
autoload :Trackable, "active_authentication/model/trackable"
|
24
29
|
end
|
25
30
|
|
@@ -37,6 +42,21 @@ module ActiveAuthentication
|
|
37
42
|
config_accessor :unlock_token_expires_in, default: 24.hours
|
38
43
|
config_accessor :max_failed_attempts, default: 10
|
39
44
|
|
45
|
+
# magiclinkable
|
46
|
+
config_accessor :magic_link_token_expires_in, default: 24.hours
|
47
|
+
|
48
|
+
# omniauthable
|
49
|
+
config_accessor :omniauth_providers, default: []
|
50
|
+
|
40
51
|
# recoverable
|
41
52
|
config_accessor :password_reset_token_expires_in, default: 1.hour
|
53
|
+
|
54
|
+
# registerable
|
55
|
+
config_accessor :profile_params, default: ->(controller) {
|
56
|
+
controller.params.require(:user).permit(:email, :password, :password_confirmation)
|
57
|
+
}
|
58
|
+
config_accessor :registration_params, default: profile_params
|
59
|
+
|
60
|
+
# timeoutable
|
61
|
+
config_accessor :timeout_in, default: 30.minutes
|
42
62
|
end
|
@@ -3,6 +3,8 @@ class ActiveAuthentication::InstallGenerator < Rails::Generators::Base
|
|
3
3
|
|
4
4
|
source_root File.expand_path("templates", __dir__)
|
5
5
|
|
6
|
+
argument :concerns, type: :array, default: %w[confirmable lockable recoverable registerable trackable], banner: "concern concern"
|
7
|
+
|
6
8
|
desc "Creates the User model, the active_authentication initializer, and adds the active_authentication route."
|
7
9
|
|
8
10
|
def self.next_migration_number(dirname)
|
@@ -13,12 +15,12 @@ class ActiveAuthentication::InstallGenerator < Rails::Generators::Base
|
|
13
15
|
invoke "active_record:model", %w[User], migration: false, skip_collision_check: true
|
14
16
|
|
15
17
|
if behavior == :invoke
|
16
|
-
inject_into_class "app/models/user.rb", "User", " authenticates_with
|
18
|
+
inject_into_class "app/models/user.rb", "User", " authenticates_with #{concerns.map { ":#{_1}" }.join(", ")}\n"
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def generate_migration
|
21
|
-
migration_template "migration.rb", "db/migrate/create_users.rb", migration_version: migration_version, ip_column: ip_column
|
23
|
+
migration_template "migration.rb", "db/migrate/create_users.rb", concerns: concerns, migration_version: migration_version, ip_column: ip_column
|
22
24
|
end
|
23
25
|
|
24
26
|
def add_route
|
@@ -48,6 +50,6 @@ class ActiveAuthentication::InstallGenerator < Rails::Generators::Base
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def postgresql?
|
51
|
-
ar_config.present? && ar_config[
|
53
|
+
ar_config.present? && ar_config.with_indifferent_access[:adapter] == "postgresql"
|
52
54
|
end
|
53
55
|
end
|
@@ -9,6 +9,21 @@ ActiveAuthentication.configure do |config|
|
|
9
9
|
# config.unlock_token_expires_in = 24.hours
|
10
10
|
# config.max_failed_attempts = 10
|
11
11
|
|
12
|
+
# configuration for the magiclinkable concern
|
13
|
+
# config.magic_link_token_expires_in = 24.hours
|
14
|
+
|
12
15
|
# configuration for the recoverable concern
|
13
16
|
# config.password_reset_token_expires_in = 1.hour
|
17
|
+
|
18
|
+
# configuration for the registerable concern
|
19
|
+
# config_accessor :profile_params, default: ->(controller) {
|
20
|
+
# controller.params.require(:user).permit(:email, :password, :password_confirmation)
|
21
|
+
# }
|
22
|
+
# by default the registration_params take the same lambda as the profile_params. if you redefine the
|
23
|
+
# profile_params setting, you will need to uncomment this line as otherwise it will take the default value,
|
24
|
+
# previously defined.
|
25
|
+
# config_accessor :registration_params, default: profile_params
|
26
|
+
|
27
|
+
# configuration for the timeoutable concern
|
28
|
+
# config.timeout_in = 30.minutes
|
14
29
|
end
|
@@ -6,18 +6,18 @@ class CreateUsers < ActiveRecord::Migration<%= migration_version %>
|
|
6
6
|
t.string :password_digest, null: false
|
7
7
|
|
8
8
|
# confirmable
|
9
|
-
t.string :unconfirmed_email
|
9
|
+
<%= "# " unless concerns.include? "confirmable" -%>t.string :unconfirmed_email
|
10
10
|
|
11
11
|
# lockable
|
12
|
-
t.integer :failed_attempts, null: false, default: 0
|
13
|
-
t.datetime :locked_at
|
12
|
+
<%= "# " unless concerns.include? "lockable" -%>t.integer :failed_attempts, null: false, default: 0
|
13
|
+
<%= "# " unless concerns.include? "lockable" -%>t.datetime :locked_at
|
14
14
|
|
15
15
|
# trackable
|
16
|
-
t.integer :sign_in_count, null: false, default: 0
|
17
|
-
t.datetime :current_sign_in_at
|
18
|
-
t.datetime :last_sign_in_at
|
19
|
-
t.<%= ip_column %> :current_sign_in_ip
|
20
|
-
t.<%= ip_column %> :last_sign_in_ip
|
16
|
+
<%= "# " unless concerns.include? "trackable" -%>t.integer :sign_in_count, null: false, default: 0
|
17
|
+
<%= "# " unless concerns.include? "trackable" -%>t.datetime :current_sign_in_at
|
18
|
+
<%= "# " unless concerns.include? "trackable" -%>t.datetime :last_sign_in_at
|
19
|
+
<%= "# " unless concerns.include? "trackable" -%>t.<%= ip_column %> :current_sign_in_ip
|
20
|
+
<%= "# " unless concerns.include? "trackable" -%>t.<%= ip_column %> :last_sign_in_ip
|
21
21
|
|
22
22
|
t.timestamps
|
23
23
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ActiveAuthentication::OmniauthableGenerator < Rails::Generators::Base
|
2
|
+
source_root File.expand_path("templates", __dir__)
|
3
|
+
|
4
|
+
desc "Creates the Authentication model"
|
5
|
+
|
6
|
+
def generate_model
|
7
|
+
invoke "active_record:model", %w[Authentication uid:string provider:string user:references auth_data:json], skip_collision_check: true
|
8
|
+
|
9
|
+
if behavior == :invoke
|
10
|
+
inject_into_class "app/models/authentication.rb", "Authentication", " validates :provider, presence: true\n validates :uid, presence: true, uniqueness: {scope: :provider}\n"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -3,7 +3,7 @@ class ActiveAuthentication::ViewsGenerator < Rails::Generators::Base
|
|
3
3
|
|
4
4
|
desc "Generates active_authentication's views."
|
5
5
|
|
6
|
-
class_option :views, aliases: "-v", type: :array, desc: "Views to generate (available: confirmations, mailer, passwords, registrations, sessions, shared, unlocks)"
|
6
|
+
class_option :views, aliases: "-v", type: :array, desc: "Views to generate (available: confirmations, magic_links, mailer, passwords, registrations, sessions, shared, unlocks)"
|
7
7
|
|
8
8
|
def copy_views
|
9
9
|
if options[:views]
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_authentication
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patricio Mac Adden
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '7.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '7.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -49,13 +49,18 @@ files:
|
|
49
49
|
- README.md
|
50
50
|
- Rakefile
|
51
51
|
- app/controllers/active_authentication/confirmations_controller.rb
|
52
|
+
- app/controllers/active_authentication/magic_links_controller.rb
|
53
|
+
- app/controllers/active_authentication/omniauth_callbacks_controller.rb
|
52
54
|
- app/controllers/active_authentication/passwords_controller.rb
|
53
55
|
- app/controllers/active_authentication/registrations_controller.rb
|
54
56
|
- app/controllers/active_authentication/sessions_controller.rb
|
55
57
|
- app/controllers/active_authentication/unlocks_controller.rb
|
58
|
+
- app/controllers/active_authentication_controller.rb
|
56
59
|
- app/mailers/active_authentication/mailer.rb
|
57
60
|
- app/views/active_authentication/confirmations/new.html.erb
|
61
|
+
- app/views/active_authentication/magic_links/new.html.erb
|
58
62
|
- app/views/active_authentication/mailer/email_confirmation_instructions.html.erb
|
63
|
+
- app/views/active_authentication/mailer/magic_link.html.erb
|
59
64
|
- app/views/active_authentication/mailer/password_reset_instructions.html.erb
|
60
65
|
- app/views/active_authentication/mailer/unlock_instructions.html.erb
|
61
66
|
- app/views/active_authentication/passwords/edit.html.erb
|
@@ -70,7 +75,9 @@ files:
|
|
70
75
|
- config/locales/es.yml
|
71
76
|
- lib/active_authentication.rb
|
72
77
|
- lib/active_authentication/controller.rb
|
78
|
+
- lib/active_authentication/controller/authenticatable.rb
|
73
79
|
- lib/active_authentication/controller/lockable.rb
|
80
|
+
- lib/active_authentication/controller/timeoutable.rb
|
74
81
|
- lib/active_authentication/controller/trackable.rb
|
75
82
|
- lib/active_authentication/current.rb
|
76
83
|
- lib/active_authentication/engine.rb
|
@@ -78,8 +85,11 @@ files:
|
|
78
85
|
- lib/active_authentication/model/authenticatable.rb
|
79
86
|
- lib/active_authentication/model/confirmable.rb
|
80
87
|
- lib/active_authentication/model/lockable.rb
|
88
|
+
- lib/active_authentication/model/magiclinkable.rb
|
89
|
+
- lib/active_authentication/model/omniauthable.rb
|
81
90
|
- lib/active_authentication/model/recoverable.rb
|
82
91
|
- lib/active_authentication/model/registerable.rb
|
92
|
+
- lib/active_authentication/model/timeoutable.rb
|
83
93
|
- lib/active_authentication/model/trackable.rb
|
84
94
|
- lib/active_authentication/routes.rb
|
85
95
|
- lib/active_authentication/test/helpers.rb
|
@@ -87,6 +97,7 @@ files:
|
|
87
97
|
- lib/generators/active_authentication/install/install_generator.rb
|
88
98
|
- lib/generators/active_authentication/install/templates/initializer.rb
|
89
99
|
- lib/generators/active_authentication/install/templates/migration.rb
|
100
|
+
- lib/generators/active_authentication/omniauthable/omniauthable_generator.rb
|
90
101
|
- lib/generators/active_authentication/views/views_generator.rb
|
91
102
|
homepage: https://github.com/sinaptia/active_authentication
|
92
103
|
licenses:
|
@@ -94,7 +105,8 @@ licenses:
|
|
94
105
|
metadata:
|
95
106
|
homepage_uri: https://github.com/sinaptia/active_authentication
|
96
107
|
source_code_uri: https://github.com/sinaptia/active_authentication
|
97
|
-
|
108
|
+
changelog_uri: https://github.com/sinaptia/active_authentication/blob/main/CHANGELOG.md
|
109
|
+
post_install_message:
|
98
110
|
rdoc_options: []
|
99
111
|
require_paths:
|
100
112
|
- lib
|
@@ -109,8 +121,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
121
|
- !ruby/object:Gem::Version
|
110
122
|
version: '0'
|
111
123
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
113
|
-
signing_key:
|
124
|
+
rubygems_version: 3.5.22
|
125
|
+
signing_key:
|
114
126
|
specification_version: 4
|
115
127
|
summary: A pure Rails authentication solution
|
116
128
|
test_files: []
|