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