maquina 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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