moduler 0.2.0 → 0.3.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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/moduler/tailwind.css +1358 -1
  3. data/app/assets/javascripts/moduler/application.js +1 -0
  4. data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
  5. data/app/assets/stylesheets/moduler/tailwind.css +3 -0
  6. data/app/components/moduler/administration/page_component.html.erb +7 -0
  7. data/app/components/moduler/administration/page_component.rb +6 -0
  8. data/app/components/moduler/badge_component.html.erb +8 -0
  9. data/app/components/moduler/badge_component.rb +10 -0
  10. data/app/components/moduler/button_component.html.erb +7 -0
  11. data/app/components/moduler/button_component.rb +19 -0
  12. data/app/components/moduler/button_link_component.html.erb +5 -0
  13. data/app/components/moduler/button_link_component.rb +15 -0
  14. data/app/components/moduler/filter_component.html.erb +12 -0
  15. data/app/components/moduler/filter_component.rb +15 -0
  16. data/app/components/moduler/form/base_error_component.html.erb +1 -0
  17. data/app/components/moduler/form/base_error_component.rb +9 -0
  18. data/app/components/moduler/form/email_component.html.erb +14 -0
  19. data/app/components/moduler/form/email_component.rb +11 -0
  20. data/app/components/moduler/form/password_component.html.erb +14 -0
  21. data/app/components/moduler/form/password_component.rb +11 -0
  22. data/app/components/moduler/form/section_component.html.erb +5 -0
  23. data/app/components/moduler/form/section_component.rb +14 -0
  24. data/app/components/moduler/form/select_component.html.erb +14 -0
  25. data/app/components/moduler/form/select_component.rb +12 -0
  26. data/app/components/moduler/form/submit_component.html.erb +3 -0
  27. data/app/components/moduler/form/submit_component.rb +10 -0
  28. data/app/components/moduler/form/text_component.html.erb +14 -0
  29. data/app/components/moduler/form/text_component.rb +11 -0
  30. data/app/components/moduler/list_component.html.erb +15 -0
  31. data/app/components/moduler/list_component.rb +18 -0
  32. data/app/components/moduler/list_item_component.html.erb +23 -0
  33. data/app/components/moduler/list_item_component.rb +14 -0
  34. data/app/components/moduler/page_header_component.html.erb +16 -0
  35. data/app/components/moduler/page_header_component.rb +12 -0
  36. data/app/components/moduler/sidebar_action_component.html.erb +5 -0
  37. data/app/components/moduler/sidebar_action_component.rb +15 -0
  38. data/app/controllers/concerns/moduler/authentication.rb +10 -2
  39. data/app/controllers/moduler/administration/application_controller.rb +7 -0
  40. data/app/controllers/moduler/administration/home_controller.rb +9 -0
  41. data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
  42. data/app/controllers/moduler/administration/users_controller.rb +56 -0
  43. data/app/controllers/moduler/application_controller.rb +15 -0
  44. data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
  45. data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
  46. data/app/controllers/moduler/profile_controller.rb +27 -0
  47. data/app/controllers/moduler/sessions_controller.rb +8 -1
  48. data/app/controllers/moduler/users_controller.rb +7 -0
  49. data/app/filters/application_filter.rb +41 -0
  50. data/app/filters/query_filter.rb +18 -0
  51. data/app/lib/moduler/form_builder.rb +2 -2
  52. data/app/models/moduler/session.rb +12 -0
  53. data/app/models/moduler/user.rb +9 -0
  54. data/app/policies/moduler/administration/base_policy.rb +11 -0
  55. data/app/policies/moduler/administration/home_policy.rb +9 -0
  56. data/app/policies/moduler/administration/session_policy.rb +13 -0
  57. data/app/policies/moduler/administration/user_policy.rb +25 -0
  58. data/app/policies/moduler/application_policy.rb +38 -0
  59. data/app/views/kaminari/_gap.html.erb +1 -0
  60. data/app/views/kaminari/_next_page.html.erb +1 -0
  61. data/app/views/kaminari/_page.html.erb +3 -0
  62. data/app/views/kaminari/_paginator.html.erb +13 -0
  63. data/app/views/kaminari/_prev_page.html.erb +1 -0
  64. data/app/views/layouts/moduler/application.html.erb +5 -6
  65. data/app/views/moduler/administration/home/index.html.erb +6 -0
  66. data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
  67. data/app/views/moduler/administration/sessions/index.html.erb +9 -0
  68. data/app/views/moduler/administration/users/_user.html.erb +13 -0
  69. data/app/views/moduler/administration/users/edit.html.erb +14 -0
  70. data/app/views/moduler/administration/users/index.html.erb +13 -0
  71. data/app/views/moduler/administration/users/new.html.erb +18 -0
  72. data/app/views/moduler/common/_flash.html.erb +16 -0
  73. data/app/views/moduler/common/_header.html.erb +12 -3
  74. data/app/views/moduler/common/_sidebar.html.erb +6 -0
  75. data/app/views/moduler/common/administration/_sidebar.html.erb +6 -0
  76. data/app/views/moduler/profile/edit.html.erb +28 -0
  77. data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
  78. data/app/views/moduler/profile/sessions/index.html.erb +26 -0
  79. data/app/views/moduler/sessions/new.html.erb +4 -18
  80. data/app/views/moduler/users/new.html.erb +5 -32
  81. data/config/importmap.rb +1 -0
  82. data/config/locales/moduler/administration/sessions/en.yml +24 -0
  83. data/config/locales/moduler/administration/users/en.yml +43 -0
  84. data/config/locales/moduler/common/en.yml +5 -0
  85. data/config/locales/moduler/components/en.yml +11 -0
  86. data/config/locales/moduler/en.yml +4 -0
  87. data/config/locales/moduler/profile/en.yml +26 -0
  88. data/config/locales/moduler/profile/sessions/en.yml +25 -0
  89. data/config/locales/moduler/sessions/en.yml +1 -0
  90. data/config/locales/moduler/users/en.yml +7 -1
  91. data/config/routes.rb +14 -1
  92. data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
  93. data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
  94. data/lib/moduler/engine.rb +2 -0
  95. data/lib/moduler/version.rb +1 -1
  96. data/lib/moduler.rb +4 -0
  97. data/lib/tasks/moduler_tasks.rake +2 -4
  98. metadata +151 -5
  99. data/app/assets/javascripts/moduler/controllers/hello_controller.js +0 -6
  100. data/app/controllers/moduler/hello_controller.rb +0 -5
  101. data/app/views/moduler/hello/index.html.erb +0 -5
