securial 0.4.2
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/MIT-LICENSE +20 -0
- data/README.md +90 -0
- data/Rakefile +8 -0
- data/app/controllers/concerns/securial/identity.rb +67 -0
- data/app/controllers/securial/accounts_controller.rb +60 -0
- data/app/controllers/securial/application_controller.rb +18 -0
- data/app/controllers/securial/passwords_controller.rb +35 -0
- data/app/controllers/securial/role_assignments_controller.rb +49 -0
- data/app/controllers/securial/roles_controller.rb +44 -0
- data/app/controllers/securial/sessions_controller.rb +76 -0
- data/app/controllers/securial/status_controller.rb +9 -0
- data/app/controllers/securial/users_controller.rb +53 -0
- data/app/jobs/securial/application_job.rb +4 -0
- data/app/mailers/securial/application_mailer.rb +6 -0
- data/app/mailers/securial/securial_mailer.rb +17 -0
- data/app/models/concerns/securial/password_resettable.rb +47 -0
- data/app/models/securial/application_record.rb +18 -0
- data/app/models/securial/current.rb +6 -0
- data/app/models/securial/role.rb +10 -0
- data/app/models/securial/role_assignment.rb +6 -0
- data/app/models/securial/session.rb +27 -0
- data/app/models/securial/user.rb +54 -0
- data/app/views/layouts/securial/mailer.html.erb +13 -0
- data/app/views/layouts/securial/mailer.text.erb +1 -0
- data/app/views/securial/accounts/show.json.jbuilder +1 -0
- data/app/views/securial/passwords/_password.json.jbuilder +2 -0
- data/app/views/securial/passwords/index.json.jbuilder +1 -0
- data/app/views/securial/passwords/show.json.jbuilder +1 -0
- data/app/views/securial/role_assignments/show.json.jbuilder +1 -0
- data/app/views/securial/roles/_securial_role.json.jbuilder +9 -0
- data/app/views/securial/roles/index.json.jbuilder +6 -0
- data/app/views/securial/roles/show.json.jbuilder +1 -0
- data/app/views/securial/securial_mailer/reset_password.html.erb +5 -0
- data/app/views/securial/securial_mailer/reset_password.text.erb +4 -0
- data/app/views/securial/sessions/_session.json.jbuilder +15 -0
- data/app/views/securial/sessions/index.json.jbuilder +6 -0
- data/app/views/securial/sessions/show.json.jbuilder +1 -0
- data/app/views/securial/status/show.json.jbuilder +3 -0
- data/app/views/securial/users/_securial_user.json.jbuilder +14 -0
- data/app/views/securial/users/index.json.jbuilder +6 -0
- data/app/views/securial/users/show.json.jbuilder +1 -0
- data/bin/securial +58 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20250515104930_create_securial_roles.rb +12 -0
- data/db/migrate/20250517155521_create_securial_users.rb +18 -0
- data/db/migrate/20250518122749_create_securial_role_assignments.rb +10 -0
- data/db/migrate/20250519075407_create_securial_sessions.rb +15 -0
- data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +6 -0
- data/lib/generators/factory_bot/model/model_generator.rb +31 -0
- data/lib/generators/factory_bot/templates/factory.erb +7 -0
- data/lib/generators/securial/install/install_generator.rb +37 -0
- data/lib/generators/securial/install/templates/securial_initializer.erb +109 -0
- data/lib/generators/securial/jbuilder/jbuilder_generator.rb +52 -0
- data/lib/generators/securial/jbuilder/templates/_resource.json.erb +10 -0
- data/lib/generators/securial/jbuilder/templates/index.json.erb +7 -0
- data/lib/generators/securial/jbuilder/templates/show.json.erb +1 -0
- data/lib/generators/securial/scaffold/scaffold_generator.rb +146 -0
- data/lib/generators/securial/scaffold/templates/controller.erb +44 -0
- data/lib/generators/securial/scaffold/templates/request_spec.erb +61 -0
- data/lib/generators/securial/scaffold/templates/routes.erb +11 -0
- data/lib/generators/securial/scaffold/templates/routing_spec.erb +31 -0
- data/lib/securial/configuration.rb +35 -0
- data/lib/securial/engine.rb +89 -0
- data/lib/securial/errors/config_errors.rb +12 -0
- data/lib/securial/errors/session_errors.rb +6 -0
- data/lib/securial/factories/securial/role_assignments.rb +6 -0
- data/lib/securial/factories/securial/roles.rb +18 -0
- data/lib/securial/factories/securial/sessions.rb +12 -0
- data/lib/securial/factories/securial/users.rb +17 -0
- data/lib/securial/helpers/auth_helper.rb +46 -0
- data/lib/securial/helpers/normalizing_helper.rb +17 -0
- data/lib/securial/helpers/regex_helper.rb +17 -0
- data/lib/securial/logger.rb +71 -0
- data/lib/securial/middleware/request_logger_tag.rb +18 -0
- data/lib/securial/route_inspector.rb +50 -0
- data/lib/securial/version.rb +3 -0
- data/lib/securial.rb +94 -0
- data/lib/tasks/securial_tasks.rake +4 -0
- metadata +435 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2e0abc0d4c0a4b2a56220ab298edd28f767b4d8909a6af78f3097421cfc2fdda
|
4
|
+
data.tar.gz: 9230d0f2c93d1899cb642206adf79ceec04936894720dc989cc2b25c873f8774
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 04102e38f339f980e2792c6dcc0559ef68a48506c31fc6b6625f44f7cc8f0a668d32c7e42583dfb7abe2d45690ca3e0df9ed9acb916f5c1630c651966413f8b2
|
7
|
+
data.tar.gz: 7db35473e9c983e9c6f803889da638ac210795ea8a9bc1270efd8136fc6a7c293ac01b7b8ca53b43fa695b33e872884e215a0496763222a0159a8c0dd5efe3d0
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Aly Badawy
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
---
|
4
|
+
# Securial
|
5
|
+
|
6
|
+
[](https://rubygems.org/gems/securial)
|
7
|
+
[](https://rubygems.org/gems/securial)
|
8
|
+
[](https://github.com/AlyBadawy/Securial?tab=MIT-1-ov-file#readme)
|
9
|
+
|
10
|
+
[](https://github.com/alybadawy/securial/actions)
|
11
|
+
[](https://coveralls.io/github/AlyBadawy/Securial?branch=main)
|
12
|
+
|
13
|
+
**Securial** is a mountable Rails engine that provides robust, extensible authentication and access control for Rails applications. It supports:
|
14
|
+
|
15
|
+
- ✅ JWT-based authentication
|
16
|
+
- ✅ API tokens
|
17
|
+
- ✅ Session-based auth
|
18
|
+
- ✅ Simple integration with web and mobile apps
|
19
|
+
- ✅ Clean, JSON-based API responses
|
20
|
+
- ✅ Database-agnostic support
|
21
|
+
|
22
|
+
Next, mount the engine in `config/routes.rb`:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Rails.application.routes.draw do
|
26
|
+
mount Securial::Engine => "/securial"
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
Full installation steps are available in the [Wiki › Installation](https://github.com/AlyBadawy/Securial/wiki/Installation).
|
31
|
+
|
32
|
+
## ⚙️ Configuration
|
33
|
+
|
34
|
+
**Securial** generates an initializer with sensible defaults and full control over logging, mailers, session settings, and roles.
|
35
|
+
|
36
|
+
For all configuration options and examples, refer to the [Wiki › Configuration](https://github.com/AlyBadawy/Securial/wiki/Configuration)
|
37
|
+
|
38
|
+
## 📦 Usage
|
39
|
+
|
40
|
+
After installation and mounting, **Securial** exposes endpoints like:
|
41
|
+
|
42
|
+
- GET /securial/status — Check service availability
|
43
|
+
- POST /securial/sessions — Sign in (JWT or session)
|
44
|
+
- DELETE /securial/sessions — Sign out
|
45
|
+
- GET /securial/accounts/cool_username — Get a user profile by username
|
46
|
+
- GET /securial/admins/roles — View roles
|
47
|
+
|
48
|
+
**Securial** returns consistent JSON API responses.
|
49
|
+
|
50
|
+
Full details, including authentication flows and protected routes, are available in the [Wiki › Authentication module docs](https://github.com/AlyBadawy/Securial/wiki/Authentication).
|
51
|
+
|
52
|
+
🧩 Modules
|
53
|
+
|
54
|
+
**Securial** is organized into modular components including:
|
55
|
+
|
56
|
+
- Authentication
|
57
|
+
- User Management
|
58
|
+
- Generators
|
59
|
+
- Securial::Identity concern
|
60
|
+
|
61
|
+
Explore all modules in the [Wiki](https://github.com/AlyBadawy/Securial/wiki).
|
62
|
+
|
63
|
+
## 🛠 Development & Testing
|
64
|
+
|
65
|
+
To run the test suite:
|
66
|
+
|
67
|
+
```bash
|
68
|
+
$ RAILS_ENV=test bundle db:schema:load
|
69
|
+
$ bundle exec rspec
|
70
|
+
```
|
71
|
+
|
72
|
+
View the coverage report:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
$ open coverage/index.html
|
76
|
+
```
|
77
|
+
|
78
|
+
## 🤝 Contributing
|
79
|
+
|
80
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alybadawy/securial.
|
81
|
+
|
82
|
+
1. Fork the repo
|
83
|
+
2. Create your feature branch (git checkout -b my-feature)
|
84
|
+
3. Commit your changes (git commit -am 'Add my feature')
|
85
|
+
4. Push to the branch (git push origin my-feature)
|
86
|
+
5. Open a Pull Request
|
87
|
+
|
88
|
+
## ⚖️ License
|
89
|
+
|
90
|
+
The gem is available as open source under the terms of the [MIT license](https://github.com/AlyBadawy/Securial?tab=MIT-1-ov-file#readme).
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Securial
|
2
|
+
module Identity
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_action :authenticate_user!
|
7
|
+
helper_method :current_user if respond_to?(:helper_method)
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def skip_authentication!(**options)
|
12
|
+
skip_before_action :authenticate_user!, **options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def authenticate_admin!
|
17
|
+
unless current_user.is_admin?
|
18
|
+
render status: :forbidden, json: { error: "You are not authorized to perform this action" } and return
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_user
|
23
|
+
Current.session&.user
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def authenticate_user!
|
29
|
+
return if internal_rails_request?
|
30
|
+
|
31
|
+
auth_header = request.headers["Authorization"]
|
32
|
+
if auth_header.present? && auth_header.start_with?("Bearer ")
|
33
|
+
token = auth_header.split(" ").last
|
34
|
+
begin
|
35
|
+
decoded_token = AuthHelper.decode(token)
|
36
|
+
Current.session = Session.find_by!(id: decoded_token["jti"], revoked: false)
|
37
|
+
rescue JWT::DecodeError, ActiveRecord::RecordNotFound => e
|
38
|
+
render status: :unauthorized, json: { error: "Invalid token: #{e.message}" } and return
|
39
|
+
end
|
40
|
+
else
|
41
|
+
render status: :unauthorized, json: { error: "Missing or invalid Authorization header" } and return
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_new_session_for(user)
|
46
|
+
user.sessions.create!(
|
47
|
+
user_agent: request.user_agent,
|
48
|
+
ip_address: request.remote_ip,
|
49
|
+
refresh_token: SecureRandom.hex(64),
|
50
|
+
last_refreshed_at: Time.current,
|
51
|
+
refresh_token_expires_at: 1.week.from_now,
|
52
|
+
).tap do |session|
|
53
|
+
Current.session = session
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_jwt_for_current_session
|
58
|
+
AuthHelper.encode(Current.session)
|
59
|
+
end
|
60
|
+
|
61
|
+
def internal_rails_request?
|
62
|
+
defined?(Rails::InfoController) && is_a?(Rails::InfoController) ||
|
63
|
+
defined?(Rails::MailersController) && is_a?(Rails::MailersController) ||
|
64
|
+
defined?(Rails::WelcomeController) && is_a?(Rails::WelcomeController)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Securial
|
2
|
+
class AccountsController < ApplicationController
|
3
|
+
def me
|
4
|
+
@securial_user = Current.user
|
5
|
+
|
6
|
+
render :show, status: :ok, location: @securial_user
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@securial_user = User.find_by(username: params.expect(:username))
|
11
|
+
render_user_profile
|
12
|
+
end
|
13
|
+
|
14
|
+
def register
|
15
|
+
@securial_user = User.new(user_params)
|
16
|
+
if @securial_user.save
|
17
|
+
render :show, status: :created, location: @securial_user
|
18
|
+
else
|
19
|
+
render json: { errors: @securial_user.errors.full_messages }, status: :unprocessable_entity
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_profile
|
24
|
+
@securial_user = Current.user
|
25
|
+
if @securial_user.authenticate(params[:securial_user][:current_password])
|
26
|
+
if @securial_user.update(user_params)
|
27
|
+
render :show, status: :ok, location: @securial_user
|
28
|
+
else
|
29
|
+
render json: { errors: @securial_user.errors.full_messages }, status: :unprocessable_entity
|
30
|
+
end
|
31
|
+
else
|
32
|
+
render json: { error: "Current password is incorrect" }, status: :unprocessable_entity
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_account
|
37
|
+
@securial_user = Current.user
|
38
|
+
if @securial_user.authenticate(params.expect(securial_user: [:current_password]).dig(:current_password))
|
39
|
+
@securial_user.destroy
|
40
|
+
render json: { message: "Account deleted successfully" }, status: :ok
|
41
|
+
else
|
42
|
+
render json: { error: "Current password is incorrect" }, status: :unprocessable_entity
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def user_params
|
49
|
+
params.expect(securial_user: [:email_address, :password, :password_confirmation, :first_name, :last_name, :phone, :username, :bio])
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_user_profile
|
53
|
+
if @securial_user
|
54
|
+
render :show, status: :ok, location: @securial_user
|
55
|
+
else
|
56
|
+
render json: { error: "User not found" }, status: :not_found
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Securial
|
2
|
+
class ApplicationController < ActionController::API
|
3
|
+
prepend_view_path Securial::Engine.root.join("app", "views")
|
4
|
+
|
5
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_404
|
6
|
+
rescue_from ActionController::ParameterMissing, with: :render_400
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def render_404
|
11
|
+
render status: :not_found, json: { error: "Record not found" }
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_400(exception)
|
15
|
+
render status: :bad_request, json: { error: exception.message }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Securial
|
2
|
+
class PasswordsController < ApplicationController
|
3
|
+
skip_authentication!
|
4
|
+
before_action :set_user_by_password_token, only: %i[ reset_password ]
|
5
|
+
|
6
|
+
def forgot_password
|
7
|
+
if user = User.find_by(email_address: params.require(:email_address))
|
8
|
+
user.generate_reset_password_token!
|
9
|
+
Securial::SecurialMailer.reset_password(user).deliver_later
|
10
|
+
end
|
11
|
+
|
12
|
+
render status: :ok, json: { message: "Password reset instructions sent (if user with that email address exists)." }
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset_password
|
16
|
+
@user.clear_reset_password_token!
|
17
|
+
if @user.update(params.permit(:password, :password_confirmation))
|
18
|
+
render status: :ok, json: { message: "Password has been reset." }
|
19
|
+
else
|
20
|
+
render status: :unprocessable_entity, json: { errors: @user.errors }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def set_user_by_password_token
|
27
|
+
@user = User.find_by_reset_password_token!(params[:token]) # rubocop:disable Rails/DynamicFindBy
|
28
|
+
unless @user.reset_password_token_valid?
|
29
|
+
render status: :unprocessable_entity, json: { errors: { token: "is invalid or has expired" } }
|
30
|
+
end
|
31
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveRecord::RecordNotFound
|
32
|
+
render status: :unprocessable_entity, json: { errors: { token: "is invalid or has expired" } } and return
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Securial
|
2
|
+
class RoleAssignmentsController < ApplicationController
|
3
|
+
def create
|
4
|
+
return unless define_user_and_role
|
5
|
+
|
6
|
+
if @user.roles.exists?(@role.id)
|
7
|
+
render json: { error: "Role already assigned to user" }, status: :unprocessable_entity
|
8
|
+
return
|
9
|
+
end
|
10
|
+
@securial_role_assignment = RoleAssignment.new(securial_role_assignment_params)
|
11
|
+
@securial_role_assignment.save
|
12
|
+
@securial_user = @user
|
13
|
+
render :show, status: :created
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
return unless define_user_and_role
|
18
|
+
@role_assignment = RoleAssignment.find_by(securial_role_assignment_params)
|
19
|
+
if @role_assignment
|
20
|
+
@role_assignment.destroy!
|
21
|
+
@securial_user = @user
|
22
|
+
render :show, status: :ok
|
23
|
+
else
|
24
|
+
render json: { error: "Role is not assigned to user" }, status: :unprocessable_entity
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def define_user_and_role
|
31
|
+
@user = User.find_by(id: params.expect(securial_role_assignment: [:user_id]).dig(:user_id))
|
32
|
+
@role = Role.find_by(id: params.expect(securial_role_assignment: [:role_id]).dig(:role_id))
|
33
|
+
if @user.nil?
|
34
|
+
render json: { error: "User not found" }, status: :unprocessable_entity
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
if @role.nil?
|
38
|
+
render json: { error: "Role not found" }, status: :unprocessable_entity
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def securial_role_assignment_params
|
46
|
+
params.expect(securial_role_assignment: [:user_id, :role_id])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Securial
|
2
|
+
class RolesController < ApplicationController
|
3
|
+
before_action :set_securial_role, only: [:show, :update, :destroy]
|
4
|
+
|
5
|
+
def index
|
6
|
+
@securial_roles = Role.all
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
@securial_role = Role.new(securial_role_params)
|
14
|
+
|
15
|
+
if @securial_role.save
|
16
|
+
render :show, status: :created, location: @securial_role
|
17
|
+
else
|
18
|
+
render json: @securial_role.errors, status: :unprocessable_entity
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
if @securial_role.update(securial_role_params)
|
24
|
+
render :show
|
25
|
+
else
|
26
|
+
render json: @securial_role.errors, status: :unprocessable_entity
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy
|
31
|
+
@securial_role.destroy
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_securial_role
|
37
|
+
@securial_role = Role.find(params[:id])
|
38
|
+
end
|
39
|
+
|
40
|
+
def securial_role_params
|
41
|
+
params.expect(securial_role: [:role_name, :hide_from_profile])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Securial
|
2
|
+
class SessionsController < ApplicationController
|
3
|
+
skip_authentication! only: %i[login refresh]
|
4
|
+
|
5
|
+
before_action :set_session, only: %i[show revoke logout]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@securial_sessions = Current.user.sessions
|
9
|
+
end
|
10
|
+
|
11
|
+
def show
|
12
|
+
end
|
13
|
+
|
14
|
+
def login
|
15
|
+
params.require([:email_address, :password])
|
16
|
+
if user = User.authenticate_by(params.permit([:email_address, :password]))
|
17
|
+
start_new_session_for user
|
18
|
+
render status: :created,
|
19
|
+
json: {
|
20
|
+
access_token: create_jwt_for_current_session,
|
21
|
+
refresh_token: Current.session.refresh_token,
|
22
|
+
refresh_token_expires_at: Current.session.refresh_token_expires_at,
|
23
|
+
}
|
24
|
+
else
|
25
|
+
render status: :unauthorized,
|
26
|
+
json: {
|
27
|
+
error: "Invalid email address or password.",
|
28
|
+
fix: "Make sure to send the correct 'email_address' and 'password' in the payload",
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def logout
|
34
|
+
@securial_session.revoke!
|
35
|
+
Current.session = nil
|
36
|
+
head :no_content
|
37
|
+
end
|
38
|
+
|
39
|
+
def refresh
|
40
|
+
if Current.session = Session.find_by(refresh_token: params[:refresh_token])
|
41
|
+
if Current.session.is_valid_session? &&
|
42
|
+
Current.session.ip_address == request.ip &&
|
43
|
+
Current.session.user_agent == request.user_agent
|
44
|
+
Current.session.refresh!
|
45
|
+
render status: :created,
|
46
|
+
json: {
|
47
|
+
access_token: create_jwt_for_current_session,
|
48
|
+
refresh_token: Current.session.refresh_token,
|
49
|
+
refresh_token_expires_at: Current.session.refresh_token_expires_at,
|
50
|
+
}
|
51
|
+
return
|
52
|
+
end
|
53
|
+
end
|
54
|
+
render status: :unprocessable_entity, json: { error: "Invalid or expired token." }
|
55
|
+
end
|
56
|
+
|
57
|
+
def revoke
|
58
|
+
@securial_session.revoke!
|
59
|
+
Current.session = nil if @securial_session == Current.session
|
60
|
+
head :no_content
|
61
|
+
end
|
62
|
+
|
63
|
+
def revoke_all
|
64
|
+
Current.user.sessions.each(&:revoke!)
|
65
|
+
Current.session = nil
|
66
|
+
head :no_content
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def set_session
|
72
|
+
id = params[:id]
|
73
|
+
@securial_session = id ? Current.user.sessions.find(params.expect(:id)) : Current.session
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Securial
|
2
|
+
class UsersController < ApplicationController
|
3
|
+
before_action :set_securial_user, only: [:show, :update, :destroy]
|
4
|
+
|
5
|
+
def index
|
6
|
+
@securial_users = User.all
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
@securial_user = User.new(securial_user_params)
|
14
|
+
|
15
|
+
if @securial_user.save
|
16
|
+
render :show, status: :created, location: @securial_user
|
17
|
+
else
|
18
|
+
render json: @securial_user.errors, status: :unprocessable_entity
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
if @securial_user.update(securial_user_params)
|
24
|
+
render :show, status: :ok, location: @securial_user
|
25
|
+
else
|
26
|
+
render json: @securial_user.errors, status: :unprocessable_entity
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy
|
31
|
+
@securial_user.destroy!
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_securial_user
|
37
|
+
@securial_user = User.find(params.expect(:id))
|
38
|
+
end
|
39
|
+
|
40
|
+
def securial_user_params
|
41
|
+
params.expect(securial_user: [
|
42
|
+
:email_address,
|
43
|
+
:password,
|
44
|
+
:password_confirmation,
|
45
|
+
:first_name,
|
46
|
+
:last_name,
|
47
|
+
:phone,
|
48
|
+
:username,
|
49
|
+
:bio
|
50
|
+
])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Securial
|
2
|
+
class SecurialMailer < ApplicationMailer
|
3
|
+
default from: Securial.configuration.mailer_sender
|
4
|
+
|
5
|
+
def reset_password(securial_user)
|
6
|
+
@user = securial_user
|
7
|
+
subject = reset_password_subject
|
8
|
+
mail subject: subject, to: securial_user.email_address
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def reset_password_subject
|
14
|
+
Securial.configuration.password_reset_email_subject
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Securial
|
2
|
+
module PasswordResettable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_secure_password
|
7
|
+
|
8
|
+
validates :password,
|
9
|
+
length: {
|
10
|
+
minimum: Securial.configuration.password_min_length,
|
11
|
+
maximum: Securial.configuration.password_max_length,
|
12
|
+
},
|
13
|
+
format: {
|
14
|
+
with: Securial.configuration.password_complexity,
|
15
|
+
message: "must contain at least one uppercase letter, one lowercase letter, one digit, and one special character",
|
16
|
+
},
|
17
|
+
allow_nil: true
|
18
|
+
|
19
|
+
validates :password_confirmation,
|
20
|
+
presence: true,
|
21
|
+
if: -> { new_record? || !password.nil? }
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_reset_password_token!
|
25
|
+
update!(
|
26
|
+
reset_password_token: SecureRandom.urlsafe_base64,
|
27
|
+
reset_password_token_created_at: Time.current
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset_password_token_valid?
|
32
|
+
return false if reset_password_token.blank? || reset_password_token_created_at.blank?
|
33
|
+
|
34
|
+
duration = Securial.configuration.reset_password_token_expires_in
|
35
|
+
return false unless duration.is_a?(ActiveSupport::Duration)
|
36
|
+
|
37
|
+
reset_password_token_created_at > duration.ago
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_reset_password_token!
|
41
|
+
update!(
|
42
|
+
reset_password_token: nil,
|
43
|
+
reset_password_token_created_at: nil
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Securial
|
2
|
+
class ApplicationRecord < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
|
5
|
+
before_create :generate_uuid_v7
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Generates a UUIDv7 for the `id` field if it is blank.
|
10
|
+
# This method is triggered by the `before_create` callback.
|
11
|
+
# The generated ID is expected to be a UUIDv7 string.
|
12
|
+
def generate_uuid_v7
|
13
|
+
return if self.id.present? || self.class.type_for_attribute(:id).type != :string
|
14
|
+
|
15
|
+
self.id ||= Random.uuid_v7
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Securial
|
2
|
+
class Role < ApplicationRecord
|
3
|
+
normalizes :role_name, with: ->(e) { Securial::NormalizingHelper.normalize_role_name(e) }
|
4
|
+
|
5
|
+
validates :role_name, presence: true, uniqueness: { case_sensitive: false }
|
6
|
+
|
7
|
+
has_many :role_assignments, dependent: :destroy
|
8
|
+
has_many :users, through: :role_assignments
|
9
|
+
end
|
10
|
+
end
|