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.
- checksums.yaml +4 -4
- data/app/assets/builds/moduler/tailwind.css +1359 -0
- data/app/assets/config/moduler/manifest.js +2 -0
- data/app/assets/javascripts/moduler/application.js +9 -0
- data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
- data/app/assets/stylesheets/moduler/tailwind.css +21 -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 +62 -0
- 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 +18 -0
- data/app/controllers/moduler/home_controller.rb +5 -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 +39 -0
- data/app/controllers/moduler/users_controller.rb +36 -0
- data/app/filters/application_filter.rb +41 -0
- data/app/filters/query_filter.rb +18 -0
- data/app/helpers/moduler/application_helper.rb +4 -0
- data/app/lib/moduler/form_builder.rb +23 -0
- data/app/models/moduler/current.rb +6 -0
- data/app/models/moduler/session.rb +17 -0
- data/app/models/moduler/user.rb +18 -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 +16 -10
- 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 +18 -0
- 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/home/index.html.erb +0 -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 +31 -0
- data/app/views/moduler/users/new.html.erb +26 -0
- data/config/importmap.rb +5 -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 +13 -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 +18 -0
- data/config/locales/moduler/users/en.yml +21 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20260322152259_create_moduler_users.rb +12 -0
- data/db/migrate/20260322153744_create_moduler_sessions.rb +13 -0
- 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/configuration.rb +12 -0
- data/lib/moduler/engine.rb +20 -0
- data/lib/moduler/router.rb +10 -0
- data/lib/moduler/routing.rb +11 -0
- data/lib/moduler/version.rb +1 -1
- data/lib/moduler.rb +12 -1
- data/lib/tasks/moduler_tasks.rake +42 -4
- metadata +250 -2
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
module Moduler
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
|
+
include Authentication
|
|
4
|
+
include Pundit::Authorization
|
|
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
|
|
3
21
|
end
|
|
4
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
|
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class SessionsController < ApplicationController
|
|
3
|
+
allow_unauthenticated_access only: %i[ new create ]
|
|
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
|
+
|
|
12
|
+
def new
|
|
13
|
+
if authenticated?
|
|
14
|
+
redirect_to after_authentication_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@user = User.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
if user = User.authenticate_by(params.require(:user).permit(:email_address, :password))
|
|
22
|
+
start_new_session_for user
|
|
23
|
+
|
|
24
|
+
redirect_to after_authentication_url
|
|
25
|
+
else
|
|
26
|
+
@user = User.new.tap do |user|
|
|
27
|
+
user.errors.add(:base, t("moduler.sessions.create.invalid_credentials"))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
render :new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
terminate_session
|
|
36
|
+
redirect_to moduler.new_session_path, status: :see_other
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class UsersController < ApplicationController
|
|
3
|
+
allow_unauthenticated_access only: %i[ new create ]
|
|
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
|
+
|
|
12
|
+
def new
|
|
13
|
+
if authenticated?
|
|
14
|
+
redirect_to after_authentication_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@user = User.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@user = User.new(permitted_parameters)
|
|
22
|
+
|
|
23
|
+
if @user.save
|
|
24
|
+
redirect_to moduler.new_session_path, notice: t("moduler.users.create.success")
|
|
25
|
+
else
|
|
26
|
+
render :new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def permitted_parameters
|
|
33
|
+
params.require(:user).permit(:email_address, :password, :password_confirmation)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -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
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
module Moduler
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def stylesheet_link_tags_if_exist(*sources, **options)
|
|
4
|
+
existing = sources.select { |source| Rails.application.assets.load_path.find("#{source}.css") }
|
|
5
|
+
stylesheet_link_tag(*existing, **options) if existing.any?
|
|
6
|
+
end
|
|
3
7
|
end
|
|
4
8
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
3
|
+
def error(field, options = {})
|
|
4
|
+
return unless error?(field)
|
|
5
|
+
|
|
6
|
+
@template.content_tag :div, **options do
|
|
7
|
+
@object.errors[field].first
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def full_error(field, options = {})
|
|
12
|
+
return unless error?(field)
|
|
13
|
+
|
|
14
|
+
@template.content_tag :div, **options do
|
|
15
|
+
@object.errors.full_messages_for(field).first
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def error?(field)
|
|
20
|
+
@object && @object.errors[field].try(:any?)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class Session < ApplicationRecord
|
|
3
|
+
include PgSearch::Model
|
|
4
|
+
|
|
5
|
+
LIFETIME = 30.days
|
|
6
|
+
|
|
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
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Moduler
|
|
2
|
+
class User < ApplicationRecord
|
|
3
|
+
include PgSearch::Model
|
|
4
|
+
|
|
5
|
+
has_many :sessions, class_name: "Moduler::Session", foreign_key: "moduler_user_id", dependent: :destroy
|
|
6
|
+
|
|
7
|
+
has_secure_password
|
|
8
|
+
|
|
9
|
+
normalizes :email_address, with: ->(email_address) { email_address.strip.downcase }
|
|
10
|
+
|
|
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 } }
|
|
17
|
+
end
|
|
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" %>
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<title>Moduler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
<%= yield :head %>
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
11
|
+
<%= stylesheet_link_tags_if_exist "moduler/tailwind", "moduler/application", media: "all" %>
|
|
13
12
|
|
|
14
|
-
<%=
|
|
13
|
+
<%= javascript_importmap_tags "moduler/application" %>
|
|
14
|
+
</head>
|
|
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" %>
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
<%= yield %>
|
|
21
|
+
</div>
|
|
22
|
+
</body>
|
|
17
23
|
</html>
|
|
@@ -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 %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<nav class="border-b border-gray-200 bg-white">
|
|
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
|
+
<%= 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 %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
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>
|
|
17
|
+
</div>
|
|
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>
|
|
File without changes
|