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.
- checksums.yaml +4 -4
- data/app/assets/builds/moduler/tailwind.css +1358 -1
- data/app/assets/javascripts/moduler/application.js +1 -0
- data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -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/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/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/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/session.rb +12 -0
- data/app/models/moduler/user.rb +9 -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/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/common/_flash.html.erb +16 -0
- data/app/views/moduler/common/_header.html.erb +12 -3
- data/app/views/moduler/common/_sidebar.html.erb +6 -0
- data/app/views/moduler/common/administration/_sidebar.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 +1 -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/common/en.yml +5 -0
- data/config/locales/moduler/components/en.yml +11 -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 +14 -1
- data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
- data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
- data/lib/moduler/engine.rb +2 -0
- data/lib/moduler/version.rb +1 -1
- data/lib/moduler.rb +4 -0
- data/lib/tasks/moduler_tasks.rake +2 -4
- metadata +151 -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,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)
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
module Moduler
|
|
2
2
|
class Session < ApplicationRecord
|
|
3
|
+
include PgSearch::Model
|
|
4
|
+
|
|
5
|
+
LIFETIME = 30.days
|
|
6
|
+
|
|
3
7
|
belongs_to :user, class_name: "Moduler::User", foreign_key: "moduler_user_id"
|
|
8
|
+
|
|
9
|
+
scope :active, -> { where(signed_out_at: nil) }
|
|
10
|
+
|
|
11
|
+
pg_search_scope :search_by_query, against: %i[ ip_address user_agent ], associated_against: { user: %i[ email_address ] }, using: { trigram: { word_similarity: true } }
|
|
12
|
+
|
|
13
|
+
def active?
|
|
14
|
+
signed_out_at.nil?
|
|
15
|
+
end
|
|
4
16
|
end
|
|
5
17
|
end
|
data/app/models/moduler/user.rb
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
module Moduler
|
|
2
2
|
class User < ApplicationRecord
|
|
3
|
+
include PgSearch::Model
|
|
4
|
+
|
|
3
5
|
has_many :sessions, class_name: "Moduler::Session", foreign_key: "moduler_user_id", dependent: :destroy
|
|
4
6
|
|
|
5
7
|
has_secure_password
|
|
6
8
|
|
|
9
|
+
normalizes :email_address, with: ->(email_address) { email_address.strip.downcase }
|
|
10
|
+
|
|
7
11
|
validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
12
|
+
validates :password, length: { minimum: 8 }, allow_nil: true
|
|
13
|
+
|
|
14
|
+
enum :role, { member: 0, administrator: 1 }
|
|
15
|
+
|
|
16
|
+
pg_search_scope :search_by_query, against: %i[ email_address ], using: { trigram: { word_similarity: true } }
|
|
8
17
|
end
|
|
9
18
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
module Administration
|
|
3
|
+
class UserPolicy < BasePolicy
|
|
4
|
+
def index?
|
|
5
|
+
administrator?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create?
|
|
9
|
+
administrator?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def update?
|
|
13
|
+
administrator? && record != user
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new?
|
|
17
|
+
create?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def edit?
|
|
21
|
+
update?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class ApplicationPolicy
|
|
3
|
+
attr_reader :user, :record
|
|
4
|
+
|
|
5
|
+
def initialize(user, record)
|
|
6
|
+
@user = user
|
|
7
|
+
@record = record
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def index?
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show?
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new?
|
|
23
|
+
create?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def edit?
|
|
31
|
+
update?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<span class="px-2 text-slate-400"><%= raw(t("views.pagination.truncate")) %></span>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= link_to raw(t("views.pagination.next")), url, rel: "next", remote: remote, class: "rounded px-3 py-1 text-slate-600 hover:bg-slate-100" %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= paginator.render do %>
|
|
2
|
+
<nav class="flex flex-wrap items-center justify-end gap-1 text-sm" aria-label="Pagination">
|
|
3
|
+
<%= prev_page_tag unless current_page.first? %>
|
|
4
|
+
<% each_page do |page| %>
|
|
5
|
+
<% if page.left_outer? || page.right_outer? || page.inside_window? %>
|
|
6
|
+
<%= page_tag page %>
|
|
7
|
+
<% elsif !page.was_truncated? %>
|
|
8
|
+
<%= gap_tag %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<%= next_page_tag unless current_page.last? %>
|
|
12
|
+
</nav>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= link_to raw(t("views.pagination.previous")), url, rel: "prev", remote: remote, class: "rounded px-3 py-1 text-slate-600 hover:bg-slate-100" %>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Moduler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
6
|
<%= csrf_meta_tags %>
|
|
6
7
|
<%= csp_meta_tag %>
|
|
7
8
|
|
|
@@ -9,14 +10,12 @@
|
|
|
9
10
|
|
|
10
11
|
<%= stylesheet_link_tags_if_exist "moduler/tailwind", "moduler/application", media: "all" %>
|
|
11
12
|
|
|
12
|
-
<%# Include host application stylesheets %>
|
|
13
|
-
<%= stylesheet_link_tags_if_exist "tailwind", "application", media: "all" %>
|
|
14
|
-
|
|
15
13
|
<%= javascript_importmap_tags "moduler/application" %>
|
|
16
14
|
</head>
|
|
17
|
-
<body>
|
|
18
|
-
<div class="
|
|
19
|
-
<%= render "moduler/common/header" %>
|
|
15
|
+
<body class="flex min-h-screen flex-col">
|
|
16
|
+
<div class="flex flex-1 flex-col">
|
|
17
|
+
<%= render "moduler/common/header" if authenticated? %>
|
|
18
|
+
<%= render "moduler/common/flash" %>
|
|
20
19
|
|
|
21
20
|
<%= yield %>
|
|
22
21
|
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% current = session == Moduler::Current.session %>
|
|
2
|
+
|
|
3
|
+
<%= render(Moduler::ListItemComponent.new(session.user.email_address, id: dom_id(session))) do |item| %>
|
|
4
|
+
<% if current %>
|
|
5
|
+
<% item.with_badge(t(".status.current"), variant: :success) %>
|
|
6
|
+
<% elsif session.active? %>
|
|
7
|
+
<% item.with_badge(t(".status.active")) %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<% item.with_badge(t(".status.signed_out"), variant: :muted) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<% item.with_meta do %>
|
|
13
|
+
<%= session.ip_address.presence || t(".unknown_ip") %>
|
|
14
|
+
<span aria-hidden="true">·</span>
|
|
15
|
+
<span title="<%= session.user_agent %>"><%= truncate(session.user_agent.presence || t(".unknown_device"), length: 60) %></span>
|
|
16
|
+
<span aria-hidden="true">·</span>
|
|
17
|
+
<%= time_tag session.signed_in_at, t(".signed_in", time: time_ago_in_words(session.signed_in_at)), title: l(session.signed_in_at, format: :long) %>
|
|
18
|
+
<% if session.signed_out_at %>
|
|
19
|
+
<span aria-hidden="true">·</span>
|
|
20
|
+
<%= time_tag session.signed_out_at, t(".signed_out", time: time_ago_in_words(session.signed_out_at)), title: l(session.signed_out_at, format: :long) %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<% if session.active? %>
|
|
25
|
+
<% item.with_action do %>
|
|
26
|
+
<%= render(Moduler::ButtonComponent.new(t(".action.destroy"), moduler.administration_session_path(session), method: :delete, size: :sm,
|
|
27
|
+
confirm: t(current ? ".confirmation.destroy_current" : ".confirmation.destroy"))) %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%= render(Moduler::Administration::PageComponent.new) do %>
|
|
2
|
+
<%= render(Moduler::PageHeaderComponent.new(t(".title"), description: t(".description"))) %>
|
|
3
|
+
|
|
4
|
+
<%= render(Moduler::FilterComponent.new(@filter, moduler.administration_sessions_path)) %>
|
|
5
|
+
|
|
6
|
+
<%= render(Moduler::ListComponent.new(@sessions, id: "sessions", empty: t(".empty"))) do %>
|
|
7
|
+
<%= render partial: "moduler/administration/sessions/session", collection: @sessions %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= render(Moduler::ListItemComponent.new(user.email_address, id: dom_id(user))) do |item| %>
|
|
2
|
+
<% item.with_badge(t("moduler.administration.users.roles.#{user.role}"), variant: user.administrator? ? :success : :neutral) %>
|
|
3
|
+
|
|
4
|
+
<% item.with_meta do %>
|
|
5
|
+
<%= time_tag user.created_at, t(".created", time: time_ago_in_words(user.created_at)), title: l(user.created_at, format: :long) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<% if Moduler::Administration::UserPolicy.new(current_user, user).edit? %>
|
|
9
|
+
<% item.with_action do %>
|
|
10
|
+
<%= render(Moduler::ButtonLinkComponent.new(t(".action.edit"), moduler.edit_administration_user_path(user), size: :sm)) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= render(Moduler::Administration::PageComponent.new) do %>
|
|
2
|
+
<%= render(Moduler::PageHeaderComponent.new(t(".title"))) %>
|
|
3
|
+
|
|
4
|
+
<%= form_with model: @user, url: moduler.administration_user_path(@user), method: :patch, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
|
|
5
|
+
<%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
|
|
6
|
+
|
|
7
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
|
|
8
|
+
<%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"))) %>
|
|
9
|
+
<%= render(::Moduler::Form::SelectComponent.new(form, :role, Moduler::User.roles.keys.map { |role| [ t("moduler.administration.users.roles.#{role}"), role ] }, label: t(".label.role"))) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.save"))) %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= render(Moduler::Administration::PageComponent.new) do %>
|
|
2
|
+
<%= render(Moduler::PageHeaderComponent.new(t(".title"), description: t(".description"))) do |header| %>
|
|
3
|
+
<% header.with_action do %>
|
|
4
|
+
<%= render(Moduler::ButtonLinkComponent.new(t(".action.new"), moduler.new_administration_user_path)) %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<%= render(Moduler::FilterComponent.new(@filter, moduler.administration_users_path)) %>
|
|
9
|
+
|
|
10
|
+
<%= render(Moduler::ListComponent.new(@users, id: "users", empty: t(".empty"))) do %>
|
|
11
|
+
<%= render partial: "moduler/administration/users/user", collection: @users %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%= render(Moduler::Administration::PageComponent.new) do %>
|
|
2
|
+
<%= render(Moduler::PageHeaderComponent.new(t(".title"))) %>
|
|
3
|
+
|
|
4
|
+
<%= form_with model: @user, url: moduler.administration_users_path, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
|
|
5
|
+
<%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
|
|
6
|
+
|
|
7
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
|
|
8
|
+
<%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"))) %>
|
|
9
|
+
<%= render(::Moduler::Form::SelectComponent.new(form, :role, Moduler::User.roles.keys.map { |role| [ t("moduler.administration.users.roles.#{role}"), role ] }, label: t(".label.role"))) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.password"), divider: true)) do %>
|
|
13
|
+
<%= render(::Moduler::Form::PasswordComponent.new(form, :password, label: t(".label.password"), description: t(".description.password"))) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.create"))) %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% if flash.any? { |_, message| message.present? } %>
|
|
2
|
+
<div class="fixed top-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2 px-4 sm:px-0">
|
|
3
|
+
<% flash.each do |key, message| %>
|
|
4
|
+
<% next if message.blank? %>
|
|
5
|
+
<div data-controller="flash"
|
|
6
|
+
class="flex items-start gap-3 rounded-md border px-4 py-3 text-sm shadow-lg transition duration-200 <%= key.to_s.in?(%w[alert error]) ? "border-red-200 bg-red-50 text-red-700" : key.to_s == "warning" ? "border-amber-200 bg-amber-50 text-amber-700" : "border-emerald-200 bg-emerald-50 text-emerald-700" %>">
|
|
7
|
+
<p class="flex-1"><%= message %></p>
|
|
8
|
+
<button type="button" data-action="flash#dismiss" class="shrink-0 cursor-pointer text-slate-400 transition hover:text-slate-600">
|
|
9
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="h-4 w-4">
|
|
10
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
11
|
+
</svg>
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
<nav class="border-b border-gray-200 bg-white">
|
|
2
|
-
<div class="
|
|
3
|
-
<div class="flex items-center gap-8">
|
|
2
|
+
<div class="flex h-16 w-full items-center justify-between gap-4 px-4 sm:px-6 lg:px-6">
|
|
3
|
+
<div class="flex shrink-0 items-center gap-4 sm:gap-8">
|
|
4
4
|
<%= link_to t(".nav.home"), moduler.root_path, class: "text-sm/6 font-semibold text-gray-900 hover:text-custom" %>
|
|
5
|
+
|
|
6
|
+
<% if policy([:moduler, :administration, :home]).index? %>
|
|
7
|
+
|
|
|
8
|
+
<%= link_to t(".nav.administration"), moduler.administration_root_path, class: "text-sm/6 font-semibold text-gray-900 hover:text-custom" %>
|
|
9
|
+
<% end %>
|
|
5
10
|
</div>
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
<div class="flex min-w-0 items-center gap-3 text-sm text-gray-900 sm:gap-6">
|
|
13
|
+
<%= link_to current_user.email_address, moduler.edit_profile_path, class: "truncate" %>
|
|
14
|
+
|
|
|
15
|
+
<%= button_to t(".action.sign_out"), moduler.session_path, method: :delete, class: "cursor-pointer whitespace-nowrap font-semibold" %>
|
|
16
|
+
</div>
|
|
8
17
|
</div>
|
|
9
18
|
</nav>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<aside class="-mx-4 border-b border-gray-200 px-2 md:mx-0 md:w-1/8 md:shrink-0 md:border-b-0 md:border-r md:px-0">
|
|
2
|
+
<nav class="flex gap-1 overflow-x-auto md:flex-col text-sm p-2">
|
|
3
|
+
<%= render(Moduler::SidebarActionComponent.new(t(".action.profile"), moduler.edit_profile_path, "moduler/profile")) %>
|
|
4
|
+
<%= render(Moduler::SidebarActionComponent.new(t(".action.sessions"), moduler.profile_sessions_path, "moduler/profile/sessions")) %>
|
|
5
|
+
</nav>
|
|
6
|
+
</aside>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<aside class="-mx-4 border-b border-gray-200 px-2 md:mx-0 md:w-1/8 md:shrink-0 md:border-b-0 md:border-r md:px-0">
|
|
2
|
+
<nav class="flex gap-1 overflow-x-auto md:flex-col text-sm p-2">
|
|
3
|
+
<%= render(Moduler::SidebarActionComponent.new(t(".action.users"), moduler.administration_users_path, "moduler/administration/users")) %>
|
|
4
|
+
<%= render(Moduler::SidebarActionComponent.new(t(".action.sessions"), moduler.administration_sessions_path, "moduler/administration/sessions")) %>
|
|
5
|
+
</nav>
|
|
6
|
+
</aside>
|
|
@@ -0,0 +1,28 @@
|
|
|
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/sidebar" %>
|
|
3
|
+
|
|
4
|
+
<div class="flex min-w-0 flex-1 flex-col pb-4 md:mt-8">
|
|
5
|
+
<div class="mb-4 flex min-h-9 items-center justify-between gap-4">
|
|
6
|
+
<h1 class="text-base font-semibold text-gray-900"><%= t(".title") %></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<%= form_with model: @user, url: moduler.profile_path, method: :patch, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
|
|
10
|
+
<%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
|
|
11
|
+
|
|
12
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
|
|
13
|
+
<%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"), disabled: :disabled)) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.password"), divider: true)) do %>
|
|
17
|
+
<%= render(::Moduler::Form::PasswordComponent.new(form, :password, label: t(".label.password"))) %>
|
|
18
|
+
<%= render(::Moduler::Form::PasswordComponent.new(form, :password_confirmation, label: t(".label.password_confirmation"), description: t(".description.password_confirmation"))) %>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<%= render(::Moduler::Form::SectionComponent.new(t(".section.current_password"), divider: true)) do %>
|
|
22
|
+
<%= render(::Moduler::Form::PasswordComponent.new(form, :password_challenge, label: t(".label.password_challenge"), description: t(".description.password_challenge"))) %>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.save"))) %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</main>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% current = session == Moduler::Current.session %>
|
|
2
|
+
<% mobile = session.user_agent.to_s.match?(/Mobile|Android|iPhone|iPad/) %>
|
|
3
|
+
|
|
4
|
+
<li id="<%= dom_id(session) %>" class="flex items-center justify-between gap-4 px-4 py-4 sm:px-5 <%= "bg-slate-50/60" unless session.active? %>">
|
|
5
|
+
<div class="flex min-w-0 items-start gap-4">
|
|
6
|
+
<div class="flex size-10 shrink-0 items-center justify-center rounded-full <%= session.active? ? "bg-slate-100 text-slate-600" : "bg-slate-100/60 text-slate-400" %>">
|
|
7
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true">
|
|
8
|
+
<% if mobile %>
|
|
9
|
+
<rect x="7" y="2.5" width="10" height="19" rx="2" />
|
|
10
|
+
<path d="M11 18.5h2" />
|
|
11
|
+
<% else %>
|
|
12
|
+
<rect x="3" y="4" width="18" height="12" rx="1.5" />
|
|
13
|
+
<path d="M8 20h8M12 16v4" />
|
|
14
|
+
<% end %>
|
|
15
|
+
</svg>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="min-w-0">
|
|
19
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
20
|
+
<p class="text-sm font-semibold <%= session.active? ? "text-gray-900" : "text-slate-500" %>">
|
|
21
|
+
<%= session.ip_address.presence || t(".unknown_ip") %>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<% if current %>
|
|
25
|
+
<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"><%= t(".status.current") %></span>
|
|
26
|
+
<% elsif session.active? %>
|
|
27
|
+
<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"><%= t(".status.active") %></span>
|
|
28
|
+
<% else %>
|
|
29
|
+
<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"><%= t(".status.signed_out") %></span>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<p class="mt-1 truncate text-xs text-slate-500" title="<%= session.user_agent %>">
|
|
34
|
+
<%= session.user_agent.presence || t(".unknown_device") %>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p class="mt-1 text-xs text-slate-400">
|
|
38
|
+
<%= time_tag session.signed_in_at, t(".signed_in", time: time_ago_in_words(session.signed_in_at)), title: l(session.signed_in_at, format: :long) %>
|
|
39
|
+
<% if session.signed_out_at %>
|
|
40
|
+
<span aria-hidden="true">·</span>
|
|
41
|
+
<%= time_tag session.signed_out_at, t(".signed_out", time: time_ago_in_words(session.signed_out_at)), title: l(session.signed_out_at, format: :long) %>
|
|
42
|
+
<% end %>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<% if session.active? %>
|
|
48
|
+
<%= button_to t(".action.destroy"), moduler.profile_session_path(session), method: :delete,
|
|
49
|
+
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",
|
|
50
|
+
form: { class: "shrink-0", data: { turbo_confirm: t(current ? ".confirmation.destroy_current" : ".confirmation.destroy") } } %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</li>
|
|
@@ -0,0 +1,26 @@
|
|
|
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/sidebar" %>
|
|
3
|
+
|
|
4
|
+
<div class="flex min-w-0 flex-1 flex-col pb-4 md:mt-8">
|
|
5
|
+
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
6
|
+
<div>
|
|
7
|
+
<h1 class="text-base font-semibold text-gray-900"><%= t(".title") %></h1>
|
|
8
|
+
<p class="mt-1 text-sm text-slate-500"><%= t(".description") %></p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%= button_to t(".action.revoke_all"), moduler.profile_sessions_revocation_path, method: :delete,
|
|
12
|
+
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",
|
|
13
|
+
form: { class: "shrink-0", data: { turbo_confirm: t(".confirmation.revoke") } } %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="flex-1">
|
|
17
|
+
<ul id="sessions" role="list" class="divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white">
|
|
18
|
+
<%= render partial: "moduler/profile/sessions/session", collection: @sessions %>
|
|
19
|
+
</ul>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="mt-4 flex justify-end">
|
|
23
|
+
<%= paginate @sessions %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</main>
|