rails_authentication 0.1.0
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/README.md +108 -0
- data/lib/generators/authentication/authentication_generator.rb +126 -0
- data/lib/generators/authentication/features/confirmable.rb +25 -0
- data/lib/generators/authentication/features/invitable.rb +26 -0
- data/lib/generators/authentication/features/lockable.rb +25 -0
- data/lib/generators/authentication/features/recoverable.rb +25 -0
- data/lib/generators/authentication/features/registerable.rb +17 -0
- data/lib/generators/authentication/features/rememberable.rb +16 -0
- data/lib/generators/authentication/features/timeoutable.rb +17 -0
- data/lib/generators/authentication/features/trackable.rb +17 -0
- data/lib/generators/authentication/features/validatable.rb +15 -0
- data/lib/generators/authentication/templates/app/controllers/concerns/authentication.rb.tt +96 -0
- data/lib/generators/authentication/templates/app/controllers/confirmations_controller.rb.tt +27 -0
- data/lib/generators/authentication/templates/app/controllers/invitations_controller.rb.tt +36 -0
- data/lib/generators/authentication/templates/app/controllers/passwords_controller.rb.tt +39 -0
- data/lib/generators/authentication/templates/app/controllers/registrations_controller.rb.tt +56 -0
- data/lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt +64 -0
- data/lib/generators/authentication/templates/app/controllers/unlocks_controller.rb.tt +23 -0
- data/lib/generators/authentication/templates/app/mailers/confirmations_mailer.rb.tt +10 -0
- data/lib/generators/authentication/templates/app/mailers/invitations_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/mailers/passwords_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/mailers/unlocks_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/models/concerns/confirmable_concern.rb.tt +74 -0
- data/lib/generators/authentication/templates/app/models/concerns/invitable_concern.rb.tt +86 -0
- data/lib/generators/authentication/templates/app/models/concerns/lockable_concern.rb.tt +48 -0
- data/lib/generators/authentication/templates/app/models/concerns/recoverable_concern.rb.tt +33 -0
- data/lib/generators/authentication/templates/app/models/concerns/rememberable_concern.rb.tt +15 -0
- data/lib/generators/authentication/templates/app/models/concerns/timeoutable_concern.rb.tt +9 -0
- data/lib/generators/authentication/templates/app/models/concerns/trackable_concern.rb.tt +7 -0
- data/lib/generators/authentication/templates/app/models/concerns/validatable_concern.rb.tt +40 -0
- data/lib/generators/authentication/templates/app/models/user_auth.rb.tt +18 -0
- data/lib/generators/authentication/templates/app/views/confirmations/new.html.erb.tt +8 -0
- data/lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/invitations/edit.html.erb.tt +9 -0
- data/lib/generators/authentication/templates/app/views/invitations/new.html.erb.tt +9 -0
- data/lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/passwords_mailer/reset.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/registrations/edit.html.erb.tt +22 -0
- data/lib/generators/authentication/templates/app/views/registrations/new.html.erb.tt +21 -0
- data/lib/generators/authentication/templates/app/views/sessions/new.html.erb.tt +23 -0
- data/lib/generators/authentication/templates/app/views/unlocks/new.html.erb.tt +8 -0
- data/lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.text.erb.tt +6 -0
- data/lib/generators/authentication/templates/db/migrate/add_confirmable_to_users.rb.tt +12 -0
- data/lib/generators/authentication/templates/db/migrate/add_invitable_to_users.rb.tt +12 -0
- data/lib/generators/authentication/templates/db/migrate/add_lockable_to_users.rb.tt +9 -0
- data/lib/generators/authentication/templates/db/migrate/add_recoverable_to_users.rb.tt +8 -0
- data/lib/generators/authentication/templates/db/migrate/add_rememberable_to_users.rb.tt +5 -0
- data/lib/generators/authentication/templates/db/migrate/create_user_auths.rb.tt +14 -0
- data/lib/rails_authentication/version.rb +5 -0
- data/lib/rails_authentication.rb +6 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 305e121fa18342571e89ab2bee6cf11f34ba708c08c2c17fb85e5880b2f0ab56
|
|
4
|
+
data.tar.gz: b62fec7dddeaf09e159321f396b2723f3b5793f7c71fc72af3044c49987e6c86
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b35c71dd413c7e12398bdfd2b04f78e2e5669484254af2c9125958a69072ff2904d41bf858800f3853a48fb5aac20616425ae438407df8ed9045db742e6d0d8e
|
|
7
|
+
data.tar.gz: dfa8c8b76bff7b8d907bda610db59b3fd8fbbf580722f659f20cef9c2153b6ea56f69d531a879dded44d94820748074bb88f8da4f2397c61c2c3e778e68ff47a
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# rails_authentication
|
|
2
|
+
|
|
3
|
+
Devise-style features on top of Rails 8's built-in authentication generator.
|
|
4
|
+
|
|
5
|
+
Rails 8's `bin/rails generate authentication` gives you the essentials: a `User` model with
|
|
6
|
+
`has_secure_password`, database-backed sessions, sign-in/sign-out, and a password reset flow. This
|
|
7
|
+
gem extends that same command to also install:
|
|
8
|
+
|
|
9
|
+
| Feature | What it does |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **Confirmable** | Emails confirmation instructions; blocks sign-in until the email address is confirmed |
|
|
12
|
+
| **Recoverable** | Password reset with DB-backed, revocable, expiring tokens (replaces the base stateless tokens) |
|
|
13
|
+
| **Registerable** | Sign-up, plus account editing and deletion |
|
|
14
|
+
| **Rememberable** | "Remember me" checkbox — persistent cookie only when checked |
|
|
15
|
+
| **Trackable** | Records every sign-in attempt (success and failure) in a `user_auths` table: IP, user agent, referrer, failure reason |
|
|
16
|
+
| **Timeoutable** | Expires sessions after 30 minutes of inactivity |
|
|
17
|
+
| **Validatable** | Email format/uniqueness, password length, and password complexity validations |
|
|
18
|
+
| **Lockable** | Locks the account after 5 failed attempts; unlock via email or automatically after 1 hour |
|
|
19
|
+
| **Invitable** | Invite users by email; they accept by choosing their own password |
|
|
20
|
+
|
|
21
|
+
Everything is **generated into your app** as plain, readable code — controllers, views, mailers,
|
|
22
|
+
migrations, and one model concern per feature. There is no runtime dependency on this gem: after
|
|
23
|
+
generating, the code is yours to edit.
|
|
24
|
+
|
|
25
|
+
This gem is meant to be installed temporarily. Install it long enough to run the generators and uninstall it after. If you keep it installed, keep it in the `development` group.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Requires Rails >= 8.0 and, for the email-driven features (Confirmable, Recoverable, Lockable,
|
|
30
|
+
Invitable), Action Mailer.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Gemfile
|
|
34
|
+
group :development do
|
|
35
|
+
gem "rails_authentication"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
bin/rails generate authentication
|
|
43
|
+
bin/rails db:migrate
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That single command runs Rails' built-in authentication generator first, then layers every feature
|
|
47
|
+
on top. Skip any feature you don't want:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
bin/rails generate authentication --skip-invitable --skip-trackable
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Available flags: `--skip-confirmable`, `--skip-recoverable`, `--skip-registerable`,
|
|
54
|
+
`--skip-rememberable`, `--skip-trackable`, `--skip-timeoutable`, `--skip-validatable`,
|
|
55
|
+
`--skip-lockable`, `--skip-invitable`, and `--reconfirmable` (Confirmable: postpone email address
|
|
56
|
+
changes until the new address is confirmed, via an `unconfirmed_email` column).
|
|
57
|
+
|
|
58
|
+
Each feature adds a single `include <Feature>Concern` line to `app/models/user.rb`; all of its
|
|
59
|
+
model behavior lives in `app/models/concerns/<feature>_concern.rb`. Tunables are plain constants in
|
|
60
|
+
the generated concerns — e.g. `TimeoutableConcern::TIMEOUT_IN`, `LockableConcern::MAXIMUM_ATTEMPTS`,
|
|
61
|
+
`LockableConcern::UNLOCK_IN`, `ConfirmableConcern::CONFIRMATION_TOKEN_EXPIRES_IN`,
|
|
62
|
+
`ValidatableConcern::PASSWORD_LENGTH`, `ValidatableConcern::PASSWORD_MINIMUM_COMPLEXITY` — edit them
|
|
63
|
+
there. Concerns keep the model clean.
|
|
64
|
+
|
|
65
|
+
### Generated routes
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
resource :registration, only: %i[ new create edit update destroy ]
|
|
69
|
+
resources :confirmations, only: %i[ new create show ], param: :token
|
|
70
|
+
resources :unlocks, only: %i[ new create show ], param: :token
|
|
71
|
+
resources :invitations, only: %i[ new create edit update ], param: :token
|
|
72
|
+
resource :session # from the base generator
|
|
73
|
+
resources :passwords, param: :token # from the base generator
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Notes
|
|
77
|
+
|
|
78
|
+
- **Existing users + Confirmable**: sign-in requires `confirmed_at`, so backfill it when adding
|
|
79
|
+
Confirmable to an app with existing users:
|
|
80
|
+
`User.update_all(confirmed_at: Time.current)`.
|
|
81
|
+
- **Recoverable** replaces the base generator's `PasswordsController`/`PasswordsMailer` (which use
|
|
82
|
+
stateless signed tokens) with DB-token versions. If you `--skip-recoverable`, the base flow is
|
|
83
|
+
left untouched.
|
|
84
|
+
- The generator overwrites the base generator's `SessionsController`, `Authentication` concern, and
|
|
85
|
+
sessions view with versions tailored to the features you selected. Run the generator once, up
|
|
86
|
+
front — re-running it after you've customized those files will prompt to overwrite them.
|
|
87
|
+
|
|
88
|
+
## Future Plans
|
|
89
|
+
|
|
90
|
+
- OTP (Google Authenticator)
|
|
91
|
+
- Passwordless (code emailed)
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
bundle install
|
|
97
|
+
bundle exec rake spec:generators # generator specs (fast, no app boot)
|
|
98
|
+
bundle exec rake dummy:prepare # build spec/dummy by running the real generator
|
|
99
|
+
bundle exec rake spec:requests # request specs against spec/dummy
|
|
100
|
+
bundle exec rspec spec/generators/lockable_spec.rb:12 # a single example
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`spec/dummy` is generated (and gitignored), never hand-maintained, so the request specs always
|
|
104
|
+
exercise exactly what the templates produce.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
require_relative "features/confirmable"
|
|
6
|
+
require_relative "features/recoverable"
|
|
7
|
+
require_relative "features/registerable"
|
|
8
|
+
require_relative "features/rememberable"
|
|
9
|
+
require_relative "features/trackable"
|
|
10
|
+
require_relative "features/timeoutable"
|
|
11
|
+
require_relative "features/validatable"
|
|
12
|
+
require_relative "features/lockable"
|
|
13
|
+
require_relative "features/invitable"
|
|
14
|
+
|
|
15
|
+
module RailsAuthentication
|
|
16
|
+
module Generators
|
|
17
|
+
# Shadows Rails' built-in `bin/rails generate authentication` command: the explicit
|
|
18
|
+
# `authentication:authentication` namespace is checked before Rails' own
|
|
19
|
+
# `rails:authentication`, so the bare `authentication` invocation lands here.
|
|
20
|
+
# It runs the built-in generator first, then layers the feature set on top.
|
|
21
|
+
#
|
|
22
|
+
# The Ruby namespace is RailsAuthentication (not Authentication, which would give the
|
|
23
|
+
# same Thor namespace implicitly) because Rails 8's base generator creates an
|
|
24
|
+
# `Authentication` controller concern in the host app — a top-level constant this gem
|
|
25
|
+
# must not squat on.
|
|
26
|
+
class AuthenticationGenerator < Rails::Generators::Base
|
|
27
|
+
namespace "authentication:authentication"
|
|
28
|
+
|
|
29
|
+
FEATURES = %i[
|
|
30
|
+
confirmable recoverable registerable rememberable trackable
|
|
31
|
+
timeoutable validatable lockable invitable
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
include ActiveRecord::Generators::Migration
|
|
35
|
+
|
|
36
|
+
include Features::Confirmable
|
|
37
|
+
include Features::Recoverable
|
|
38
|
+
include Features::Registerable
|
|
39
|
+
include Features::Rememberable
|
|
40
|
+
include Features::Trackable
|
|
41
|
+
include Features::Timeoutable
|
|
42
|
+
include Features::Validatable
|
|
43
|
+
include Features::Lockable
|
|
44
|
+
include Features::Invitable
|
|
45
|
+
|
|
46
|
+
source_root File.expand_path("templates", __dir__)
|
|
47
|
+
|
|
48
|
+
FEATURES.each do |feature|
|
|
49
|
+
class_option :"skip_#{feature}", type: :boolean, default: false,
|
|
50
|
+
desc: "Skip #{feature}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class_option :reconfirmable, type: :boolean, default: false,
|
|
54
|
+
desc: "Confirmable: postpone email address changes until reconfirmed (adds unconfirmed_email column)"
|
|
55
|
+
|
|
56
|
+
def install_base_authentication
|
|
57
|
+
say "Running Rails' built-in authentication generator", :green
|
|
58
|
+
Rails::Generators.invoke("rails:authentication", [], behavior: behavior, destination_root: destination_root)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def install_validatable
|
|
62
|
+
generate_validatable if validatable?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def install_registerable
|
|
66
|
+
generate_registerable if registerable?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def install_recoverable
|
|
70
|
+
generate_recoverable if recoverable?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def install_confirmable
|
|
74
|
+
generate_confirmable if confirmable?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def install_rememberable
|
|
78
|
+
generate_rememberable if rememberable?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def install_trackable
|
|
82
|
+
generate_trackable if trackable?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def install_timeoutable
|
|
86
|
+
generate_timeoutable if timeoutable?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def install_lockable
|
|
90
|
+
generate_lockable if lockable?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def install_invitable
|
|
94
|
+
generate_invitable if invitable?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Confirmable, Rememberable, Trackable, Timeoutable, and Lockable all hook into the
|
|
98
|
+
# sign-in flow, so the base generator's session files are replaced with versions
|
|
99
|
+
# rendered from the enabled feature set. Overwriting is safe: the base copies were
|
|
100
|
+
# written moments ago by this same run.
|
|
101
|
+
def customize_session_layer
|
|
102
|
+
template "app/controllers/sessions_controller.rb", force: true
|
|
103
|
+
template "app/controllers/concerns/authentication.rb", force: true
|
|
104
|
+
template "app/views/sessions/new.html.erb", force: true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
no_commands do
|
|
108
|
+
FEATURES.each do |feature|
|
|
109
|
+
define_method(:"#{feature}?") { !options[:"skip_#{feature}"] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def reconfirmable?
|
|
113
|
+
confirmable? && options[:reconfirmable]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def include_concern_in_user(concern)
|
|
117
|
+
inject_into_class "app/models/user.rb", "User", " include #{concern}\n"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def migration_version
|
|
121
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Confirmable
|
|
7
|
+
private
|
|
8
|
+
def generate_confirmable
|
|
9
|
+
template "app/models/concerns/confirmable_concern.rb"
|
|
10
|
+
include_concern_in_user "ConfirmableConcern"
|
|
11
|
+
migration_template "db/migrate/add_confirmable_to_users.rb", "db/migrate/add_confirmable_to_users.rb"
|
|
12
|
+
template "app/controllers/confirmations_controller.rb"
|
|
13
|
+
template "app/views/confirmations/new.html.erb"
|
|
14
|
+
route "resources :confirmations, only: %i[ new create show ], param: :token"
|
|
15
|
+
|
|
16
|
+
if defined?(ActionMailer::Railtie)
|
|
17
|
+
template "app/mailers/confirmations_mailer.rb"
|
|
18
|
+
template "app/views/confirmations_mailer/confirmation_instructions.html.erb"
|
|
19
|
+
template "app/views/confirmations_mailer/confirmation_instructions.text.erb"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Invitable
|
|
7
|
+
private
|
|
8
|
+
def generate_invitable
|
|
9
|
+
template "app/models/concerns/invitable_concern.rb"
|
|
10
|
+
include_concern_in_user "InvitableConcern"
|
|
11
|
+
migration_template "db/migrate/add_invitable_to_users.rb", "db/migrate/add_invitable_to_users.rb"
|
|
12
|
+
template "app/controllers/invitations_controller.rb"
|
|
13
|
+
template "app/views/invitations/new.html.erb"
|
|
14
|
+
template "app/views/invitations/edit.html.erb"
|
|
15
|
+
route "resources :invitations, only: %i[ new create edit update ], param: :token"
|
|
16
|
+
|
|
17
|
+
if defined?(ActionMailer::Railtie)
|
|
18
|
+
template "app/mailers/invitations_mailer.rb"
|
|
19
|
+
template "app/views/invitations_mailer/invitation_instructions.html.erb"
|
|
20
|
+
template "app/views/invitations_mailer/invitation_instructions.text.erb"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Lockable
|
|
7
|
+
private
|
|
8
|
+
def generate_lockable
|
|
9
|
+
template "app/models/concerns/lockable_concern.rb"
|
|
10
|
+
include_concern_in_user "LockableConcern"
|
|
11
|
+
migration_template "db/migrate/add_lockable_to_users.rb", "db/migrate/add_lockable_to_users.rb"
|
|
12
|
+
template "app/controllers/unlocks_controller.rb"
|
|
13
|
+
template "app/views/unlocks/new.html.erb"
|
|
14
|
+
route "resources :unlocks, only: %i[ new create show ], param: :token"
|
|
15
|
+
|
|
16
|
+
if defined?(ActionMailer::Railtie)
|
|
17
|
+
template "app/mailers/unlocks_mailer.rb"
|
|
18
|
+
template "app/views/unlocks_mailer/unlock_instructions.html.erb"
|
|
19
|
+
template "app/views/unlocks_mailer/unlock_instructions.text.erb"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Recoverable
|
|
7
|
+
private
|
|
8
|
+
# Replaces the base generator's stateless generates_token_for password reset
|
|
9
|
+
# with DB-backed, revocable tokens.
|
|
10
|
+
def generate_recoverable
|
|
11
|
+
template "app/models/concerns/recoverable_concern.rb"
|
|
12
|
+
include_concern_in_user "RecoverableConcern"
|
|
13
|
+
migration_template "db/migrate/add_recoverable_to_users.rb", "db/migrate/add_recoverable_to_users.rb"
|
|
14
|
+
template "app/controllers/passwords_controller.rb", force: true
|
|
15
|
+
|
|
16
|
+
if defined?(ActionMailer::Railtie)
|
|
17
|
+
template "app/mailers/passwords_mailer.rb", force: true
|
|
18
|
+
template "app/views/passwords_mailer/reset.html.erb", force: true
|
|
19
|
+
template "app/views/passwords_mailer/reset.text.erb", force: true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Registerable
|
|
7
|
+
private
|
|
8
|
+
def generate_registerable
|
|
9
|
+
template "app/controllers/registrations_controller.rb"
|
|
10
|
+
template "app/views/registrations/new.html.erb"
|
|
11
|
+
template "app/views/registrations/edit.html.erb"
|
|
12
|
+
route "resource :registration, only: %i[ new create edit update destroy ]"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Rememberable
|
|
7
|
+
private
|
|
8
|
+
def generate_rememberable
|
|
9
|
+
template "app/models/concerns/rememberable_concern.rb"
|
|
10
|
+
include_concern_in_user "RememberableConcern"
|
|
11
|
+
migration_template "db/migrate/add_rememberable_to_users.rb", "db/migrate/add_rememberable_to_users.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Timeoutable
|
|
7
|
+
private
|
|
8
|
+
# No migration: inactivity is measured against sessions.updated_at, which the
|
|
9
|
+
# base generator's sessions table already has.
|
|
10
|
+
def generate_timeoutable
|
|
11
|
+
template "app/models/concerns/timeoutable_concern.rb"
|
|
12
|
+
include_concern_in_user "TimeoutableConcern"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Trackable
|
|
7
|
+
private
|
|
8
|
+
def generate_trackable
|
|
9
|
+
template "app/models/concerns/trackable_concern.rb"
|
|
10
|
+
include_concern_in_user "TrackableConcern"
|
|
11
|
+
template "app/models/user_auth.rb"
|
|
12
|
+
migration_template "db/migrate/create_user_auths.rb", "db/migrate/create_user_auths.rb"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
module Validatable
|
|
7
|
+
private
|
|
8
|
+
def generate_validatable
|
|
9
|
+
template "app/models/concerns/validatable_concern.rb"
|
|
10
|
+
include_concern_in_user "ValidatableConcern"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Authentication
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
before_action :require_authentication
|
|
6
|
+
helper_method :authenticated?, :current_user
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def allow_unauthenticated_access(**options)
|
|
11
|
+
skip_before_action :require_authentication, **options
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def authenticated?
|
|
18
|
+
resume_session
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current_user
|
|
22
|
+
Current.user
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def require_authentication
|
|
26
|
+
resume_session || request_authentication
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resume_session
|
|
30
|
+
Current.session ||= find_session_by_cookie
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
<% if timeoutable? -%>
|
|
34
|
+
def find_session_by_cookie
|
|
35
|
+
return unless cookies.signed[:session_id]
|
|
36
|
+
return unless session = Session.find_by(id: cookies.signed[:session_id])
|
|
37
|
+
|
|
38
|
+
<% if rememberable? -%>
|
|
39
|
+
if !session.user.remembered? && session.user.timedout?(session.updated_at)
|
|
40
|
+
<% else -%>
|
|
41
|
+
if session.user.timedout?(session.updated_at)
|
|
42
|
+
<% end -%>
|
|
43
|
+
session.destroy
|
|
44
|
+
cookies.delete(:session_id)
|
|
45
|
+
nil
|
|
46
|
+
else
|
|
47
|
+
session.touch
|
|
48
|
+
session
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
<% else -%>
|
|
52
|
+
def find_session_by_cookie
|
|
53
|
+
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
|
|
54
|
+
end
|
|
55
|
+
<% end -%>
|
|
56
|
+
|
|
57
|
+
def request_authentication
|
|
58
|
+
session[:return_to_after_authenticating] = request.url
|
|
59
|
+
redirect_to new_session_path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def after_authentication_url
|
|
63
|
+
session.delete(:return_to_after_authenticating) || root_url
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
<% if rememberable? -%>
|
|
67
|
+
def start_new_session_for(user, remember: false)
|
|
68
|
+
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
|
69
|
+
Current.session = session
|
|
70
|
+
|
|
71
|
+
if remember
|
|
72
|
+
user.remember_me!
|
|
73
|
+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
|
74
|
+
else
|
|
75
|
+
user.forget_me!
|
|
76
|
+
cookies.signed[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
<% else -%>
|
|
81
|
+
def start_new_session_for(user)
|
|
82
|
+
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
|
83
|
+
Current.session = session
|
|
84
|
+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
<% end -%>
|
|
88
|
+
|
|
89
|
+
def terminate_session
|
|
90
|
+
<% if rememberable? -%>
|
|
91
|
+
Current.session.user.forget_me!
|
|
92
|
+
<% end -%>
|
|
93
|
+
Current.session.destroy
|
|
94
|
+
cookies.delete(:session_id)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class ConfirmationsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access
|
|
3
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_confirmation_path, alert: "Try again later." }
|
|
4
|
+
|
|
5
|
+
def new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
user = User.find_by(email_address: params[:email_address])
|
|
10
|
+
<% if reconfirmable? -%>
|
|
11
|
+
user.send_confirmation_instructions if user && (!user.confirmed? || user.unconfirmed_email.present?)
|
|
12
|
+
<% else -%>
|
|
13
|
+
user.send_confirmation_instructions if user && !user.confirmed?
|
|
14
|
+
<% end -%>
|
|
15
|
+
|
|
16
|
+
redirect_to new_session_path, notice: "Confirmation instructions sent (if user with that email address exists and is unconfirmed)."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show
|
|
20
|
+
if user = User.find_by_valid_confirmation_token(params[:token])
|
|
21
|
+
user.confirm!
|
|
22
|
+
redirect_to new_session_path, notice: "Your email address has been confirmed. Please sign in."
|
|
23
|
+
else
|
|
24
|
+
redirect_to new_confirmation_path, alert: "Confirmation link is invalid or has expired."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class InvitationsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access only: %i[ edit update ]
|
|
3
|
+
before_action :set_user_by_token, only: %i[ edit update ]
|
|
4
|
+
|
|
5
|
+
def new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
user = User.invite!(params[:email_address], invited_by: Current.user)
|
|
10
|
+
|
|
11
|
+
if user.persisted?
|
|
12
|
+
redirect_to new_invitation_path, notice: "Invitation sent to #{user.email_address}."
|
|
13
|
+
else
|
|
14
|
+
redirect_to new_invitation_path, alert: user.errors.full_messages.to_sentence
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def edit
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update
|
|
22
|
+
if @user.accept_invitation!(password: params[:password], password_confirmation: params[:password_confirmation])
|
|
23
|
+
start_new_session_for @user
|
|
24
|
+
redirect_to after_authentication_url, notice: "Welcome! Your account is ready."
|
|
25
|
+
else
|
|
26
|
+
redirect_to edit_invitation_path(params[:token]), alert: @user.errors.full_messages.to_sentence
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def set_user_by_token
|
|
33
|
+
@user = User.find_by_valid_invitation_token(params[:token])
|
|
34
|
+
redirect_to new_session_path, alert: "Invitation link is invalid or has expired." unless @user
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class PasswordsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access
|
|
3
|
+
before_action :set_user_by_token, only: %i[ edit update ]
|
|
4
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
if user = User.find_by(email_address: params[:email_address])
|
|
11
|
+
user.send_reset_password_instructions
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def edit
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update
|
|
21
|
+
if @user.update(params.permit(:password, :password_confirmation))
|
|
22
|
+
@user.clear_reset_password_token!
|
|
23
|
+
<% if lockable? -%>
|
|
24
|
+
@user.unlock!
|
|
25
|
+
<% end -%>
|
|
26
|
+
@user.sessions.destroy_all
|
|
27
|
+
redirect_to new_session_path, notice: "Password has been reset."
|
|
28
|
+
else
|
|
29
|
+
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def set_user_by_token
|
|
36
|
+
@user = User.find_by_valid_reset_password_token(params[:token])
|
|
37
|
+
redirect_to new_password_path, alert: "Password reset link is invalid or has expired." unless @user
|
|
38
|
+
end
|
|
39
|
+
end
|