moduler 0.2.0 → 0.4.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 +4 -4
- data/app/assets/builds/moduler/tailwind.css +2103 -1
- data/app/assets/javascripts/moduler/application.js +1 -0
- data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
- data/app/assets/javascripts/moduler/controllers/wysiwyg_controller.js +126 -0
- data/app/assets/javascripts/moduler/vendor/marked.esm.js +77 -0
- data/app/assets/stylesheets/moduler/tailwind.css +3 -0
- data/app/components/moduler/administration/page_component.html.erb +7 -0
- data/app/components/moduler/administration/page_component.rb +6 -0
- data/app/components/moduler/badge_component.html.erb +8 -0
- data/app/components/moduler/badge_component.rb +10 -0
- data/app/components/moduler/button_component.html.erb +7 -0
- data/app/components/moduler/button_component.rb +19 -0
- data/app/components/moduler/button_link_component.html.erb +5 -0
- data/app/components/moduler/button_link_component.rb +15 -0
- data/app/components/moduler/filter_component.html.erb +12 -0
- data/app/components/moduler/filter_component.rb +15 -0
- data/app/components/moduler/form/base_error_component.html.erb +1 -0
- data/app/components/moduler/form/base_error_component.rb +9 -0
- data/app/components/moduler/form/email_component.html.erb +14 -0
- data/app/components/moduler/form/email_component.rb +11 -0
- data/app/components/moduler/form/password_component.html.erb +14 -0
- data/app/components/moduler/form/password_component.rb +11 -0
- data/app/components/moduler/form/section_component.html.erb +5 -0
- data/app/components/moduler/form/section_component.rb +14 -0
- data/app/components/moduler/form/select_component.html.erb +14 -0
- data/app/components/moduler/form/select_component.rb +12 -0
- data/app/components/moduler/form/submit_component.html.erb +3 -0
- data/app/components/moduler/form/submit_component.rb +10 -0
- data/app/components/moduler/form/text_component.html.erb +14 -0
- data/app/components/moduler/form/text_component.rb +11 -0
- data/app/components/moduler/form/textarea_component.html.erb +14 -0
- data/app/components/moduler/form/textarea_component.rb +11 -0
- data/app/components/moduler/form/wysiwyg_component.html.erb +30 -0
- data/app/components/moduler/form/wysiwyg_component.rb +62 -0
- data/app/components/moduler/list_component.html.erb +15 -0
- data/app/components/moduler/list_component.rb +18 -0
- data/app/components/moduler/list_item_component.html.erb +23 -0
- data/app/components/moduler/list_item_component.rb +14 -0
- data/app/components/moduler/page_header_component.html.erb +16 -0
- data/app/components/moduler/page_header_component.rb +12 -0
- data/app/components/moduler/sidebar_action_component.html.erb +5 -0
- data/app/components/moduler/sidebar_action_component.rb +15 -0
- data/app/controllers/concerns/moduler/authentication.rb +10 -2
- data/app/controllers/moduler/administration/application_controller.rb +7 -0
- data/app/controllers/moduler/administration/articles_controller.rb +57 -0
- data/app/controllers/moduler/administration/home_controller.rb +9 -0
- data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
- data/app/controllers/moduler/administration/users_controller.rb +56 -0
- data/app/controllers/moduler/application_controller.rb +15 -0
- data/app/controllers/moduler/articles_controller.rb +15 -0
- data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
- data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
- data/app/controllers/moduler/profile_controller.rb +27 -0
- data/app/controllers/moduler/sessions_controller.rb +8 -1
- data/app/controllers/moduler/users_controller.rb +7 -0
- data/app/filters/application_filter.rb +41 -0
- data/app/filters/query_filter.rb +18 -0
- data/app/lib/moduler/form_builder.rb +2 -2
- data/app/models/moduler/article.rb +25 -0
- data/app/models/moduler/session.rb +12 -0
- data/app/models/moduler/user.rb +9 -0
- data/app/policies/moduler/administration/article_policy.rb +29 -0
- data/app/policies/moduler/administration/base_policy.rb +11 -0
- data/app/policies/moduler/administration/home_policy.rb +9 -0
- data/app/policies/moduler/administration/session_policy.rb +13 -0
- data/app/policies/moduler/administration/user_policy.rb +25 -0
- data/app/policies/moduler/application_policy.rb +38 -0
- data/app/views/kaminari/_gap.html.erb +1 -0
- data/app/views/kaminari/_next_page.html.erb +1 -0
- data/app/views/kaminari/_page.html.erb +3 -0
- data/app/views/kaminari/_paginator.html.erb +13 -0
- data/app/views/kaminari/_prev_page.html.erb +1 -0
- data/app/views/layouts/moduler/application.html.erb +5 -6
- data/app/views/layouts/moduler/articles.html.erb +18 -0
- data/app/views/moduler/administration/articles/_article.html.erb +13 -0
- data/app/views/moduler/administration/articles/_form.html.erb +18 -0
- data/app/views/moduler/administration/articles/edit.html.erb +1 -0
- data/app/views/moduler/administration/articles/index.html.erb +13 -0
- data/app/views/moduler/administration/articles/new.html.erb +1 -0
- data/app/views/moduler/administration/home/index.html.erb +6 -0
- data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
- data/app/views/moduler/administration/sessions/index.html.erb +9 -0
- data/app/views/moduler/administration/users/_user.html.erb +13 -0
- data/app/views/moduler/administration/users/edit.html.erb +14 -0
- data/app/views/moduler/administration/users/index.html.erb +13 -0
- data/app/views/moduler/administration/users/new.html.erb +18 -0
- data/app/views/moduler/articles/_article.html.erb +16 -0
- data/app/views/moduler/articles/index.html.erb +15 -0
- data/app/views/moduler/articles/show.html.erb +21 -0
- data/app/views/moduler/common/_flash.html.erb +16 -0
- data/app/views/moduler/common/_header.html.erb +12 -3
- data/app/views/moduler/common/_sidebar.html.erb +7 -0
- data/app/views/moduler/common/administration/_sidebar.html.erb +12 -0
- data/app/views/moduler/common/profile/_sidebar.html.erb +6 -0
- data/app/views/moduler/home/index.html.erb +6 -0
- data/app/views/moduler/profile/edit.html.erb +28 -0
- data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
- data/app/views/moduler/profile/sessions/index.html.erb +26 -0
- data/app/views/moduler/sessions/new.html.erb +4 -18
- data/app/views/moduler/users/new.html.erb +5 -32
- data/config/importmap.rb +3 -0
- data/config/locales/moduler/administration/articles/en.yml +37 -0
- data/config/locales/moduler/administration/sessions/en.yml +24 -0
- data/config/locales/moduler/administration/users/en.yml +43 -0
- data/config/locales/moduler/articles/en.yml +13 -0
- data/config/locales/moduler/common/en.yml +12 -0
- data/config/locales/moduler/components/en.yml +27 -0
- data/config/locales/moduler/en.yml +4 -0
- data/config/locales/moduler/profile/en.yml +26 -0
- data/config/locales/moduler/profile/sessions/en.yml +25 -0
- data/config/locales/moduler/sessions/en.yml +1 -0
- data/config/locales/moduler/users/en.yml +7 -1
- data/config/routes.rb +18 -1
- data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
- data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
- data/db/migrate/20261026203827_create_moduler_articles.rb +15 -0
- data/lib/moduler/engine.rb +2 -0
- data/lib/moduler/version.rb +1 -1
- data/lib/moduler.rb +7 -0
- data/lib/tasks/moduler_tasks.rake +2 -4
- metadata +202 -5
- data/app/assets/javascripts/moduler/controllers/hello_controller.js +0 -6
- data/app/controllers/moduler/hello_controller.rb +0 -5
- data/app/views/moduler/hello/index.html.erb +0 -5
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="mt-2">
|
|
2
|
+
<% if @options.key?(:label) %>
|
|
3
|
+
<%= @form.label @name, @options[:label], class: "block text-sm text-slate-500" %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<div class="mt-2">
|
|
6
|
+
<%= @form.text_area @name, class: "mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-900 focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900" %>
|
|
7
|
+
</div>
|
|
8
|
+
<% if @options.key?(:description) %>
|
|
9
|
+
<p class="mt-1 text-xs text-gray-400"><%= @options[:description] %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% if @form.error?(@name) %>
|
|
12
|
+
<%= @form.error(@name, class: "text-red-500 text-sm mt-1") %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="mt-2">
|
|
2
|
+
<% if @options.key?(:label) %>
|
|
3
|
+
<%= @form.label @name, @options[:label], class: "block text-sm text-slate-500" %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<%= tag.div class: "mt-2", data: controller_data do %>
|
|
6
|
+
<div class="flex flex-wrap gap-1 rounded-t-md border border-b-0 border-slate-300 bg-slate-50 px-2 py-1.5">
|
|
7
|
+
<% ACTIONS.each do |action, style| %>
|
|
8
|
+
<button type="button" data-action="click->wysiwyg#<%= action %>" class="rounded px-2 py-1 text-sm text-slate-700 hover:bg-slate-200 <%= style %>"><%= t("moduler.components.form.wysiwyg.action.#{action.to_s.underscore}") %></button>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if upload? %>
|
|
11
|
+
<button type="button" data-action="click->wysiwyg#upload" class="rounded px-2 py-1 text-sm text-slate-700 hover:bg-slate-200"><%= t("moduler.components.form.wysiwyg.action.upload") %></button>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
<%= @form.text_area @name, rows: rows,
|
|
15
|
+
data: { wysiwyg_target: "markup", action: "input->wysiwyg#render keydown->wysiwyg#insertTab" },
|
|
16
|
+
class: "block w-full border border-slate-300 px-3 py-2 font-mono text-sm text-slate-900 focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900" %>
|
|
17
|
+
<div class="prose prose-sm prose-slate prose-100 rounded-b-md border border-t-0 border-slate-300 bg-slate-50 px-4 py-3 prose-img:w-full prose-img:max-w-5xl" data-wysiwyg-target="viewer"></div>
|
|
18
|
+
<% if upload? %>
|
|
19
|
+
<% attached_signed_ids.each do |signed_id| %>
|
|
20
|
+
<%= hidden_field_tag attachments_field_name, signed_id, id: nil %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% if @options.key?(:description) %>
|
|
25
|
+
<p class="mt-1 text-xs text-gray-400"><%= @options[:description] %></p>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% if @form.error?(@name) %>
|
|
28
|
+
<%= @form.error(@name, class: "text-red-500 text-sm mt-1") %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moduler
|
|
4
|
+
class Form::WysiwygComponent < ViewComponent::Base
|
|
5
|
+
ACTIONS = {
|
|
6
|
+
bold: "font-semibold",
|
|
7
|
+
italic: "italic",
|
|
8
|
+
h1: "font-semibold",
|
|
9
|
+
h2: "font-semibold",
|
|
10
|
+
h3: "font-semibold",
|
|
11
|
+
quote: "",
|
|
12
|
+
code: "font-mono",
|
|
13
|
+
ul: "",
|
|
14
|
+
ol: "",
|
|
15
|
+
fencedCode: "font-mono",
|
|
16
|
+
link: "",
|
|
17
|
+
image: ""
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(form, name, options = {})
|
|
21
|
+
@form = form
|
|
22
|
+
@name = name
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def rows
|
|
27
|
+
@options.fetch(:rows, 14)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Name of the has_many_attached association that receives uploaded images,
|
|
31
|
+
# `attachments: false` disables uploading.
|
|
32
|
+
def attachments
|
|
33
|
+
@options.fetch(:attachments, :images)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def upload?
|
|
37
|
+
attachments.present? && @form.object.respond_to?(attachments)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def attachments_field_name
|
|
41
|
+
"#{@form.object_name}[#{attachments}][]"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Assigning has_many_attached replaces existing attachments, so already attached
|
|
45
|
+
# blobs are submitted again together with the newly uploaded ones. Blobs rejected
|
|
46
|
+
# by validation are not persisted, so they have no signed id and are skipped.
|
|
47
|
+
def attached_signed_ids
|
|
48
|
+
@form.object.public_send(attachments).map(&:blob).select(&:persisted?).map(&:signed_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def controller_data
|
|
52
|
+
data = { controller: "wysiwyg" }
|
|
53
|
+
return data unless upload?
|
|
54
|
+
|
|
55
|
+
data.merge(
|
|
56
|
+
wysiwyg_direct_upload_url_value: helpers.main_app.rails_direct_uploads_path,
|
|
57
|
+
wysiwyg_blob_url_value: helpers.main_app.rails_service_blob_path(":signed_id", ":filename"),
|
|
58
|
+
wysiwyg_attachments_field_name_value: attachments_field_name
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="mt-6 flex-1">
|
|
2
|
+
<% if @collection.any? %>
|
|
3
|
+
<ul id="<%= @options[:id] %>" role="list" class="divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white">
|
|
4
|
+
<%= content %>
|
|
5
|
+
</ul>
|
|
6
|
+
<% else %>
|
|
7
|
+
<p class="rounded-lg border border-dashed border-slate-300 px-4 py-8 text-center text-sm text-slate-500"><%= empty_message %></p>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if paginated? %>
|
|
12
|
+
<div class="mt-4 flex justify-end">
|
|
13
|
+
<%= helpers.paginate @collection %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moduler
|
|
4
|
+
class ListComponent < ViewComponent::Base
|
|
5
|
+
def initialize(collection, options = {})
|
|
6
|
+
@collection = collection
|
|
7
|
+
@options = options
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def empty_message
|
|
11
|
+
@options.fetch(:empty) { t("moduler.components.list.empty") }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def paginated?
|
|
15
|
+
@collection.respond_to?(:total_pages)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<li id="<%= @options[:id] %>" class="flex items-center justify-between gap-4 px-4 py-4 sm:px-5">
|
|
2
|
+
<div class="min-w-0">
|
|
3
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
4
|
+
<p class="truncate text-sm font-semibold text-gray-900"><%= @title %></p>
|
|
5
|
+
|
|
6
|
+
<% badges.each do |badge| %>
|
|
7
|
+
<%= badge %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if meta? %>
|
|
12
|
+
<p class="mt-1 text-xs text-slate-400"><%= meta %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if actions? %>
|
|
17
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
18
|
+
<% actions.each do |action| %>
|
|
19
|
+
<%= action %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</li>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moduler
|
|
4
|
+
class ListItemComponent < ViewComponent::Base
|
|
5
|
+
renders_many :badges, Moduler::BadgeComponent
|
|
6
|
+
renders_one :meta
|
|
7
|
+
renders_many :actions
|
|
8
|
+
|
|
9
|
+
def initialize(title, options = {})
|
|
10
|
+
@title = title
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-base font-semibold text-gray-900"><%= @title %></h1>
|
|
4
|
+
<% if @options.key?(:description) %>
|
|
5
|
+
<p class="mt-1 text-sm text-slate-500"><%= @options[:description] %></p>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if actions? %>
|
|
10
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
11
|
+
<% actions.each do |action| %>
|
|
12
|
+
<%= action %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moduler
|
|
4
|
+
class SidebarActionComponent < ViewComponent::Base
|
|
5
|
+
def initialize(action, route, expected_controller)
|
|
6
|
+
@action = action
|
|
7
|
+
@route = route
|
|
8
|
+
@expected_controller = expected_controller
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def active?
|
|
12
|
+
controller_path.eql?(@expected_controller)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -28,7 +28,9 @@ module Moduler
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def find_session_by_cookie
|
|
31
|
-
|
|
31
|
+
if cookies.signed[:session_id]
|
|
32
|
+
Session.active.find_by(id: cookies.signed[:session_id], signed_in_at: Session::LIFETIME.ago..)
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def request_authentication
|
|
@@ -42,7 +44,13 @@ module Moduler
|
|
|
42
44
|
def start_new_session_for(user)
|
|
43
45
|
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip, signed_in_at: Time.current).tap do |session|
|
|
44
46
|
Current.session = session
|
|
45
|
-
cookies.signed
|
|
47
|
+
cookies.signed[:session_id] = {
|
|
48
|
+
value: session.id,
|
|
49
|
+
expires: Session::LIFETIME.from_now,
|
|
50
|
+
httponly: true,
|
|
51
|
+
same_site: :lax,
|
|
52
|
+
secure: Rails.env.production?
|
|
53
|
+
}
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Administration
|
|
3
|
+
class ArticlesController < ApplicationController
|
|
4
|
+
before_action -> { authorize([ :moduler, :administration, :article ]) }
|
|
5
|
+
before_action :set_article, only: %i[ edit update destroy ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@filter, @articles = QueryFilter.new(Article, permitted_filter_parameters).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@article = Article.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@article = Article.new(permitted_parameters)
|
|
17
|
+
|
|
18
|
+
if @article.save
|
|
19
|
+
redirect_to moduler.administration_articles_path, notice: t(".notice")
|
|
20
|
+
else
|
|
21
|
+
render :new, status: :unprocessable_entity
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def edit; end
|
|
26
|
+
|
|
27
|
+
def update
|
|
28
|
+
if @article.update(permitted_parameters)
|
|
29
|
+
redirect_to moduler.administration_articles_path, notice: t(".notice")
|
|
30
|
+
else
|
|
31
|
+
render :edit, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy
|
|
36
|
+
@article.destroy!
|
|
37
|
+
|
|
38
|
+
redirect_to moduler.administration_articles_path, status: :see_other, notice: t(".notice")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def set_article
|
|
44
|
+
@article = Article.find(params[:id])
|
|
45
|
+
authorize(@article, policy_class: ArticlePolicy)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def permitted_parameters
|
|
49
|
+
params.expect(article: [ :path, :title, :description, :keywords, :content, images: [] ])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def permitted_filter_parameters
|
|
53
|
+
(params.permit(filter: %i[ query ])[:filter] || {}).merge(params.permit(:page))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Administration
|
|
3
|
+
class SessionsController < ApplicationController
|
|
4
|
+
before_action -> { authorize([ :moduler, :administration, :session ]) }
|
|
5
|
+
before_action :set_session, only: %i[ destroy ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@filter, @sessions = QueryFilter.new(Session.includes(:user), permitted_filter_parameters).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def destroy
|
|
12
|
+
@session.touch(:signed_out_at)
|
|
13
|
+
|
|
14
|
+
if @session == Current.session
|
|
15
|
+
cookies.delete(:session_id)
|
|
16
|
+
redirect_to moduler.new_session_path, status: :see_other
|
|
17
|
+
else
|
|
18
|
+
redirect_to moduler.administration_sessions_path, status: :see_other, notice: t(".notice")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def set_session
|
|
25
|
+
@session = Session.active.find(params[:id])
|
|
26
|
+
authorize(@session, policy_class: SessionPolicy)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def permitted_filter_parameters
|
|
30
|
+
(params.permit(filter: %i[ query ])[:filter] || {}).merge(params.permit(:page))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Administration
|
|
3
|
+
class UsersController < ApplicationController
|
|
4
|
+
before_action -> { authorize([ :moduler, :administration, :user ]) }
|
|
5
|
+
before_action :set_user, only: %i[ edit update ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@filter, @users = QueryFilter.new(User, permitted_filter_parameters).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@user = User.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@user = User.new(permitted_parameters)
|
|
17
|
+
|
|
18
|
+
if @user.save
|
|
19
|
+
redirect_to moduler.administration_users_path, notice: t(".notice")
|
|
20
|
+
else
|
|
21
|
+
render :new, status: :unprocessable_entity
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def edit
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
if @user.update(permitted_update_parameters)
|
|
30
|
+
redirect_to moduler.administration_users_path, notice: t(".notice")
|
|
31
|
+
else
|
|
32
|
+
render :edit, status: :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def set_user
|
|
39
|
+
@user = User.find(params[:id])
|
|
40
|
+
authorize(@user, policy_class: UserPolicy)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def permitted_parameters
|
|
44
|
+
params.expect(user: %i[ email_address role password ])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def permitted_update_parameters
|
|
48
|
+
params.expect(user: %i[ email_address role ])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def permitted_filter_parameters
|
|
52
|
+
(params.permit(filter: %i[ query ])[:filter] || {}).merge(params.permit(:page))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
module Moduler
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
3
|
include Authentication
|
|
4
|
+
include Pundit::Authorization
|
|
4
5
|
|
|
5
6
|
default_form_builder FormBuilder
|
|
7
|
+
|
|
8
|
+
rescue_from Pundit::NotAuthorizedError, with: :not_authorized
|
|
9
|
+
|
|
10
|
+
helper_method :current_user
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def current_user
|
|
15
|
+
Current.user
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def not_authorized
|
|
19
|
+
redirect_to moduler.root_path, alert: t("moduler.flash.not_authorized")
|
|
20
|
+
end
|
|
6
21
|
end
|
|
7
22
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class ArticlesController < ApplicationController
|
|
3
|
+
layout "moduler/articles"
|
|
4
|
+
|
|
5
|
+
allow_unauthenticated_access
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@articles = Article.order(created_at: :desc).page(params[:page])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
@article = Article.find_by!(path: params[:path])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Profile
|
|
3
|
+
module Sessions
|
|
4
|
+
class RevocationController < ApplicationController
|
|
5
|
+
def destroy
|
|
6
|
+
Current.user.sessions.active.update_all(signed_out_at: Time.current)
|
|
7
|
+
cookies.delete(:session_id)
|
|
8
|
+
|
|
9
|
+
redirect_to moduler.new_session_path, status: :see_other
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Profile
|
|
3
|
+
class SessionsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@sessions = Current.user.sessions.order(created_at: :desc).page(params[:page]).per(10)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def destroy
|
|
9
|
+
session = Current.user.sessions.active.find(params[:id])
|
|
10
|
+
session.touch(:signed_out_at)
|
|
11
|
+
|
|
12
|
+
if session == Current.session
|
|
13
|
+
cookies.delete(:session_id)
|
|
14
|
+
redirect_to moduler.new_session_path, status: :see_other
|
|
15
|
+
else
|
|
16
|
+
redirect_to moduler.profile_sessions_path, status: :see_other
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class ProfileController < ApplicationController
|
|
3
|
+
def edit
|
|
4
|
+
@user = Current.user
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def update
|
|
8
|
+
@user = Current.user
|
|
9
|
+
|
|
10
|
+
@user.assign_attributes(permitted_parameters)
|
|
11
|
+
|
|
12
|
+
if @user.save(context: :self_service_update)
|
|
13
|
+
@user.sessions.active.where.not(id: Current.session.id).update_all(signed_out_at: Time.current) if @user.saved_change_to_password_digest?
|
|
14
|
+
|
|
15
|
+
redirect_to moduler.edit_profile_path, notice: t(".notice")
|
|
16
|
+
else
|
|
17
|
+
render :edit, status: :unprocessable_entity
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def permitted_parameters
|
|
24
|
+
params.expect(user: %i[ password_challenge password password_confirmation ])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -2,6 +2,13 @@ module Moduler
|
|
|
2
2
|
class SessionsController < ApplicationController
|
|
3
3
|
allow_unauthenticated_access only: %i[ new create ]
|
|
4
4
|
|
|
5
|
+
rate_limit to: 10, within: 3.minutes, only: :create,
|
|
6
|
+
with: -> { redirect_to moduler.new_session_path, alert: t("moduler.sessions.create.rate_limited") }
|
|
7
|
+
|
|
8
|
+
rate_limit to: 5, within: 3.minutes, only: :create,
|
|
9
|
+
by: -> { params.dig(:user, :email_address) },
|
|
10
|
+
with: -> { redirect_to moduler.new_session_path, alert: t("moduler.sessions.create.rate_limited") }
|
|
11
|
+
|
|
5
12
|
def new
|
|
6
13
|
if authenticated?
|
|
7
14
|
redirect_to after_authentication_url
|
|
@@ -26,7 +33,7 @@ module Moduler
|
|
|
26
33
|
|
|
27
34
|
def destroy
|
|
28
35
|
terminate_session
|
|
29
|
-
redirect_to moduler.new_session_path
|
|
36
|
+
redirect_to moduler.new_session_path, status: :see_other
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -2,6 +2,13 @@ module Moduler
|
|
|
2
2
|
class UsersController < ApplicationController
|
|
3
3
|
allow_unauthenticated_access only: %i[ new create ]
|
|
4
4
|
|
|
5
|
+
rate_limit to: 10, within: 3.minutes, only: :create,
|
|
6
|
+
with: -> { redirect_to moduler.new_user_path, alert: t("moduler.users.create.rate_limited") }
|
|
7
|
+
|
|
8
|
+
rate_limit to: 5, within: 3.minutes, only: :create,
|
|
9
|
+
by: -> { params.dig(:user, :email_address) },
|
|
10
|
+
with: -> { redirect_to moduler.new_user_path, alert: t("moduler.users.create.rate_limited") }
|
|
11
|
+
|
|
5
12
|
def new
|
|
6
13
|
if authenticated?
|
|
7
14
|
redirect_to after_authentication_url
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class ApplicationFilter
|
|
2
|
+
include ActiveModel::Model
|
|
3
|
+
include ActiveModel::Dirty
|
|
4
|
+
include ActiveModel::Attributes
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
def modifiers
|
|
8
|
+
@modifiers ||= []
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(scope, parameters, options = {})
|
|
13
|
+
super(parameters)
|
|
14
|
+
|
|
15
|
+
@scope = scope
|
|
16
|
+
@options = options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
[ filter, results ]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def filter
|
|
26
|
+
return self unless changed?
|
|
27
|
+
|
|
28
|
+
tap(&:valid?)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def results
|
|
32
|
+
return @scope.none.page(1) if !changed? && @options.fetch(:allow_empty, false)
|
|
33
|
+
return @scope.none.page(1) if invalid?
|
|
34
|
+
|
|
35
|
+
self.class.modifiers.each do |modifier|
|
|
36
|
+
@scope = modifier.call(@scope, self) || @scope
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@scope
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class QueryFilter < ApplicationFilter
|
|
2
|
+
attribute :query, :string
|
|
3
|
+
attribute :page, :integer
|
|
4
|
+
|
|
5
|
+
validates :query, length: { maximum: 100 }
|
|
6
|
+
|
|
7
|
+
modifiers << proc do |scope, filter|
|
|
8
|
+
scope.page(filter.page).per(10)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
modifiers << proc do |scope, _filter|
|
|
12
|
+
scope.order(created_at: :desc)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
modifiers << proc do |scope, filter|
|
|
16
|
+
scope.search_by_query(filter.query) if filter.query.present?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -5,7 +5,7 @@ module Moduler
|
|
|
5
5
|
|
|
6
6
|
@template.content_tag :div, **options do
|
|
7
7
|
@object.errors[field].first
|
|
8
|
-
end
|
|
8
|
+
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def full_error(field, options = {})
|
|
@@ -13,7 +13,7 @@ module Moduler
|
|
|
13
13
|
|
|
14
14
|
@template.content_tag :div, **options do
|
|
15
15
|
@object.errors.full_messages_for(field).first
|
|
16
|
-
end
|
|
16
|
+
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def error?(field)
|