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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +90 -0
  4. data/Rakefile +8 -0
  5. data/app/controllers/concerns/securial/identity.rb +67 -0
  6. data/app/controllers/securial/accounts_controller.rb +60 -0
  7. data/app/controllers/securial/application_controller.rb +18 -0
  8. data/app/controllers/securial/passwords_controller.rb +35 -0
  9. data/app/controllers/securial/role_assignments_controller.rb +49 -0
  10. data/app/controllers/securial/roles_controller.rb +44 -0
  11. data/app/controllers/securial/sessions_controller.rb +76 -0
  12. data/app/controllers/securial/status_controller.rb +9 -0
  13. data/app/controllers/securial/users_controller.rb +53 -0
  14. data/app/jobs/securial/application_job.rb +4 -0
  15. data/app/mailers/securial/application_mailer.rb +6 -0
  16. data/app/mailers/securial/securial_mailer.rb +17 -0
  17. data/app/models/concerns/securial/password_resettable.rb +47 -0
  18. data/app/models/securial/application_record.rb +18 -0
  19. data/app/models/securial/current.rb +6 -0
  20. data/app/models/securial/role.rb +10 -0
  21. data/app/models/securial/role_assignment.rb +6 -0
  22. data/app/models/securial/session.rb +27 -0
  23. data/app/models/securial/user.rb +54 -0
  24. data/app/views/layouts/securial/mailer.html.erb +13 -0
  25. data/app/views/layouts/securial/mailer.text.erb +1 -0
  26. data/app/views/securial/accounts/show.json.jbuilder +1 -0
  27. data/app/views/securial/passwords/_password.json.jbuilder +2 -0
  28. data/app/views/securial/passwords/index.json.jbuilder +1 -0
  29. data/app/views/securial/passwords/show.json.jbuilder +1 -0
  30. data/app/views/securial/role_assignments/show.json.jbuilder +1 -0
  31. data/app/views/securial/roles/_securial_role.json.jbuilder +9 -0
  32. data/app/views/securial/roles/index.json.jbuilder +6 -0
  33. data/app/views/securial/roles/show.json.jbuilder +1 -0
  34. data/app/views/securial/securial_mailer/reset_password.html.erb +5 -0
  35. data/app/views/securial/securial_mailer/reset_password.text.erb +4 -0
  36. data/app/views/securial/sessions/_session.json.jbuilder +15 -0
  37. data/app/views/securial/sessions/index.json.jbuilder +6 -0
  38. data/app/views/securial/sessions/show.json.jbuilder +1 -0
  39. data/app/views/securial/status/show.json.jbuilder +3 -0
  40. data/app/views/securial/users/_securial_user.json.jbuilder +14 -0
  41. data/app/views/securial/users/index.json.jbuilder +6 -0
  42. data/app/views/securial/users/show.json.jbuilder +1 -0
  43. data/bin/securial +58 -0
  44. data/config/routes.rb +41 -0
  45. data/db/migrate/20250515104930_create_securial_roles.rb +12 -0
  46. data/db/migrate/20250517155521_create_securial_users.rb +18 -0
  47. data/db/migrate/20250518122749_create_securial_role_assignments.rb +10 -0
  48. data/db/migrate/20250519075407_create_securial_sessions.rb +15 -0
  49. data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +6 -0
  50. data/lib/generators/factory_bot/model/model_generator.rb +31 -0
  51. data/lib/generators/factory_bot/templates/factory.erb +7 -0
  52. data/lib/generators/securial/install/install_generator.rb +37 -0
  53. data/lib/generators/securial/install/templates/securial_initializer.erb +109 -0
  54. data/lib/generators/securial/jbuilder/jbuilder_generator.rb +52 -0
  55. data/lib/generators/securial/jbuilder/templates/_resource.json.erb +10 -0
  56. data/lib/generators/securial/jbuilder/templates/index.json.erb +7 -0
  57. data/lib/generators/securial/jbuilder/templates/show.json.erb +1 -0
  58. data/lib/generators/securial/scaffold/scaffold_generator.rb +146 -0
  59. data/lib/generators/securial/scaffold/templates/controller.erb +44 -0
  60. data/lib/generators/securial/scaffold/templates/request_spec.erb +61 -0
  61. data/lib/generators/securial/scaffold/templates/routes.erb +11 -0
  62. data/lib/generators/securial/scaffold/templates/routing_spec.erb +31 -0
  63. data/lib/securial/configuration.rb +35 -0
  64. data/lib/securial/engine.rb +89 -0
  65. data/lib/securial/errors/config_errors.rb +12 -0
  66. data/lib/securial/errors/session_errors.rb +6 -0
  67. data/lib/securial/factories/securial/role_assignments.rb +6 -0
  68. data/lib/securial/factories/securial/roles.rb +18 -0
  69. data/lib/securial/factories/securial/sessions.rb +12 -0
  70. data/lib/securial/factories/securial/users.rb +17 -0
  71. data/lib/securial/helpers/auth_helper.rb +46 -0
  72. data/lib/securial/helpers/normalizing_helper.rb +17 -0
  73. data/lib/securial/helpers/regex_helper.rb +17 -0
  74. data/lib/securial/logger.rb +71 -0
  75. data/lib/securial/middleware/request_logger_tag.rb +18 -0
  76. data/lib/securial/route_inspector.rb +50 -0
  77. data/lib/securial/version.rb +3 -0
  78. data/lib/securial.rb +94 -0
  79. data/lib/tasks/securial_tasks.rake +4 -0
  80. 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
+ ![image](https://github.com/user-attachments/assets/d7cb9645-c28e-4cca-9c1b-5085a91c11d4)
2
+
3
+ ---
4
+ # Securial
5
+
6
+ [![Gem Version](https://img.shields.io/gem/v/securial?logo=rubygems&logoColor=ffffff&logoSize=auto&label=version&color=violet&cacheSeconds=120)](https://rubygems.org/gems/securial)
7
+ [![Gem Downloads](https://img.shields.io/gem/dt/securial.svg)](https://rubygems.org/gems/securial)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/AlyBadawy/Securial?tab=MIT-1-ov-file#readme)
9
+
10
+ [![Tests](https://github.com/alybadawy/securial/actions/workflows/ci.yml/badge.svg)](https://github.com/alybadawy/securial/actions)
11
+ [![Coverage Status](https://coveralls.io/repos/github/AlyBadawy/Securial/badge.svg?branch=main)](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,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -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,9 @@
1
+ module Securial
2
+ class StatusController < ApplicationController
3
+ skip_authentication!
4
+
5
+ def show
6
+ Securial::ENGINE_LOGGER.info("Status check initiated")
7
+ end
8
+ end
9
+ 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,4 @@
1
+ module Securial
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Securial
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ 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,6 @@
1
+ module Securial
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :session
4
+ delegate :user, to: :session, allow_nil: true
5
+ end
6
+ 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
@@ -0,0 +1,6 @@
1
+ module Securial
2
+ class RoleAssignment < ApplicationRecord
3
+ belongs_to :user
4
+ belongs_to :role
5
+ end
6
+ end