maquina 0.5.2 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -35
- data/app/assets/javascripts/controllers/alert_controller.js +22 -0
- data/app/assets/stylesheets/maquina/application.tailwind.css +4 -101
- data/app/assets/stylesheets/maquina.css +114 -0
- data/app/controllers/concerns/maquina/authenticate.rb +29 -1
- data/app/controllers/concerns/maquina/index.rb +81 -2
- data/app/controllers/concerns/maquina/resourceful.rb +95 -9
- data/app/controllers/maquina/application_controller.rb +1 -1
- data/app/controllers/maquina/dashboard_controller.rb +10 -5
- data/app/controllers/maquina/invitations_controller.rb +0 -2
- data/app/controllers/maquina/plans_controller.rb +13 -23
- data/app/controllers/maquina/sessions_controller.rb +1 -1
- data/app/controllers/maquina/users_controller.rb +0 -2
- data/app/helpers/maquina/application_helper.rb +0 -8
- data/app/helpers/maquina/navbar_menu_helper.rb +1 -1
- data/app/models/concerns/maquina/blockeable.rb +64 -0
- data/app/models/concerns/maquina/organization_scoped.rb +26 -0
- data/app/models/concerns/maquina/retain_passwords.rb +44 -0
- data/app/models/concerns/maquina/sqlite_search.rb +92 -0
- data/app/models/concerns/maquina/user_scoped.rb +26 -0
- data/app/models/maquina/active_session.rb +40 -0
- data/app/models/maquina/current.rb +39 -2
- data/app/models/maquina/invitation.rb +28 -0
- data/app/models/maquina/membership.rb +23 -1
- data/app/models/maquina/organization.rb +19 -2
- data/app/models/maquina/plan.rb +26 -6
- data/app/models/maquina/used_password.rb +30 -0
- data/app/models/maquina/user.rb +50 -8
- data/app/policies/maquina/application_policy.rb +1 -1
- data/app/policies/maquina/dashboard_policy.rb +7 -0
- data/app/policies/maquina/navigation_policy.rb +2 -0
- data/app/views/layouts/maquina/application.html.erb +2 -2
- data/app/views/layouts/maquina/sessions.html.erb +1 -1
- data/app/views/maquina/application/_navbar.html.erb +8 -3
- data/app/views/maquina/application/components/checkbox_component.rb +3 -3
- data/app/views/maquina/application/components/component_base.rb +3 -2
- data/app/views/maquina/application/edit.html.erb +10 -7
- data/app/views/maquina/application/filters.rb +118 -0
- data/app/views/maquina/application/index.html.erb +6 -3
- data/app/views/maquina/application/index_header.rb +5 -2
- data/app/views/maquina/application/index_table.rb +71 -6
- data/app/views/maquina/application/index_tools.rb +17 -0
- data/app/views/maquina/application/new.html.erb +10 -7
- data/app/views/maquina/application/search.rb +42 -0
- data/app/views/maquina/application/sessions_header.rb +3 -9
- data/app/views/maquina/dashboard/index.html.erb +4 -0
- data/app/views/maquina/dashboard/stats.rb +35 -0
- data/app/views/maquina/dashboard/tasks.rb +124 -0
- data/app/views/maquina/first_runs/form.rb +0 -2
- data/app/views/maquina/first_runs/show.html.erb +4 -1
- data/app/views/maquina/navbar/title.rb +4 -2
- data/config/importmap.rb +1 -13
- data/config/locales/flash.en.yml +6 -0
- data/config/locales/flash.es.yml +6 -0
- data/config/locales/forms.en.yml +33 -4
- data/config/locales/forms.es.yml +22 -11
- data/config/locales/models.en.yml +10 -0
- data/config/locales/models.es.yml +10 -0
- data/config/locales/views.en.yml +33 -5
- data/config/locales/views.es.yml +28 -10
- data/config/routes.rb +1 -0
- data/db/migrate/20221109010726_create_maquina_plans.rb +1 -1
- data/db/migrate/20221113000409_create_maquina_users.rb +1 -1
- data/db/migrate/20221113020108_create_maquina_used_passwords.rb +1 -1
- data/db/migrate/20221115223414_create_maquina_active_sessions.rb +1 -3
- data/db/migrate/20230201203922_create_maquina_invitations.rb +1 -1
- data/db/migrate/20230829183530_create_maquina_organizations.rb +1 -1
- data/db/migrate/20230829192656_create_maquina_memberships.rb +2 -4
- data/db/migrate/20241109191405_add_counter_cache_to_plans.rb +5 -0
- data/lib/generators/maquina/install_generator.rb +67 -1
- data/lib/generators/maquina/tailwind_config/templates/lib/generators/tailwind_config/templates/config/tailwind.config.js.tt +9 -5
- data/lib/generators/maquina/tailwind_config/templates/lib/tasks/tailwind.rake.tt +2 -0
- data/lib/generators/maquina/templates/config/initializers/maquina.rb.tt +2 -1
- data/lib/maquina/engine.rb +6 -3
- data/lib/maquina/version.rb +1 -1
- data/lib/maquina.rb +2 -9
- metadata +20 -76
- data/app/assets/javascripts/maquina/application.js +0 -4
- data/app/assets/javascripts/maquina/controllers/alert_controller.js +0 -29
- data/app/assets/javascripts/maquina/controllers/application.js +0 -9
- data/app/assets/javascripts/maquina/controllers/index.js +0 -11
- data/app/models/concerns/maquina/authenticate_by.rb +0 -33
- data/app/views/maquina/navbar/search.rb +0 -40
- data/lib/generators/maquina/install_templates/install_templates_generator.rb +0 -31
- /data/app/assets/javascripts/{maquina/controllers → controllers}/backdrop_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/file_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/mobile_menu_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/modal_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/modal_open_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/popup_menu_controller.js +0 -0
- /data/app/assets/javascripts/{maquina/controllers → controllers}/submit_form_controller.js +0 -0
@@ -1,30 +1,92 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Maquina
|
4
|
+
##
|
5
|
+
# The Resourceful concern provides RESTful controller actions and helper methods
|
6
|
+
# for Rails controllers. It implements common CRUD operations and follows
|
7
|
+
# Rails conventions while allowing customization of actions, attributes, and policies.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# class PlansController < ApplicationController
|
11
|
+
# resourceful(
|
12
|
+
# resource_class: Plan,
|
13
|
+
# form_attributes: [{name: {type: :input}}],
|
14
|
+
# list_attributes: [:name, :price],
|
15
|
+
# policy_class: PlanPolicy
|
16
|
+
# )
|
17
|
+
# end
|
18
|
+
#
|
4
19
|
module Resourceful
|
5
20
|
extend ActiveSupport::Concern
|
6
21
|
|
22
|
+
##
|
23
|
+
# Internal class to handle success/failure response blocks in dual actions
|
7
24
|
class ResourceResponse
|
25
|
+
# Stores the response block
|
8
26
|
def response(&block)
|
9
27
|
@block = block
|
10
28
|
end
|
11
29
|
|
30
|
+
##
|
31
|
+
# Returns the stored block
|
12
32
|
def code
|
13
33
|
@block
|
14
34
|
end
|
15
35
|
end
|
16
36
|
|
17
37
|
included do
|
38
|
+
##
|
39
|
+
# The model class this controller manages
|
18
40
|
class_attribute :resource_class, instance_writer: false
|
41
|
+
|
42
|
+
##
|
43
|
+
# Optional namespace for route generation
|
19
44
|
class_attribute :namespace, instance_writer: false
|
45
|
+
|
46
|
+
##
|
47
|
+
# The parameter to use for finding resources (defaults to :id)
|
20
48
|
class_attribute :find_by_param, instance_writer: false
|
49
|
+
|
50
|
+
##
|
51
|
+
# Attributes to display in index views. It is a list of symbols.
|
21
52
|
class_attribute :list_attributes, instance_writer: false
|
53
|
+
|
54
|
+
##
|
55
|
+
# Attributes for form rendering with their configurations. It is a list of hashes.
|
56
|
+
# Each hash must contain a key for the attribute name and a value for the configuration.
|
57
|
+
# The configuration can be a hash with the following keys:
|
58
|
+
# - :type: The type of input to render. It can be :input, :checkbox, :textarea
|
59
|
+
# - :control_html: HTML attributes for the input control. It is a hash.
|
60
|
+
# - :input_html: HTML attributes for the input. It is a hash.
|
22
61
|
class_attribute :form_attributes, instance_writer: false
|
62
|
+
|
63
|
+
##
|
64
|
+
# Attributes to display in show views. It is a list of symbols.
|
23
65
|
class_attribute :show_attributes, instance_writer: false
|
66
|
+
|
67
|
+
##
|
68
|
+
# The ActionPolicy class for authorization
|
24
69
|
class_attribute :policy_class, instance_writer: false
|
70
|
+
|
71
|
+
##
|
72
|
+
# Whether to show the resource link in the index view
|
25
73
|
class_attribute :show_link, instance_writer: false
|
26
74
|
|
27
|
-
|
75
|
+
##
|
76
|
+
# The filters to apply to the index action
|
77
|
+
class_attribute :filters, instance_writer: false
|
78
|
+
|
79
|
+
##
|
80
|
+
# The sortable attributes for the index action
|
81
|
+
class_attribute :sortable, instance_writer: false
|
82
|
+
|
83
|
+
##
|
84
|
+
# The current resource instance
|
85
|
+
attr_reader :resource
|
86
|
+
|
87
|
+
##
|
88
|
+
# The resource collection for index action
|
89
|
+
attr_reader :collection
|
28
90
|
|
29
91
|
protected
|
30
92
|
|
@@ -39,22 +101,32 @@ module Maquina
|
|
39
101
|
end
|
40
102
|
|
41
103
|
def base_plural_path_name
|
104
|
+
return nil if resource_class.nil?
|
42
105
|
@base_plural_path_name ||= "#{namespace_path}#{resource_class.model_name.route_key}_path"
|
43
106
|
end
|
44
107
|
|
108
|
+
##
|
109
|
+
# Generates the path for a new resource
|
45
110
|
def new_resource_path(options = {})
|
46
|
-
Rails.application.routes.url_helpers.send("new_#{base_singular_path_name}", **options)
|
111
|
+
Rails.application.routes.url_helpers.send(:"new_#{base_singular_path_name}", **options)
|
47
112
|
end
|
48
113
|
|
114
|
+
##
|
115
|
+
# Generates the edit path for a resource
|
49
116
|
def edit_resource_path(resource, options = {})
|
50
|
-
Rails.application.routes.url_helpers.send("edit_#{base_singular_path_name}", resource, **options)
|
117
|
+
Rails.application.routes.url_helpers.send(:"edit_#{base_singular_path_name}", resource, **options)
|
51
118
|
end
|
52
119
|
|
120
|
+
##
|
121
|
+
# Generates the path for a resource
|
53
122
|
def resource_path(resource)
|
54
123
|
Rails.application.routes.url_helpers.send(base_singular_path_name, resource)
|
55
124
|
end
|
56
125
|
|
126
|
+
##
|
127
|
+
# Generates the collection path
|
57
128
|
def collection_path(params = {})
|
129
|
+
return nil if resource_class.nil?
|
58
130
|
Rails.application.routes.url_helpers.send(base_plural_path_name, **params)
|
59
131
|
end
|
60
132
|
|
@@ -64,7 +136,7 @@ module Maquina
|
|
64
136
|
# Controller secure params
|
65
137
|
def resource_secure_params
|
66
138
|
method_name = :secure_params
|
67
|
-
method_action_name = "#{action_name}_#{method_name}"
|
139
|
+
method_action_name = :"#{action_name}_#{method_name}"
|
68
140
|
|
69
141
|
if respond_to?(method_action_name, true)
|
70
142
|
send(method_action_name)
|
@@ -96,7 +168,7 @@ module Maquina
|
|
96
168
|
|
97
169
|
success = ResourceResponse.new
|
98
170
|
failure = ResourceResponse.new
|
99
|
-
|
171
|
+
yield success, failure
|
100
172
|
|
101
173
|
if has_errors
|
102
174
|
failure.code.call
|
@@ -108,7 +180,7 @@ module Maquina
|
|
108
180
|
responded = true
|
109
181
|
|
110
182
|
success = ResourceResponse.new
|
111
|
-
|
183
|
+
yield success
|
112
184
|
|
113
185
|
success.code.call
|
114
186
|
end
|
@@ -118,7 +190,6 @@ module Maquina
|
|
118
190
|
respond_to do |format|
|
119
191
|
if has_errors
|
120
192
|
format.html { render object.persisted? ? :edit : :new }
|
121
|
-
# format.turbo_stream
|
122
193
|
format.json { render json: {errors: object.errors.map { |error| {error.attribute => error.message} }} }
|
123
194
|
else
|
124
195
|
format.html { redirect_to collection_path, status: :see_other }
|
@@ -138,16 +209,31 @@ module Maquina
|
|
138
209
|
end
|
139
210
|
|
140
211
|
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, :show_link
|
212
|
+
:collection_path, :resource_path, :new_resource_path, :edit_resource_path, :submit_path, :show_link, :filters,
|
213
|
+
:sortable
|
142
214
|
end
|
143
215
|
|
144
216
|
class_methods do
|
145
|
-
|
217
|
+
##
|
218
|
+
# Configures the resourceful behavior for the controller
|
219
|
+
#
|
220
|
+
# Example:
|
221
|
+
# resourceful(
|
222
|
+
# resource_class: User,
|
223
|
+
# list_attributes: [:email, :created_at],
|
224
|
+
# policy_class: UserPolicy,
|
225
|
+
# only: [:index, :show]
|
226
|
+
# )
|
227
|
+
def resourceful(resource_class: nil, namespace: nil, find_by_param: :id, only: [], except: [],
|
228
|
+
list_attributes: [], form_attributes: [], show_attributes: [], policy_class: nil, show_link: nil, filters: {},
|
229
|
+
sortable: [])
|
146
230
|
self.resource_class = resource_class || controller_path.classify.safe_constantize
|
147
231
|
self.find_by_param = find_by_param || :id
|
148
232
|
self.list_attributes = Array(list_attributes).compact
|
149
233
|
self.form_attributes = Array(form_attributes).compact
|
150
234
|
self.show_attributes = Array(show_attributes).compact
|
235
|
+
self.filters = filters
|
236
|
+
self.sortable = sortable
|
151
237
|
self.policy_class = policy_class
|
152
238
|
self.show_link = show_link
|
153
239
|
self.namespace = namespace
|
@@ -2,15 +2,20 @@
|
|
2
2
|
|
3
3
|
module Maquina
|
4
4
|
class DashboardController < ApplicationController
|
5
|
-
before_action :authenticate!
|
6
|
-
|
7
5
|
def index
|
6
|
+
if Maquina::Current.signed_in? && Maquina::Current.management?
|
7
|
+
@stats = [
|
8
|
+
{label: t("maquina.dashboard.index.stats.total_plans"), value: Maquina::Plan.count},
|
9
|
+
{label: t("maquina.dashboard.index.stats.total_organizations"), value: Maquina::Organization.count}
|
10
|
+
]
|
11
|
+
end
|
8
12
|
end
|
9
13
|
|
10
|
-
|
14
|
+
private
|
11
15
|
|
12
|
-
def
|
13
|
-
|
16
|
+
def policy_class
|
17
|
+
Maquina::DashboardPolicy
|
14
18
|
end
|
19
|
+
helper_method :policy_class
|
15
20
|
end
|
16
21
|
end
|
@@ -2,15 +2,6 @@
|
|
2
2
|
|
3
3
|
module Maquina
|
4
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
5
|
resourceful(
|
15
6
|
resource_class: Maquina::Plan,
|
16
7
|
form_attributes: [{name: {type: :input, control_html: {class: "sm:col-span-6"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}},
|
@@ -18,18 +9,13 @@ module Maquina
|
|
18
9
|
{price: {type: :input, control_html: {class: "sm:col-span-2"}, input_html: {class: "block w-full min-w-0 flex-1 input"}}},
|
19
10
|
{free: {type: :checkbox, control_html: {class: "sm:col-span-6 relative flex items-start"}, input_html: {class: "check"}}},
|
20
11
|
{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],
|
12
|
+
list_attributes: [:name, :organizations_count, :trial, :price, :free, :active, :updated_at],
|
23
13
|
policy_class: Maquina::PlanPolicy,
|
14
|
+
filters: {free: {Yes: true, No: false}},
|
15
|
+
sortable: [:name, :price, :updated_at, :organizations_count],
|
24
16
|
except: [:show, :destroy]
|
25
17
|
)
|
26
18
|
|
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
19
|
private
|
34
20
|
|
35
21
|
def new_resource_path(options = {})
|
@@ -40,17 +26,21 @@ module Maquina
|
|
40
26
|
edit_plan_path(resource, **options)
|
41
27
|
end
|
42
28
|
|
43
|
-
def resource_path(resource)
|
44
|
-
plan_path(resource)
|
29
|
+
def resource_path(resource, options = {})
|
30
|
+
plan_path(resource, **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def collection_path(params = {})
|
34
|
+
plans_path(**params)
|
45
35
|
end
|
46
36
|
|
47
|
-
def
|
48
|
-
|
37
|
+
def before_order_by(attribute, direction)
|
38
|
+
attribute = :price_cents if attribute == :price
|
39
|
+
[attribute, direction]
|
49
40
|
end
|
50
41
|
|
51
|
-
# Only allow a list of trusted parameters through.
|
52
42
|
def secure_params
|
53
|
-
params.require(:
|
43
|
+
params.require(:plan).permit(:name, :trial, :price, :free, :active)
|
54
44
|
end
|
55
45
|
end
|
56
46
|
end
|
@@ -2,14 +2,6 @@
|
|
2
2
|
|
3
3
|
module Maquina
|
4
4
|
module ApplicationHelper
|
5
|
-
def maquina_importmap_tags(entry_point = "application", importmap: Maquina.configuration.importmap)
|
6
|
-
safe_join [
|
7
|
-
javascript_inline_importmap_tag(importmap.to_json(resolver: self)),
|
8
|
-
javascript_importmap_module_preload_tags(importmap),
|
9
|
-
javascript_import_module_tag(entry_point)
|
10
|
-
].compact, "\n"
|
11
|
-
end
|
12
|
-
|
13
5
|
def class_to_form_frame(klass)
|
14
6
|
"#{klass.to_s.underscore.tr("/", "_")}_form"
|
15
7
|
end
|
@@ -1,6 +1,70 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Maquina
|
4
|
+
##
|
5
|
+
# A concern that implements blocking functionality for models.
|
6
|
+
# Supports both permanent and temporary blocking mechanisms.
|
7
|
+
#
|
8
|
+
# == Usage
|
9
|
+
#
|
10
|
+
# Include this concern in models that need blocking capability:
|
11
|
+
#
|
12
|
+
# class User < ApplicationRecord
|
13
|
+
# include Maquina::Blockeable
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# == Required Attributes
|
17
|
+
#
|
18
|
+
# Models must have one or both of:
|
19
|
+
# - +blocked_at+:: Timestamp for permanent blocks
|
20
|
+
# - +temporary_blocked_at+:: Timestamp for temporary blocks
|
21
|
+
#
|
22
|
+
# == Configuration
|
23
|
+
#
|
24
|
+
# Temporary blocking duration is controlled by:
|
25
|
+
# - +Maquina.configuration.temporary_block+:: Duration of temporary blocks
|
26
|
+
#
|
27
|
+
# == Special Behavior for User Model
|
28
|
+
#
|
29
|
+
# When included in Maquina::User:
|
30
|
+
# - Adds +memberships_blocked?+ method to check if all user's memberships are blocked
|
31
|
+
# - Excludes management users from membership blocking checks
|
32
|
+
#
|
33
|
+
# == Scopes
|
34
|
+
#
|
35
|
+
# === unblocked
|
36
|
+
#
|
37
|
+
# Returns records that are not blocked (either permanently or temporarily)
|
38
|
+
#
|
39
|
+
# With both blocked_at and temporary_blocked_at:
|
40
|
+
# scope :unblocked, -> { where(blocked_at: nil).where("(coalesce(temporary_blocked_at + interval '? minutes', now())) <= now()", ...) }
|
41
|
+
#
|
42
|
+
# With only blocked_at:
|
43
|
+
# scope :unblocked, -> { where(blocked_at: nil) }
|
44
|
+
#
|
45
|
+
# == Instance Methods
|
46
|
+
#
|
47
|
+
# === blocked?
|
48
|
+
#
|
49
|
+
# Returns true if the record is blocked by any mechanism:
|
50
|
+
# - Has permanent block (blocked_at present)
|
51
|
+
# - Has active temporary block
|
52
|
+
# - All memberships are blocked (User model only)
|
53
|
+
#
|
54
|
+
# === memberships_blocked?
|
55
|
+
#
|
56
|
+
# Only available for User model. Returns true if user has no active unblocked memberships.
|
57
|
+
#
|
58
|
+
# == Example
|
59
|
+
#
|
60
|
+
# class User < ApplicationRecord
|
61
|
+
# include Maquina::Blockeable
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# user.update(blocked_at: Time.current)
|
65
|
+
# user.blocked? # => true
|
66
|
+
# User.unblocked # => excludes blocked users
|
67
|
+
#
|
4
68
|
module Blockeable
|
5
69
|
extend ActiveSupport::Concern
|
6
70
|
|
@@ -1,6 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Maquina
|
4
|
+
##
|
5
|
+
# A concern that adds organization association to models.
|
6
|
+
#
|
7
|
+
# == Usage
|
8
|
+
#
|
9
|
+
# Include this concern in any model that needs to be scoped to an organization:
|
10
|
+
#
|
11
|
+
# class MyModel < ApplicationRecord
|
12
|
+
# include Maquina::OrganizationScoped
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# == Associations
|
16
|
+
#
|
17
|
+
# When included, automatically adds:
|
18
|
+
# - +organization+:: Belongs to an Organization through maquina_organization_id foreign key
|
19
|
+
#
|
20
|
+
# == Example
|
21
|
+
#
|
22
|
+
# class Project < ApplicationRecord
|
23
|
+
# include Maquina::OrganizationScoped
|
24
|
+
# # Project now has organization association
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# project = Project.create(organization: current_organization)
|
28
|
+
# project.organization # => returns associated Maquina::Organization
|
29
|
+
#
|
4
30
|
module OrganizationScoped
|
5
31
|
extend ActiveSupport::Concern
|
6
32
|
|
@@ -1,6 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Maquina
|
4
|
+
##
|
5
|
+
# A concern that implements password history and reuse prevention.
|
6
|
+
#
|
7
|
+
# == Usage
|
8
|
+
#
|
9
|
+
# Include this concern in models that need password history tracking:
|
10
|
+
#
|
11
|
+
# class User < ApplicationRecord
|
12
|
+
# include Maquina::RetainPasswords
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# == Configuration
|
16
|
+
#
|
17
|
+
# Password retention behavior is controlled by:
|
18
|
+
# - +Maquina.configuration.password_retain_count+:: Number of previous passwords to retain
|
19
|
+
#
|
20
|
+
# == Callbacks
|
21
|
+
#
|
22
|
+
# When included, automatically adds:
|
23
|
+
# - Validation to prevent password reuse
|
24
|
+
# - After create: Stores initial password in history
|
25
|
+
# - After update: Stores new password in history when password changes
|
26
|
+
#
|
27
|
+
# == Validations
|
28
|
+
#
|
29
|
+
# - +password+:: Must not match any previously used passwords within retention limit
|
30
|
+
#
|
31
|
+
# == Example
|
32
|
+
#
|
33
|
+
# class User < ApplicationRecord
|
34
|
+
# include Maquina::RetainPasswords
|
35
|
+
# has_secure_password
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# user.update(password: 'old_password') # Stored in history
|
39
|
+
# user.update(password: 'old_password') # Validation error: password already used
|
40
|
+
#
|
4
41
|
module RetainPasswords
|
5
42
|
extend ActiveSupport::Concern
|
6
43
|
|
@@ -11,6 +48,10 @@ module Maquina
|
|
11
48
|
|
12
49
|
private
|
13
50
|
|
51
|
+
# Validates that the new password hasn't been used before
|
52
|
+
#
|
53
|
+
# Compares the new password against stored password history
|
54
|
+
# Adds error if password was previously used within retention limit
|
14
55
|
def password_uniqueness
|
15
56
|
return if Maquina.configuration.password_retain_count.blank? || Maquina.configuration.password_retain_count.zero?
|
16
57
|
|
@@ -24,6 +65,9 @@ module Maquina
|
|
24
65
|
errors.add(:password, :password_already_used) if used_before.present?
|
25
66
|
end
|
26
67
|
|
68
|
+
# Stores the current password digest in password history
|
69
|
+
#
|
70
|
+
# Delegates to UsedPassword to handle storage and history maintenance
|
27
71
|
def store_password_digest
|
28
72
|
Maquina::UsedPassword.store_password_digest(id, password_digest)
|
29
73
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Maquina
|
2
|
+
module SqliteSearch
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private def update_search_index
|
6
|
+
primary_key = self.class.primary_key
|
7
|
+
table_name = self.class.table_name
|
8
|
+
foreign_key = self.class.to_s.foreign_key
|
9
|
+
|
10
|
+
search_attrs = @@search_scope_attrs.each_with_object({}) { |attr, acc|
|
11
|
+
acc[attr] = quote_string(send(attr) || "")
|
12
|
+
}
|
13
|
+
id_value = attributes[primary_key]
|
14
|
+
|
15
|
+
sql_delete = <<~SQL.strip
|
16
|
+
DELETE FROM fts_#{table_name} WHERE #{foreign_key} = #{id_value};
|
17
|
+
SQL
|
18
|
+
self.class.connection.execute(sql_delete)
|
19
|
+
|
20
|
+
sql_insert = <<~SQL.strip
|
21
|
+
INSERT INTO fts_#{table_name}(#{search_attrs.keys.join(", ")}, #{foreign_key})
|
22
|
+
VALUES (#{search_attrs.values.map { |value| "'#{value}'" }.join(", ")}, #{attributes[primary_key]});
|
23
|
+
SQL
|
24
|
+
self.class.connection.execute(sql_insert)
|
25
|
+
end
|
26
|
+
|
27
|
+
private def delete_search_index
|
28
|
+
primary_key = self.class.primary_key
|
29
|
+
table_name = self.class.table_name
|
30
|
+
foreign_key = self.class.to_s.foreign_key
|
31
|
+
id_value = attributes[primary_key]
|
32
|
+
|
33
|
+
sql_delete = <<~SQL.strip
|
34
|
+
DELETE FROM fts_#{table_name} WHERE #{foreign_key} = #{id_value};
|
35
|
+
SQL
|
36
|
+
self.class.connection.execute(sql_delete)
|
37
|
+
end
|
38
|
+
|
39
|
+
included do
|
40
|
+
after_save_commit :update_search_index
|
41
|
+
after_destroy_commit :delete_search_index
|
42
|
+
|
43
|
+
scope_foreign_key = to_s.foreign_key
|
44
|
+
scope :full_search, ->(query) {
|
45
|
+
return none if query.blank?
|
46
|
+
|
47
|
+
sql = <<~SQL.strip
|
48
|
+
SELECT #{scope_foreign_key} AS id FROM fts_#{table_name}
|
49
|
+
WHERE fts_#{table_name} = '#{query}' ORDER BY rank;
|
50
|
+
SQL
|
51
|
+
ids = connection.execute(sql).map(&:values).flatten
|
52
|
+
where(id: ids)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
class_methods do
|
57
|
+
def search_scope(*attrs)
|
58
|
+
@@search_scope_attrs = attrs
|
59
|
+
end
|
60
|
+
|
61
|
+
def rebuild_search_index(*ids)
|
62
|
+
target_ids = Array(ids)
|
63
|
+
target_ids = self.ids if target_ids.empty?
|
64
|
+
|
65
|
+
scope_foreign_key = to_s.foreign_key
|
66
|
+
|
67
|
+
delete_where = Array(ids).any? ? "WHERE #{scope_foreign_key} IN (#{ids.join(", ")})" : ""
|
68
|
+
sql_delete = <<~SQL.strip
|
69
|
+
DELETE FROM fts_#{table_name} #{delete_where};
|
70
|
+
SQL
|
71
|
+
connection.execute(sql_delete)
|
72
|
+
|
73
|
+
target_ids.each do |id|
|
74
|
+
record = where(id: id).pluck(*@@search_scope_attrs, :id).first
|
75
|
+
if record.present?
|
76
|
+
id = record.pop
|
77
|
+
|
78
|
+
sql_insert = <<~SQL.strip
|
79
|
+
INSERT INTO fts_#{table_name}(#{@@search_scope_attrs.join(", ")}, #{scope_foreign_key})
|
80
|
+
VALUES (#{record.map { |value| "'#{quote_string(value)}'" }.join(", ")}, #{id});
|
81
|
+
SQL
|
82
|
+
connection.execute(sql_insert)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def quote_string(s)
|
88
|
+
s.gsub("\\", '\&\&').gsub("'", "''")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -1,6 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Maquina
|
4
|
+
##
|
5
|
+
# A concern that adds user association to models.
|
6
|
+
#
|
7
|
+
# == Usage
|
8
|
+
#
|
9
|
+
# Include this concern in any model that needs to be scoped to a user:
|
10
|
+
#
|
11
|
+
# class MyModel < ApplicationRecord
|
12
|
+
# include Maquina::UserScoped
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# == Associations
|
16
|
+
#
|
17
|
+
# When included, automatically adds:
|
18
|
+
# - +user+:: Belongs to a User through maquina_user_id foreign key
|
19
|
+
#
|
20
|
+
# == Example
|
21
|
+
#
|
22
|
+
# class Document < ApplicationRecord
|
23
|
+
# include Maquina::UserScoped
|
24
|
+
# # Document now has user association
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# document = Document.create(user: current_user)
|
28
|
+
# document.user # => returns associated Maquina::User
|
29
|
+
#
|
4
30
|
module UserScoped
|
5
31
|
extend ActiveSupport::Concern
|
6
32
|
|