moduler 0.1.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/moduler/tailwind.css +1359 -0
  3. data/app/assets/config/moduler/manifest.js +2 -0
  4. data/app/assets/javascripts/moduler/application.js +9 -0
  5. data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
  6. data/app/assets/stylesheets/moduler/tailwind.css +21 -0
  7. data/app/components/moduler/administration/page_component.html.erb +7 -0
  8. data/app/components/moduler/administration/page_component.rb +6 -0
  9. data/app/components/moduler/badge_component.html.erb +8 -0
  10. data/app/components/moduler/badge_component.rb +10 -0
  11. data/app/components/moduler/button_component.html.erb +7 -0
  12. data/app/components/moduler/button_component.rb +19 -0
  13. data/app/components/moduler/button_link_component.html.erb +5 -0
  14. data/app/components/moduler/button_link_component.rb +15 -0
  15. data/app/components/moduler/filter_component.html.erb +12 -0
  16. data/app/components/moduler/filter_component.rb +15 -0
  17. data/app/components/moduler/form/base_error_component.html.erb +1 -0
  18. data/app/components/moduler/form/base_error_component.rb +9 -0
  19. data/app/components/moduler/form/email_component.html.erb +14 -0
  20. data/app/components/moduler/form/email_component.rb +11 -0
  21. data/app/components/moduler/form/password_component.html.erb +14 -0
  22. data/app/components/moduler/form/password_component.rb +11 -0
  23. data/app/components/moduler/form/section_component.html.erb +5 -0
  24. data/app/components/moduler/form/section_component.rb +14 -0
  25. data/app/components/moduler/form/select_component.html.erb +14 -0
  26. data/app/components/moduler/form/select_component.rb +12 -0
  27. data/app/components/moduler/form/submit_component.html.erb +3 -0
  28. data/app/components/moduler/form/submit_component.rb +10 -0
  29. data/app/components/moduler/form/text_component.html.erb +14 -0
  30. data/app/components/moduler/form/text_component.rb +11 -0
  31. data/app/components/moduler/list_component.html.erb +15 -0
  32. data/app/components/moduler/list_component.rb +18 -0
  33. data/app/components/moduler/list_item_component.html.erb +23 -0
  34. data/app/components/moduler/list_item_component.rb +14 -0
  35. data/app/components/moduler/page_header_component.html.erb +16 -0
  36. data/app/components/moduler/page_header_component.rb +12 -0
  37. data/app/components/moduler/sidebar_action_component.html.erb +5 -0
  38. data/app/components/moduler/sidebar_action_component.rb +15 -0
  39. data/app/controllers/concerns/moduler/authentication.rb +62 -0
  40. data/app/controllers/moduler/administration/application_controller.rb +7 -0
  41. data/app/controllers/moduler/administration/home_controller.rb +9 -0
  42. data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
  43. data/app/controllers/moduler/administration/users_controller.rb +56 -0
  44. data/app/controllers/moduler/application_controller.rb +18 -0
  45. data/app/controllers/moduler/home_controller.rb +5 -0
  46. data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
  47. data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
  48. data/app/controllers/moduler/profile_controller.rb +27 -0
  49. data/app/controllers/moduler/sessions_controller.rb +39 -0
  50. data/app/controllers/moduler/users_controller.rb +36 -0
  51. data/app/filters/application_filter.rb +41 -0
  52. data/app/filters/query_filter.rb +18 -0
  53. data/app/helpers/moduler/application_helper.rb +4 -0
  54. data/app/lib/moduler/form_builder.rb +23 -0
  55. data/app/models/moduler/current.rb +6 -0
  56. data/app/models/moduler/session.rb +17 -0
  57. data/app/models/moduler/user.rb +18 -0
  58. data/app/policies/moduler/administration/base_policy.rb +11 -0
  59. data/app/policies/moduler/administration/home_policy.rb +9 -0
  60. data/app/policies/moduler/administration/session_policy.rb +13 -0
  61. data/app/policies/moduler/administration/user_policy.rb +25 -0
  62. data/app/policies/moduler/application_policy.rb +38 -0
  63. data/app/views/kaminari/_gap.html.erb +1 -0
  64. data/app/views/kaminari/_next_page.html.erb +1 -0
  65. data/app/views/kaminari/_page.html.erb +3 -0
  66. data/app/views/kaminari/_paginator.html.erb +13 -0
  67. data/app/views/kaminari/_prev_page.html.erb +1 -0
  68. data/app/views/layouts/moduler/application.html.erb +16 -10
  69. data/app/views/moduler/administration/home/index.html.erb +6 -0
  70. data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
  71. data/app/views/moduler/administration/sessions/index.html.erb +9 -0
  72. data/app/views/moduler/administration/users/_user.html.erb +13 -0
  73. data/app/views/moduler/administration/users/edit.html.erb +14 -0
  74. data/app/views/moduler/administration/users/index.html.erb +13 -0
  75. data/app/views/moduler/administration/users/new.html.erb +18 -0
  76. data/app/views/moduler/common/_flash.html.erb +16 -0
  77. data/app/views/moduler/common/_header.html.erb +18 -0
  78. data/app/views/moduler/common/_sidebar.html.erb +6 -0
  79. data/app/views/moduler/common/administration/_sidebar.html.erb +6 -0
  80. data/app/views/moduler/home/index.html.erb +0 -0
  81. data/app/views/moduler/profile/edit.html.erb +28 -0
  82. data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
  83. data/app/views/moduler/profile/sessions/index.html.erb +26 -0
  84. data/app/views/moduler/sessions/new.html.erb +31 -0
  85. data/app/views/moduler/users/new.html.erb +26 -0
  86. data/config/importmap.rb +5 -0
  87. data/config/locales/moduler/administration/sessions/en.yml +24 -0
  88. data/config/locales/moduler/administration/users/en.yml +43 -0
  89. data/config/locales/moduler/common/en.yml +13 -0
  90. data/config/locales/moduler/components/en.yml +11 -0
  91. data/config/locales/moduler/en.yml +4 -0
  92. data/config/locales/moduler/profile/en.yml +26 -0
  93. data/config/locales/moduler/profile/sessions/en.yml +25 -0
  94. data/config/locales/moduler/sessions/en.yml +18 -0
  95. data/config/locales/moduler/users/en.yml +21 -0
  96. data/config/routes.rb +19 -0
  97. data/db/migrate/20260322152259_create_moduler_users.rb +12 -0
  98. data/db/migrate/20260322153744_create_moduler_sessions.rb +13 -0
  99. data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
  100. data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
  101. data/lib/moduler/configuration.rb +12 -0
  102. data/lib/moduler/engine.rb +20 -0
  103. data/lib/moduler/router.rb +10 -0
  104. data/lib/moduler/routing.rb +11 -0
  105. data/lib/moduler/version.rb +1 -1
  106. data/lib/moduler.rb +12 -1
  107. data/lib/tasks/moduler_tasks.rake +42 -4
  108. metadata +250 -2