@@ -1,3 +1,4 @@
1
+ import "@hotwired/turbo-rails"
1
2
  import { Application } from "@hotwired/stimulus"
2
3
  import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
3
4
 
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { dismissAfter: { type: Number, default: 5000 } }
5
+
6
+ connect() {
7
+ this.timeout = setTimeout(() => this.dismiss(), this.dismissAfterValue)
8
+ }
9
+
10
+ disconnect() {
11
+ clearTimeout(this.timeout)
12
+ }
13
+
14
+ dismiss() {
15
+ clearTimeout(this.timeout)
16
+ this.element.classList.add("opacity-0", "translate-x-2")
17
+ this.element.addEventListener("transitionend", () => this.element.remove(), { once: true })
18
+ }
19
+ }
@@ -8,6 +8,9 @@
8
8
  @source inline('bg-red-100');
9
9
  @source inline('bg-green-100');
10
10
 
11
+ @source "../../../views/**/*.html.erb";
12
+ @source "../../../components/**/*.html.erb";
13
+
11
14
  @theme {
12
15
  --color-custom: #000;
13
16
  --font-custom: "Arial", sans-serif;
@@ -0,0 +1,7 @@
1
+ <main class="mx-auto flex w-full flex-1 max-w-[120rem] flex-col gap-4 px-4 md:flex-row md:gap-8 md:pl-0 md:pr-6">
2
+ <%= render "moduler/common/administration/sidebar" %>
3
+
4
+ <div class="flex min-w-0 flex-1 flex-col pb-4 md:mt-8">
5
+ <%= content %>
6
+ </div>
7
+ </main>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Administration::PageComponent < ViewComponent::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ <% case @variant %>
2
+ <% when :success %>
3
+ <span class="inline-flex items-center rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20"><%= @text %></span>
4
+ <% when :muted %>
5
+ <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-slate-400 ring-1 ring-inset ring-slate-200"><%= @text %></span>
6
+ <% else %>
7
+ <span class="inline-flex items-center rounded-full bg-slate-50 px-2 py-0.5 text-xs font-medium text-slate-600 ring-1 ring-inset ring-slate-500/20"><%= @text %></span>
8
+ <% end %>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class BadgeComponent < ViewComponent::Base
5
+ def initialize(text, options = {})
6
+ @text = text
7
+ @variant = options.fetch(:variant, :neutral)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ <% if small? %>
2
+ <%= button_to @text, @url, method: @options.fetch(:method, :post), form: form_options,
3
+ class: "cursor-pointer rounded-md bg-white px-2.5 py-1 text-sm font-medium text-slate-600 shadow-sm ring-1 ring-inset ring-slate-300 hover:bg-slate-50 hover:text-red-600" %>
4
+ <% else %>
5
+ <%= button_to @text, @url, method: @options.fetch(:method, :post), form: form_options,
6
+ class: "cursor-pointer whitespace-nowrap rounded-md bg-white px-3 py-1.5 text-sm/6 font-semibold text-red-600 shadow-sm ring-1 ring-inset ring-slate-300 hover:bg-red-50 hover:ring-red-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" %>
7
+ <% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class ButtonComponent < ViewComponent::Base
5
+ def initialize(text, url, options = {})
6
+ @text = text
7
+ @url = url
8
+ @options = options
9
+ end
10
+
11
+ def small?
12
+ @options[:size] == :sm
13
+ end
14
+
15
+ def form_options
16
+ { class: "shrink-0", data: { turbo_confirm: @options[:confirm] }.compact }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ <% if small? %>
2
+ <%= link_to @text, @url, class: "shrink-0 rounded-md bg-white px-2.5 py-1 text-sm font-medium text-slate-600 shadow-sm ring-1 ring-inset ring-slate-300 hover:bg-slate-50 hover:text-slate-900" %>
3
+ <% else %>
4
+ <%= link_to @text, @url, class: "cursor-pointer whitespace-nowrap rounded-md bg-white px-3 py-2 text-sm/6 font-semibold text-custom ring-1 ring-inset ring-slate-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-custom" %>
5
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class ButtonLinkComponent < ViewComponent::Base
5
+ def initialize(text, url, options = {})
6
+ @text = text
7
+ @url = url
8
+ @options = options
9
+ end
10
+
11
+ def small?
12
+ @options[:size] == :sm
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ <%= form_with model: @filter, url: @url, method: :get, scope: :filter, data: { turbo: false }, class: "flex flex-wrap items-end gap-3" do |form| %>
2
+ <div class="min-w-48 flex-1">
3
+ <%= form.text_field :query, placeholder: placeholder, autocomplete: "off",
4
+ class: "w-full rounded-md border border-slate-300 px-3 text-sm text-slate-900 placeholder:text-slate-400 focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900" %>
5
+ </div>
6
+
7
+ <%= form.submit t("moduler.components.filter.action.filter"),
8
+ class: "cursor-pointer whitespace-nowrap rounded-md bg-white px-3 py-2 text-sm/6 font-semibold text-custom ring-1 ring-inset ring-slate-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-custom" %>
9
+ <% if @filter.query.present? %>
10
+ <%= link_to t("moduler.components.filter.action.clear"), @url, class: "flex h-10 items-center text-sm text-slate-500 hover:text-slate-900" %>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class FilterComponent < ViewComponent::Base
5
+ def initialize(filter, url, options = {})
6
+ @filter = filter
7
+ @url = url
8
+ @options = options
9
+ end
10
+
11
+ def placeholder
12
+ @options.fetch(:placeholder) { t("moduler.components.filter.placeholder.query") }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ <%= @form.full_error :base, class: "my-2 text-sm text-red-600" %>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::BaseErrorComponent < ViewComponent::Base
5
+ def initialize(form)
6
+ @form = form
7
+ end
8
+ end
9
+ end
@@ -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.email_field @name, placeholder: @options[:placeholder], disabled: @options[:disabled], class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-custom sm:text-sm/6" %>
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::EmailComponent < ViewComponent::Base
5
+ def initialize(form, name, options = {})
6
+ @form = form
7
+ @name = name
8
+ @options = options
9
+ end
10
+ end
11
+ end
@@ -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.password_field @name, placeholder: @options[:placeholder], class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-custom sm:text-sm/6" %>
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::PasswordComponent < ViewComponent::Base
5
+ def initialize(form, name, options = {})
6
+ @form = form
7
+ @name = name
8
+ @options = options
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ <div class="<%= divider? ? "space-y-4 border-t border-slate-100 pt-6" : "space-y-4" %>">
2
+ <p class="text-xs font-medium uppercase tracking-wide text-slate-400"><%= @title %></p>
3
+
4
+ <%= content %>
5
+ </div>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::SectionComponent < ViewComponent::Base
5
+ def initialize(title, options = {})
6
+ @title = title
7
+ @options = options
8
+ end
9
+
10
+ def divider?
11
+ @options[:divider]
12
+ end
13
+ end
14
+ end
@@ -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.select @name, @choices, {}, class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-custom sm:text-sm/6" %>
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::SelectComponent < ViewComponent::Base
5
+ def initialize(form, name, choices, options = {})
6
+ @form = form
7
+ @name = name
8
+ @choices = choices
9
+ @options = options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ <div class="mt-4">
2
+ <%= @form.submit @options[:text], class: "cursor-pointer flex float-right rounded-md bg-custom px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-custom focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-custom" %>
3
+ </div>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::SubmitComponent < ViewComponent::Base
5
+ def initialize(form, options = {})
6
+ @form = form
7
+ @options = options
8
+ end
9
+ end
10
+ end
@@ -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_field @name, placeholder: @options[:placeholder], class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-custom sm:text-sm/6" %>
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class Form::TextComponent < ViewComponent::Base
5
+ def initialize(form, name, options = {})
6
+ @form = form
7
+ @name = name
8
+ @options = options
9
+ end
10
+ end
11
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moduler
4
+ class PageHeaderComponent < ViewComponent::Base
5
+ renders_many :actions
6
+
7
+ def initialize(title, options = {})
8
+ @title = title
9
+ @options = options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ <% if active? %>
2
+ <%= link_to @action, @route, class: "rounded-md px-3 py-2 bg-slate-100 font-medium text-gray-900" %>
3
+ <% else %>
4
+ <%= link_to @action, @route, class: "rounded-md px-3 py-2 text-slate-500 hover:bg-slate-50 hover:text-gray-900" %>
5
+ <% end %>
@@ -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
- Session.find_by(id: cookies.signed[:session_id], signed_out_at: nil) if cookies.signed[:session_id]
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.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
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,7 @@
1
+ module Moduler
2
+ module Administration
3
+ class ApplicationController < ::Moduler::ApplicationController
4
+ after_action :verify_authorized
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Moduler
2
+ module Administration
3
+ class HomeController < ApplicationController
4
+ before_action -> { authorize([ :moduler, :administration, :home ]) }
5
+
6
+ def index; end
7
+ end
8
+ end
9
+ 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,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