maquina 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.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/images/maquina/maquina.svg +18 -0
  6. data/app/assets/javascripts/maquina/application.js +4 -0
  7. data/app/assets/javascripts/maquina/controllers/alert_controller.js +29 -0
  8. data/app/assets/javascripts/maquina/controllers/application.js +9 -0
  9. data/app/assets/javascripts/maquina/controllers/file_controller.js +60 -0
  10. data/app/assets/javascripts/maquina/controllers/index.js +11 -0
  11. data/app/assets/javascripts/maquina/controllers/mobile_menu_controller.js +31 -0
  12. data/app/assets/javascripts/maquina/controllers/modal_controller.js +39 -0
  13. data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +15 -0
  14. data/app/assets/javascripts/maquina/controllers/popup_menu_controller.js +17 -0
  15. data/app/assets/javascripts/maquina/controllers/submit_form_controller.js +11 -0
  16. data/app/assets/stylesheets/maquina/application.css +15 -0
  17. data/app/assets/stylesheets/maquina/application.tailwind.css +102 -0
  18. data/app/controllers/concerns/maquina/authenticate.rb +41 -0
  19. data/app/controllers/concerns/maquina/create.rb +27 -0
  20. data/app/controllers/concerns/maquina/destroy.rb +28 -0
  21. data/app/controllers/concerns/maquina/edit.rb +29 -0
  22. data/app/controllers/concerns/maquina/index.rb +33 -0
  23. data/app/controllers/concerns/maquina/new.rb +22 -0
  24. data/app/controllers/concerns/maquina/resourceful.rb +180 -0
  25. data/app/controllers/concerns/maquina/show.rb +27 -0
  26. data/app/controllers/concerns/maquina/update.rb +31 -0
  27. data/app/controllers/maquina/accept_invitations_controller.rb +28 -0
  28. data/app/controllers/maquina/application_controller.rb +19 -0
  29. data/app/controllers/maquina/dashboard_controller.rb +16 -0
  30. data/app/controllers/maquina/invitations_controller.rb +51 -0
  31. data/app/controllers/maquina/plans_controller.rb +56 -0
  32. data/app/controllers/maquina/sessions_controller.rb +56 -0
  33. data/app/controllers/maquina/unauthorized_controller.rb +9 -0
  34. data/app/controllers/maquina/users_controller.rb +24 -0
  35. data/app/helpers/maquina/application_helper.rb +19 -0
  36. data/app/helpers/maquina/navbar_menu_helper.rb +29 -0
  37. data/app/helpers/maquina/views_helper.rb +9 -0
  38. data/app/jobs/maquina/application_job.rb +4 -0
  39. data/app/mailers/maquina/application_mailer.rb +8 -0
  40. data/app/mailers/maquina/user_notifications_mailer.rb +16 -0
  41. data/app/models/concerns/maquina/authenticate_by.rb +33 -0
  42. data/app/models/concerns/maquina/blockeable.rb +28 -0
  43. data/app/models/concerns/maquina/multifactor.rb +80 -0
  44. data/app/models/concerns/maquina/retain_passwords.rb +32 -0
  45. data/app/models/concerns/maquina/searchable.rb +24 -0
  46. data/app/models/maquina/active_session.rb +33 -0
  47. data/app/models/maquina/application_record.rb +11 -0
  48. data/app/models/maquina/current.rb +21 -0
  49. data/app/models/maquina/invitation.rb +15 -0
  50. data/app/models/maquina/plan.rb +38 -0
  51. data/app/models/maquina/used_password.rb +17 -0
  52. data/app/models/maquina/user.rb +33 -0
  53. data/app/policies/maquina/application_policy.rb +50 -0
  54. data/app/policies/maquina/invitation_policy.rb +13 -0
  55. data/app/policies/maquina/navigation_policy.rb +13 -0
  56. data/app/policies/maquina/plan_policy.rb +49 -0
  57. data/app/policies/maquina/user_policy.rb +27 -0
  58. data/app/views/layouts/maquina/application.html.erb +26 -0
  59. data/app/views/layouts/maquina/mailer.html.erb +377 -0
  60. data/app/views/layouts/maquina/mailer.text.erb +12 -0
  61. data/app/views/layouts/maquina/sessions.html.erb +24 -0
  62. data/app/views/maquina/accept_invitations/new.html.erb +9 -0
  63. data/app/views/maquina/accept_invitations/new_view.rb +41 -0
  64. data/app/views/maquina/application/_navbar.html.erb +21 -0
  65. data/app/views/maquina/application/alert.rb +104 -0
  66. data/app/views/maquina/application/components/action_text_component.rb +20 -0
  67. data/app/views/maquina/application/components/checkbox_component.rb +21 -0
  68. data/app/views/maquina/application/components/component_base.rb +60 -0
  69. data/app/views/maquina/application/components/file_component.rb +59 -0
  70. data/app/views/maquina/application/components/input_component.rb +20 -0
  71. data/app/views/maquina/application/components/select_component.rb +44 -0
  72. data/app/views/maquina/application/create.turbo_stream.erb +11 -0
  73. data/app/views/maquina/application/edit.html.erb +9 -0
  74. data/app/views/maquina/application/edit.rb +17 -0
  75. data/app/views/maquina/application/form.rb +77 -0
  76. data/app/views/maquina/application/index.html.erb +10 -0
  77. data/app/views/maquina/application/index_header.rb +46 -0
  78. data/app/views/maquina/application/index_modal.rb +43 -0
  79. data/app/views/maquina/application/index_table.rb +121 -0
  80. data/app/views/maquina/application/new.html.erb +9 -0
  81. data/app/views/maquina/application/new.rb +18 -0
  82. data/app/views/maquina/application/sessions_header.rb +31 -0
  83. data/app/views/maquina/application/show.html.erb +1 -0
  84. data/app/views/maquina/application/update.turbo_stream.erb +11 -0
  85. data/app/views/maquina/application_view.rb +46 -0
  86. data/app/views/maquina/dashboard/index.html.erb +0 -0
  87. data/app/views/maquina/invitations/create.turbo_stream.erb +13 -0
  88. data/app/views/maquina/invitations/new.html.erb +3 -0
  89. data/app/views/maquina/navbar/menu.rb +62 -0
  90. data/app/views/maquina/navbar/menu_item_link.rb +34 -0
  91. data/app/views/maquina/navbar/mobile_button.rb +29 -0
  92. data/app/views/maquina/navbar/mobile_menu.rb +47 -0
  93. data/app/views/maquina/navbar/notification.rb +37 -0
  94. data/app/views/maquina/navbar/profile.rb +16 -0
  95. data/app/views/maquina/navbar/profile_button.rb +24 -0
  96. data/app/views/maquina/navbar/profile_menu.rb +108 -0
  97. data/app/views/maquina/navbar/profile_menu_item_link.rb +41 -0
  98. data/app/views/maquina/navbar/search.rb +40 -0
  99. data/app/views/maquina/navbar/title.rb +22 -0
  100. data/app/views/maquina/sessions/create.turbo_stream.erb +11 -0
  101. data/app/views/maquina/sessions/form.rb +56 -0
  102. data/app/views/maquina/sessions/new.html.erb +9 -0
  103. data/app/views/maquina/unauthorized/401.html.erb +1 -0
  104. data/app/views/maquina/user_notifications_mailer/invitation_email.html.erb +40 -0
  105. data/app/views/maquina/user_notifications_mailer/invitation_email.text.erb +12 -0
  106. data/config/definitions.rb +1 -0
  107. data/config/initializers/importmap.rb +17 -0
  108. data/config/initializers/money.rb +116 -0
  109. data/config/initializers/pagy.rb +235 -0
  110. data/config/locales/flash.en.yml +44 -0
  111. data/config/locales/forms.en.yml +58 -0
  112. data/config/locales/mailers.en.yml +35 -0
  113. data/config/locales/models.en.yml +34 -0
  114. data/config/locales/routes.en.yml +7 -0
  115. data/config/locales/views.en.yml +45 -0
  116. data/config/routes.rb +17 -0
  117. data/db/migrate/20221109010726_create_maquina_plans.rb +13 -0
  118. data/db/migrate/20221113000409_create_maquina_users.rb +19 -0
  119. data/db/migrate/20221113020108_create_maquina_used_passwords.rb +10 -0
  120. data/db/migrate/20221115223414_create_maquina_active_sessions.rb +15 -0
  121. data/db/migrate/20230201203922_create_maquina_invitations.rb +12 -0
  122. data/db/schema.rb +1 -0
  123. data/lib/generators/maquina/install_generator.rb +32 -0
  124. data/lib/generators/maquina/install_templates/install_templates_generator.rb +31 -0
  125. data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +11 -0
  126. data/lib/generators/maquina/tailwind_config/templates/app/assets/config/maquina/tailwind.config.js.tt +68 -0
  127. data/lib/generators/maquina/templates/config/initializers/maquina.rb +3 -0
  128. data/lib/maquina/engine.rb +17 -0
  129. data/lib/maquina/version.rb +3 -0
  130. data/lib/maquina.rb +48 -0
  131. data/lib/tasks/install.rake +19 -0
  132. data/lib/tasks/maquina_tasks.rake +4 -0
  133. data/lib/tasks/tailwind.rake +25 -0
  134. metadata +456 -0
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Resourceful
5
+ extend ActiveSupport::Concern
6
+
7
+ class ResourceResponse
8
+ def response(&block)
9
+ @block = block
10
+ end
11
+
12
+ def code
13
+ @block
14
+ end
15
+ end
16
+
17
+ included do
18
+ class_attribute :resource_class, instance_writer: false
19
+ class_attribute :find_by_param, instance_writer: false
20
+ class_attribute :list_attributes, instance_writer: false
21
+ class_attribute :form_attributes, instance_writer: false
22
+ class_attribute :show_attributes, instance_writer: false
23
+ class_attribute :policy_class, instance_writer: false
24
+
25
+ attr_reader :resource, :collection
26
+
27
+ protected
28
+
29
+ # Route proxies
30
+
31
+ def base_singular_path_name
32
+ @base_path_name ||= begin
33
+ namespace_path = ""
34
+ "#{namespace_path}#{resource_class.model_name.singular_route_key}_path"
35
+ end
36
+ end
37
+
38
+ def base_plural_path_name
39
+ @base_plural_path_name ||= begin
40
+ namespace_path = ""
41
+ "#{namespace_path}#{resource_class.model_name.route_key}_path"
42
+ end
43
+ end
44
+
45
+ def new_resource_path(options = {})
46
+ Rails.application.routes.url_helpers.send("new_#{base_singular_path_name}", **options)
47
+ end
48
+
49
+ def edit_resource_path(resource, options = {})
50
+ Rails.application.routes.url_helpers.send("edit_#{base_singular_path_name}", resource, **options)
51
+ end
52
+
53
+ def resource_path(resource)
54
+ Rails.application.routes.url_helpers.send(base_singular_path_name, resource)
55
+ end
56
+
57
+ def collection_path(params = {})
58
+ Rails.application.routes.url_helpers.send(base_plural_path_name, **params)
59
+ end
60
+
61
+ def submit_path(params = {})
62
+ end
63
+
64
+ # Controller secure params
65
+ def resource_secure_params
66
+ method_name = :secure_params
67
+ method_action_name = "#{action_name}_#{method_name}".to_sym
68
+
69
+ if respond_to?(method_action_name)
70
+ send(method_action_name)
71
+ elsif respond_to?(method_name)
72
+ send(method_name)
73
+ else
74
+ params.require(resource_class.model_name.element.to_sym).permit(form_attributes.flat_map { |field| field.keys })
75
+ end
76
+ end
77
+
78
+ def set_flash_message(status)
79
+ key = (%w[created accepted].include?(status.to_s) || (status == :no_content && action_name == "destroy")) ? :notice : :alert
80
+ message_hash = t(
81
+ "flash.#{controller_name}.#{action_name}.#{key}",
82
+ default: t("flash.#{action_name}.#{key}")
83
+ )
84
+
85
+ flash[key] = message_hash if !%w[create update].include?(action_name) || (%w[create update].include?(action_name) && %w[created accepted].include?(status.to_s))
86
+ flash.now[key] = message_hash if %w[create update].include?(action_name) && status == :unprocessable_entity
87
+ end
88
+
89
+ def dual_action_response(object, &block)
90
+ has_errors = object&.errors&.any?
91
+ responded = false
92
+
93
+ case block.try(:arity)
94
+ when 2
95
+ responded = true
96
+
97
+ success = ResourceResponse.new
98
+ failure = ResourceResponse.new
99
+ block.call success, failure
100
+
101
+ if has_errors
102
+ failure.code.call
103
+ else
104
+ success.code.call
105
+ end
106
+ when 1
107
+ if !has_errors
108
+ responded = true
109
+
110
+ success = ResourceResponse.new
111
+ block.call success
112
+
113
+ success.code.call
114
+ end
115
+ end
116
+
117
+ if !responded
118
+ respond_to do |format|
119
+ if has_errors
120
+ format.html { render object.persisted? ? :edit : :new }
121
+ format.turbo_stream
122
+ format.json { render json: {errors: object.errors.map { |error| {error.attribute => error.message} }} }
123
+ else
124
+ format.html { redirect_to collection_path, status: :see_other }
125
+ format.json { render json: {data: Array(object), links: build_json_links(object)} }
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def build_json_links(object)
132
+ [
133
+ {
134
+ rel: :self,
135
+ uri: resource_path(object)
136
+ }
137
+ ]
138
+ end
139
+
140
+ helper_method :resource_class, :policy_class, :resource, :list_attributes, :form_attributes, :collection,
141
+ :collection_path, :resource_path, :new_resource_path, :edit_resource_path, :submit_path
142
+ end
143
+
144
+ class_methods do
145
+ def resourceful(resource_class: nil, find_by_param: :id, only: [], except: [], list_attributes: [], form_attributes: [], show_attributes: [], policy_class: nil)
146
+ self.resource_class = resource_class || controller_path.classify.safe_constantize
147
+ self.find_by_param = find_by_param || :id
148
+ self.list_attributes = Array(list_attributes).compact
149
+ self.form_attributes = Array(form_attributes).compact
150
+ self.show_attributes = Array(show_attributes).compact
151
+ self.policy_class = policy_class
152
+
153
+ valid_rest_actions = {
154
+ index: Maquina::Index,
155
+ new: Maquina::New,
156
+ create: Maquina::Create,
157
+ edit: Maquina::Edit,
158
+ update: Maquina::Update,
159
+ show: Maquina::Show,
160
+ destroy: Maquina::Destroy
161
+ }.freeze
162
+
163
+ sanitized_only = Array(only).compact
164
+ sanitized_except = Array(except).compact
165
+ rest_actions_keys = valid_rest_actions.keys
166
+
167
+ calculated_only = sanitized_only.empty? ? rest_actions_keys : rest_actions_keys & sanitized_only
168
+
169
+ (calculated_only - sanitized_except).each do |action|
170
+ rest_action = valid_rest_actions[action]
171
+ if rest_action.present?
172
+ include rest_action
173
+ else
174
+ Rails.logger.error "[#{self.class} :: Action #{action} is undefined]"
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Show
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def show(&block)
9
+ @resource ||= begin
10
+ scope = resource_class
11
+ # TODO: Implement filtering by organization
12
+ # scope = scope.where(organization)
13
+ # TODO: Implement policy authorization (ActionPolicy)
14
+ scope = yield(scope) if block.present?
15
+
16
+ scope.find_by!(find_by_param => params[:id])
17
+ end
18
+
19
+ respond_to do |format|
20
+ format.html
21
+ format.json { render json: @resource }
22
+ end
23
+ end
24
+ alias_method :show!, :show
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Update
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def update(&block)
9
+ @resource ||= begin
10
+ scope = resource_class
11
+ # scope = scope.where(organization)
12
+ # TODO: Implement filtering by organization
13
+ resource = scope.find_by!(find_by_param => params[:id])
14
+
15
+ authorize! resource, with: policy_class if policy_class.present?
16
+
17
+ resource
18
+ end
19
+
20
+ saved = @resource.update(resource_secure_params)
21
+
22
+ status = saved ? :accepted : :unprocessable_entity
23
+ response.status = status
24
+ set_flash_message(status)
25
+
26
+ dual_action_response(@resource, &block)
27
+ end
28
+ alias_method :update!, :update
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class AcceptInvitationsController < ApplicationController
5
+ layout "maquina/sessions"
6
+
7
+ before_action :load_invitation
8
+
9
+ def new
10
+ end
11
+
12
+ def update
13
+ end
14
+
15
+ private
16
+
17
+ def load_invitation
18
+ invitation_token = params[:token] || params.dig(:invitation, :invitation_token)
19
+
20
+ if invitation_token.present?
21
+ invitation_token = CGI.unescape(invitation_token)
22
+ @invitation = Maquina::Invitation.where(accepted_at: nil).find_signed(invitation_token, purpose: :invitation)
23
+ end
24
+
25
+ @invitation ||= Maquina::Invitation.new
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class ApplicationController < ActionController::Base
5
+ include Maquina::Resourceful
6
+ include Maquina::Authenticate
7
+
8
+ helper Maquina::NavbarMenuHelper
9
+ helper Maquina::ViewsHelper
10
+
11
+ rescue_from ActionPolicy::Unauthorized, with: :not_authorized
12
+
13
+ private
14
+
15
+ def not_authorized
16
+ redirect_to unauthorized_path
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class DashboardController < ApplicationController
5
+ before_action :authenticate!
6
+
7
+ def index
8
+ end
9
+
10
+ protected
11
+
12
+ def collection_path
13
+ nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class InvitationsController < ApplicationController
5
+ before_action :authenticate!
6
+
7
+ layout false
8
+
9
+ resourceful(
10
+ resource_class: Maquina::Invitation,
11
+ form_attributes: [{email: {type: :input, control_html: {class: "sm:col-span-6"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}}],
12
+ policy_class: Maquina::InvitationPolicy,
13
+ only: [:new, :create]
14
+ )
15
+
16
+ def create
17
+ authorize! with: policy_class if policy_class.present?
18
+
19
+ email = params.dig(:invitation, :email)&.strip
20
+ # management = Maquina::Current.management?
21
+
22
+ @resource = Maquina::Invitation.order(created_at: :desc).find_or_initialize_by(email: email)
23
+ if @resource.new_record?
24
+ @resource.save
25
+ elsif @resource.accepted?
26
+ @resource = Maquina::Invitation.new(email: email)
27
+ @resource.errors.add(:email, :invalid)
28
+ end
29
+
30
+ create! do |success|
31
+ success.response do
32
+ url = maquina.new_accept_invitations_url(token: CGI.escape(@resource.signed_id(purpose: :invitation, expires_in: 3.days)))
33
+ Maquina::UserNotificationsMailer.with(email: @resource.email, inviteer: Maquina::Current.user.email, url: url).invitation_email.deliver_later
34
+
35
+ flash[:notice] = {title: t("flash.#{@resource.model_name.i18n_key}.create.notice.title"), description: t("flash.#{@resource.model_name.i18n_key}.create.notice.description", email: email)}
36
+ redirect_to collection_path
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def submit_path(params = {})
44
+ invitations_path(**params)
45
+ end
46
+
47
+ def collection_path
48
+ users_path
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class PlansController < ApplicationController
5
+ # include Maquina::Resourceful # Include this module if your ApplicationController does not include it.
6
+ # include Maquina::Authenticate # Include this module if your ApplicationController does not include it.
7
+ before_action :authenticate!
8
+
9
+ # resource_class: Model Name
10
+ # form_attributes: List of attributes for edit with type
11
+ # list_attributes: List of attributes to display in index action
12
+ # show_attributes: List of attributes to display in show action
13
+ # policy_class: ActionPolicy class to check action authorization. Update this policy file.
14
+ resourceful(
15
+ resource_class: Maquina::Plan,
16
+ form_attributes: [{name: {type: :input, control_html: {class: "sm:col-span-6"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}},
17
+ {trial: {type: :input, control_html: {class: "sm:col-span-2"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}},
18
+ {price: {type: :input, control_html: {class: "sm:col-span-2"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}},
19
+ {free: {type: :checkbox, control_html: {class: "sm:col-span-6 relative flex items-start"}, input_html: {class: "check"}}},
20
+ {active: {type: :checkbox, control_html: {class: "sm:col-span-6 relative flex items-start"}, input_html: {class: "check"}}}],
21
+ list_attributes: [:name, :trial, :price, :free, :active],
22
+ show_attributes: [:name, :trial, :price, :free, :active],
23
+ policy_class: Maquina::PlanPolicy,
24
+ except: [:show, :destroy]
25
+ )
26
+
27
+ def create
28
+ create! do |success|
29
+ success.response { redirect_to edit_plan_path(resource), status: :see_other }
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def new_resource_path(options = {})
36
+ new_plan_path(**options)
37
+ end
38
+
39
+ def edit_resource_path(resource, options = {})
40
+ edit_plan_path(resource, **options)
41
+ end
42
+
43
+ def resource_path(resource)
44
+ plan_path(resource)
45
+ end
46
+
47
+ def collection_path
48
+ plans_path
49
+ end
50
+
51
+ # Only allow a list of trusted parameters through.
52
+ def secure_params
53
+ params.require(:maquina_plan).permit(:name, :trial, :price, :free, :active)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class SessionsController < ApplicationController
5
+ layout "maquina/sessions"
6
+
7
+ def new
8
+ @return_to = params[:return_to]
9
+ @return_to = nil if @return_to == "/" || @return_to == "%2F"
10
+ end
11
+
12
+ def create
13
+ user = Maquina::User.authenticate_by(email: params.dig(:email), password: params.dig(:password))
14
+
15
+ @return_to = params.dig(:return_to)
16
+ result = create_session(user, @return_to)
17
+ return redirect_to(result, status: :see_other, format: :html) if result.present?
18
+
19
+ response.status = :unprocessable_entity
20
+ flash.now.alert = t("flash.sessions.create.alert")
21
+
22
+ respond_to do |format|
23
+ format.html { render :new }
24
+ format.turbo_stream
25
+ end
26
+ end
27
+
28
+ def destroy
29
+ session["--active_session"] = nil
30
+ Maquina::Current.reset
31
+
32
+ flash.notice = t("flash.sessions.destroy.notice")
33
+ redirect_to main_app.root_path
34
+ end
35
+
36
+ private
37
+
38
+ def calculate_redirect_path(active_session)
39
+ return maquina.new_multifactor_path if active_session.user.multifactor?
40
+ return active_session.return_url if active_session.return_url.present?
41
+
42
+ active_session.user.management? ? maquina.root_path : main_app.root_path
43
+ end
44
+
45
+ def create_session(user, return_to)
46
+ return nil if user.blank?
47
+
48
+ active_session = ActiveSession.create(user: user, user_agent: request.user_agent, remote_addr: request.remote_ip, return_url: return_to)
49
+ return nil if !active_session.persisted?
50
+
51
+ session["--active_session"] = active_session.id
52
+ flash.notice = t("flash.sessions.create.notice")
53
+ calculate_redirect_path(active_session)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class UnauthorizedController < ApplicationController
5
+ def show
6
+ render :"401", status: :unauthorized
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class UsersController < ApplicationController
5
+ before_action :authenticate!
6
+
7
+ resourceful(
8
+ resource_class: User,
9
+ list_attributes: [:email, :blocked_at, :created_at],
10
+ policy_class: UserPolicy,
11
+ only: [:index]
12
+ )
13
+
14
+ private
15
+
16
+ def new_resource_path
17
+ new_invitation_path
18
+ end
19
+
20
+ def collection_path
21
+ users_path
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module ApplicationHelper
5
+ def maquina_importmap_tags(entry_point = "application", shim: true)
6
+ safe_join [
7
+ javascript_inline_importmap_tag(Maquina.configuration.importmap.to_json(resolver: self)),
8
+ javascript_importmap_module_preload_tags(Maquina.configuration.importmap),
9
+ (javascript_importmap_shim_nonce_configuration_tag if shim),
10
+ (javascript_importmap_shim_tag if shim),
11
+ javascript_import_module_tag(entry_point)
12
+ ].compact, "\n"
13
+ end
14
+
15
+ def class_to_form_frame(klass)
16
+ "#{klass.to_s.underscore.tr("/", "_")}_form"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module NavbarMenuHelper
5
+ def menu_options
6
+ options = {}
7
+ options[:plans] = maquina.plans_path if Maquina::Current.signed_in? && allowed_to?(:plans?, with: Maquina::NavigationPolicy)
8
+ options[:users] = maquina.users_path if Maquina::Current.signed_in? && allowed_to?(:users?, with: Maquina::NavigationPolicy)
9
+
10
+ options
11
+ end
12
+
13
+ def active_menu_option?(path)
14
+ request.path == path
15
+ end
16
+
17
+ def profile_menu_options
18
+ if Maquina::Current.signed_in?
19
+ {
20
+ signout: {method: :delete, path: sessions_path}
21
+ }
22
+ else
23
+ {
24
+ signin: new_sessions_path
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module ViewsHelper
5
+ def brand_icon
6
+ asset_path("maquina/maquina.svg")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module Maquina
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: I18n.t("mailers.default_from")
6
+ layout "maquina/mailer"
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ class UserNotificationsMailer < ApplicationMailer
5
+ def invitation_email
6
+ @email = params[:email]
7
+ @url = params[:url]
8
+ @inviteer = params[:inviteer]
9
+ @org = params[:org] || I18n.t("application_name")
10
+
11
+ subject = I18n.t("mailers.invitation_email.subject", org: @org)
12
+
13
+ mail(to: @email, subject: subject)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module AuthenticateBy
5
+ extend ActiveSupport::Concern
6
+ included do
7
+ add_authenticate_by
8
+ end
9
+
10
+ class_methods do
11
+ def add_authenticate_by
12
+ version = Gem::Version.new(Rails::VERSION::STRING)
13
+ return if version.to_s.to_f >= 7.1
14
+
15
+ Rails.logger.warn "[#{self}] Adding class method authenticate_by"
16
+ self.class.define_method(:authenticate_by) do |attributes|
17
+ passwords, identifiers = attributes.to_h.partition do |name, value|
18
+ !has_attribute?(name) && has_attribute?("#{name}_digest")
19
+ end.map(&:to_h)
20
+
21
+ raise ArgumentError, "One or more password arguments are required" if passwords.empty?
22
+ raise ArgumentError, "One or more finder arguments are required" if identifiers.empty?
23
+ if (record = find_by(identifiers))
24
+ record if passwords.count { |name, value| record.public_send(:"authenticate_#{name}", value) } == passwords.size
25
+ else
26
+ new(passwords)
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Blockeable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ if has_attribute?(:blocked_at) && has_attribute?(:temporary_blocked_at)
9
+ define_method(:blocked?) do
10
+ temporary_blocked_until = nil
11
+ if Maquina.configuration.temporary_block.present? && temporary_blocked_at.present?
12
+ temporary_blocked_until = temporary_blocked_at.since(Maquina.configuration.temporary_block)
13
+ end
14
+
15
+ blocked_at.present? || (temporary_blocked_until.present? && temporary_blocked_until > Time.zone.now)
16
+ end
17
+
18
+ scope :unblocked, -> { where(blocked_at: nil).where("(coalesce(temporary_blocked_at + interval '? minutes', now())) <= now()", Maquina.configuration.temporary_block&.in_minutes&.to_i || 0) }
19
+ elsif has_attribute(:blocked_at)
20
+ define_method(:blocked?) do
21
+ blocked_at.present?
22
+ end
23
+
24
+ scope :unblocked, -> { where(blocked_at: nil) }
25
+ end
26
+ end
27
+ end
28
+ end