@@ -0,0 +1,2 @@
1
+ //= link_tree ../../javascripts/moduler .js
2
+ //= link_tree ../../builds/moduler .css
@@ -0,0 +1,9 @@
1
+ import "@hotwired/turbo-rails"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4
+
5
+ const application = Application.start()
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ eagerLoadControllersFrom("moduler/controllers", application)
@@ -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
+ }
@@ -0,0 +1,21 @@
1
+ @import "tailwindcss";
2
+ @import "tailwindcss/utilities";
3
+ @plugin "@tailwindcss/forms";
4
+ @plugin "@tailwindcss/aspect-ratio";
5
+ @plugin "@tailwindcss/typography";
6
+
7
+ @source inline('bg-yellow-100');
8
+ @source inline('bg-red-100');
9
+ @source inline('bg-green-100');
10
+
11
+ @source "../../../views/**/*.html.erb";
12
+ @source "../../../components/**/*.html.erb";
13
+
14
+ @theme {
15
+ --color-custom: #000;
16
+ --font-custom: "Arial", sans-serif;
17
+ }
18
+
19
+ .prose-100 {
20
+ max-width: 100%;
21
+ }
@@ -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
@@ -0,0 +1,62 @@
1
+ module Moduler
2
+ module Authentication
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :require_authentication
7
+ helper_method :authenticated?
8
+ end
9
+
10
+ class_methods do
11
+ def allow_unauthenticated_access(**options)
12
+ skip_before_action :require_authentication, **options
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def authenticated?
19
+ resume_session
20
+ end
21
+
22
+ def require_authentication
23
+ resume_session || request_authentication
24
+ end
25
+
26
+ def resume_session
27
+ Current.session ||= find_session_by_cookie
28
+ end
29
+
30
+ def find_session_by_cookie
31
+ if cookies.signed[:session_id]
32
+ Session.active.find_by(id: cookies.signed[:session_id], signed_in_at: Session::LIFETIME.ago..)
33
+ end
34
+ end
35
+
36
+ def request_authentication
37
+ redirect_to moduler.new_session_path
38
+ end
39
+
40
+ def after_authentication_url
41
+ moduler.root_path
42
+ end
43
+
44
+ def start_new_session_for(user)
45
+ user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip, signed_in_at: Time.current).tap do |session|
46
+ Current.session = session
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
+ }
54
+ end
55
+ end
56
+
57
+ def terminate_session
58
+ Current.session.touch(:signed_out_at)
59
+ cookies.delete(:session_id)
60
+ end
61
+ end
62
+ end
@@ -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