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