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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/images/maquina/maquina.svg +18 -0
- data/app/assets/javascripts/maquina/application.js +4 -0
- data/app/assets/javascripts/maquina/controllers/alert_controller.js +29 -0
- data/app/assets/javascripts/maquina/controllers/application.js +9 -0
- data/app/assets/javascripts/maquina/controllers/file_controller.js +60 -0
- data/app/assets/javascripts/maquina/controllers/index.js +11 -0
- data/app/assets/javascripts/maquina/controllers/mobile_menu_controller.js +31 -0
- data/app/assets/javascripts/maquina/controllers/modal_controller.js +39 -0
- data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +15 -0
- data/app/assets/javascripts/maquina/controllers/popup_menu_controller.js +17 -0
- data/app/assets/javascripts/maquina/controllers/submit_form_controller.js +11 -0
- data/app/assets/stylesheets/maquina/application.css +15 -0
- data/app/assets/stylesheets/maquina/application.tailwind.css +102 -0
- data/app/controllers/concerns/maquina/authenticate.rb +41 -0
- data/app/controllers/concerns/maquina/create.rb +27 -0
- data/app/controllers/concerns/maquina/destroy.rb +28 -0
- data/app/controllers/concerns/maquina/edit.rb +29 -0
- data/app/controllers/concerns/maquina/index.rb +33 -0
- data/app/controllers/concerns/maquina/new.rb +22 -0
- data/app/controllers/concerns/maquina/resourceful.rb +180 -0
- data/app/controllers/concerns/maquina/show.rb +27 -0
- data/app/controllers/concerns/maquina/update.rb +31 -0
- data/app/controllers/maquina/accept_invitations_controller.rb +28 -0
- data/app/controllers/maquina/application_controller.rb +19 -0
- data/app/controllers/maquina/dashboard_controller.rb +16 -0
- data/app/controllers/maquina/invitations_controller.rb +51 -0
- data/app/controllers/maquina/plans_controller.rb +56 -0
- data/app/controllers/maquina/sessions_controller.rb +56 -0
- data/app/controllers/maquina/unauthorized_controller.rb +9 -0
- data/app/controllers/maquina/users_controller.rb +24 -0
- data/app/helpers/maquina/application_helper.rb +19 -0
- data/app/helpers/maquina/navbar_menu_helper.rb +29 -0
- data/app/helpers/maquina/views_helper.rb +9 -0
- data/app/jobs/maquina/application_job.rb +4 -0
- data/app/mailers/maquina/application_mailer.rb +8 -0
- data/app/mailers/maquina/user_notifications_mailer.rb +16 -0
- data/app/models/concerns/maquina/authenticate_by.rb +33 -0
- data/app/models/concerns/maquina/blockeable.rb +28 -0
- data/app/models/concerns/maquina/multifactor.rb +80 -0
- data/app/models/concerns/maquina/retain_passwords.rb +32 -0
- data/app/models/concerns/maquina/searchable.rb +24 -0
- data/app/models/maquina/active_session.rb +33 -0
- data/app/models/maquina/application_record.rb +11 -0
- data/app/models/maquina/current.rb +21 -0
- data/app/models/maquina/invitation.rb +15 -0
- data/app/models/maquina/plan.rb +38 -0
- data/app/models/maquina/used_password.rb +17 -0
- data/app/models/maquina/user.rb +33 -0
- data/app/policies/maquina/application_policy.rb +50 -0
- data/app/policies/maquina/invitation_policy.rb +13 -0
- data/app/policies/maquina/navigation_policy.rb +13 -0
- data/app/policies/maquina/plan_policy.rb +49 -0
- data/app/policies/maquina/user_policy.rb +27 -0
- data/app/views/layouts/maquina/application.html.erb +26 -0
- data/app/views/layouts/maquina/mailer.html.erb +377 -0
- data/app/views/layouts/maquina/mailer.text.erb +12 -0
- data/app/views/layouts/maquina/sessions.html.erb +24 -0
- data/app/views/maquina/accept_invitations/new.html.erb +9 -0
- data/app/views/maquina/accept_invitations/new_view.rb +41 -0
- data/app/views/maquina/application/_navbar.html.erb +21 -0
- data/app/views/maquina/application/alert.rb +104 -0
- data/app/views/maquina/application/components/action_text_component.rb +20 -0
- data/app/views/maquina/application/components/checkbox_component.rb +21 -0
- data/app/views/maquina/application/components/component_base.rb +60 -0
- data/app/views/maquina/application/components/file_component.rb +59 -0
- data/app/views/maquina/application/components/input_component.rb +20 -0
- data/app/views/maquina/application/components/select_component.rb +44 -0
- data/app/views/maquina/application/create.turbo_stream.erb +11 -0
- data/app/views/maquina/application/edit.html.erb +9 -0
- data/app/views/maquina/application/edit.rb +17 -0
- data/app/views/maquina/application/form.rb +77 -0
- data/app/views/maquina/application/index.html.erb +10 -0
- data/app/views/maquina/application/index_header.rb +46 -0
- data/app/views/maquina/application/index_modal.rb +43 -0
- data/app/views/maquina/application/index_table.rb +121 -0
- data/app/views/maquina/application/new.html.erb +9 -0
- data/app/views/maquina/application/new.rb +18 -0
- data/app/views/maquina/application/sessions_header.rb +31 -0
- data/app/views/maquina/application/show.html.erb +1 -0
- data/app/views/maquina/application/update.turbo_stream.erb +11 -0
- data/app/views/maquina/application_view.rb +46 -0
- data/app/views/maquina/dashboard/index.html.erb +0 -0
- data/app/views/maquina/invitations/create.turbo_stream.erb +13 -0
- data/app/views/maquina/invitations/new.html.erb +3 -0
- data/app/views/maquina/navbar/menu.rb +62 -0
- data/app/views/maquina/navbar/menu_item_link.rb +34 -0
- data/app/views/maquina/navbar/mobile_button.rb +29 -0
- data/app/views/maquina/navbar/mobile_menu.rb +47 -0
- data/app/views/maquina/navbar/notification.rb +37 -0
- data/app/views/maquina/navbar/profile.rb +16 -0
- data/app/views/maquina/navbar/profile_button.rb +24 -0
- data/app/views/maquina/navbar/profile_menu.rb +108 -0
- data/app/views/maquina/navbar/profile_menu_item_link.rb +41 -0
- data/app/views/maquina/navbar/search.rb +40 -0
- data/app/views/maquina/navbar/title.rb +22 -0
- data/app/views/maquina/sessions/create.turbo_stream.erb +11 -0
- data/app/views/maquina/sessions/form.rb +56 -0
- data/app/views/maquina/sessions/new.html.erb +9 -0
- data/app/views/maquina/unauthorized/401.html.erb +1 -0
- data/app/views/maquina/user_notifications_mailer/invitation_email.html.erb +40 -0
- data/app/views/maquina/user_notifications_mailer/invitation_email.text.erb +12 -0
- data/config/definitions.rb +1 -0
- data/config/initializers/importmap.rb +17 -0
- data/config/initializers/money.rb +116 -0
- data/config/initializers/pagy.rb +235 -0
- data/config/locales/flash.en.yml +44 -0
- data/config/locales/forms.en.yml +58 -0
- data/config/locales/mailers.en.yml +35 -0
- data/config/locales/models.en.yml +34 -0
- data/config/locales/routes.en.yml +7 -0
- data/config/locales/views.en.yml +45 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20221109010726_create_maquina_plans.rb +13 -0
- data/db/migrate/20221113000409_create_maquina_users.rb +19 -0
- data/db/migrate/20221113020108_create_maquina_used_passwords.rb +10 -0
- data/db/migrate/20221115223414_create_maquina_active_sessions.rb +15 -0
- data/db/migrate/20230201203922_create_maquina_invitations.rb +12 -0
- data/db/schema.rb +1 -0
- data/lib/generators/maquina/install_generator.rb +32 -0
- data/lib/generators/maquina/install_templates/install_templates_generator.rb +31 -0
- data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +11 -0
- data/lib/generators/maquina/tailwind_config/templates/app/assets/config/maquina/tailwind.config.js.tt +68 -0
- data/lib/generators/maquina/templates/config/initializers/maquina.rb +3 -0
- data/lib/maquina/engine.rb +17 -0
- data/lib/maquina/version.rb +3 -0
- data/lib/maquina.rb +48 -0
- data/lib/tasks/install.rake +19 -0
- data/lib/tasks/maquina_tasks.rake +4 -0
- data/lib/tasks/tailwind.rake +25 -0
- 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,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,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,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
|