katalyst-koi 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +23 -0
- data/Upgrade.md +6 -0
- data/app/assets/builds/koi/admin.css +1 -0
- data/app/assets/builds/koi/nav_items.css +1 -0
- data/app/assets/config/koi.js +10 -0
- data/app/assets/images/koi/application/chevron-right.svg +10 -0
- data/app/assets/images/koi/application/glyphicons-halflings-white.png +0 -0
- data/app/assets/images/koi/application/glyphicons-halflings.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-down.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-up.png +0 -0
- data/app/assets/images/koi/application/icon-file-doc.png +0 -0
- data/app/assets/images/koi/application/icon-file-img.png +0 -0
- data/app/assets/images/koi/application/icon-file-pdf.png +0 -0
- data/app/assets/images/koi/application/icon-file-ppt.png +0 -0
- data/app/assets/images/koi/application/icon-file-unknown.png +0 -0
- data/app/assets/images/koi/application/icon-file-xls.png +0 -0
- data/app/assets/images/koi/application/icon-file-zip.png +0 -0
- data/app/assets/images/koi/application/icon-form-date-picker.png +0 -0
- data/app/assets/images/koi/application/icon-form-error.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-ascending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-descending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort.png +0 -0
- data/app/assets/images/koi/application/icon-index-sortable.png +0 -0
- data/app/assets/images/koi/application/icon-menu-cursor.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-add.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-close.png +0 -0
- data/app/assets/images/koi/application/icon-sortable.png +0 -0
- data/app/assets/images/koi/application/jcrop.gif +0 -0
- data/app/assets/images/koi/application/loading.gif +0 -0
- data/app/assets/images/koi/application/select-arrow.svg +3 -0
- data/app/assets/images/koi/application/select_arrow.png +0 -0
- data/app/assets/images/koi/application/sort-ascending.png +0 -0
- data/app/assets/images/koi/application/sort-descending.png +0 -0
- data/app/assets/javascripts/koi/admin.js +4 -0
- data/app/assets/javascripts/koi/controllers/application.js +11 -0
- data/app/assets/javascripts/koi/controllers/document_field_controller.js +26 -0
- data/app/assets/javascripts/koi/controllers/file_field_controller.js +143 -0
- data/app/assets/javascripts/koi/controllers/flash_controller.js +12 -0
- data/app/assets/javascripts/koi/controllers/form_request_submit_controller.js +11 -0
- data/app/assets/javascripts/koi/controllers/image_field_controller.js +24 -0
- data/app/assets/javascripts/koi/controllers/index.js +6 -0
- data/app/assets/javascripts/koi/controllers/index_actions_controller.js +61 -0
- data/app/assets/javascripts/koi/controllers/keyboard_controller.js +149 -0
- data/app/assets/javascripts/koi/controllers/navigation_controller.js +84 -0
- data/app/assets/javascripts/koi/controllers/navigation_toggle_controller.js +7 -0
- data/app/assets/javascripts/koi/controllers/show_hide_controller.js +25 -0
- data/app/assets/javascripts/koi/controllers/sluggable_controller.js +30 -0
- data/app/assets/javascripts/koi/controllers/webauthn_authentication_controller.js +23 -0
- data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +30 -0
- data/app/assets/javascripts/koi/utils/transition.js +220 -0
- data/app/assets/stylesheets/koi/admin.scss +27 -0
- data/app/assets/stylesheets/koi/base/_button.scss +122 -0
- data/app/assets/stylesheets/koi/base/_icon.scss +29 -0
- data/app/assets/stylesheets/koi/base/_index.scss +18 -0
- data/app/assets/stylesheets/koi/base/_input.scss +13 -0
- data/app/assets/stylesheets/koi/base/_link.scss +26 -0
- data/app/assets/stylesheets/koi/base/_list.scss +11 -0
- data/app/assets/stylesheets/koi/base/_typography.scss +160 -0
- data/app/assets/stylesheets/koi/components/_actions-group.scss +7 -0
- data/app/assets/stylesheets/koi/components/_image-field.scss +33 -0
- data/app/assets/stylesheets/koi/components/_index-actions.scss +69 -0
- data/app/assets/stylesheets/koi/components/_index-table.scss +91 -0
- data/app/assets/stylesheets/koi/components/_index.scss +6 -0
- data/app/assets/stylesheets/koi/components/_item-table.scss +33 -0
- data/app/assets/stylesheets/koi/components/_pagy.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_banner.scss +7 -0
- data/app/assets/stylesheets/koi/layouts/_content.scss +40 -0
- data/app/assets/stylesheets/koi/layouts/_flash.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_header.scss +62 -0
- data/app/assets/stylesheets/koi/layouts/_index.scss +48 -0
- data/app/assets/stylesheets/koi/layouts/_main.scss +23 -0
- data/app/assets/stylesheets/koi/layouts/_navigation.scss +156 -0
- data/app/assets/stylesheets/koi/layouts/_stack.scss +13 -0
- data/app/assets/stylesheets/koi/pages/_index.scss +1 -0
- data/app/assets/stylesheets/koi/pages/_login.scss +40 -0
- data/app/assets/stylesheets/koi/themes/_content.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_govuk.scss +52 -0
- data/app/assets/stylesheets/koi/themes/_index.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_kpop.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_navigation.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_trix.scss +32 -0
- data/app/assets/stylesheets/koi/utils/_breakpoints.scss +13 -0
- data/app/assets/stylesheets/koi/utils/_hide.scss +11 -0
- data/app/assets/stylesheets/koi/utils/_index.scss +2 -0
- data/app/assets/stylesheets/koi/utils/_typography.scss +24 -0
- data/app/components/koi/header/edit_component.rb +58 -0
- data/app/components/koi/header/index_component.rb +23 -0
- data/app/components/koi/header/new_component.rb +40 -0
- data/app/components/koi/header/show_component.rb +51 -0
- data/app/components/koi/header_component.html.erb +16 -0
- data/app/components/koi/header_component.rb +28 -0
- data/app/components/koi/index_table_component.rb +21 -0
- data/app/controllers/admin/admin_users_controller.rb +88 -0
- data/app/controllers/admin/application_controller.rb +9 -0
- data/app/controllers/admin/caches_controller.rb +11 -0
- data/app/controllers/admin/credentials_controller.rb +64 -0
- data/app/controllers/admin/dashboards_controller.rb +7 -0
- data/app/controllers/admin/sessions_controller.rb +78 -0
- data/app/controllers/admin/url_rewrites_controller.rb +87 -0
- data/app/controllers/concerns/koi/controller/has_admin_users.rb +49 -0
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +45 -0
- data/app/controllers/concerns/koi/controller/is_admin_controller.rb +52 -0
- data/app/helpers/katalyst/content/editor/errors.rb +21 -0
- data/app/helpers/katalyst/navigation/editor/errors.rb +21 -0
- data/app/helpers/koi/application_helper.rb +7 -0
- data/app/helpers/koi/date_helper.rb +36 -0
- data/app/helpers/koi/definition_list_helper.rb +92 -0
- data/app/helpers/koi/index_actions_helper.rb +99 -0
- data/app/jobs/koi/application_job.rb +6 -0
- data/app/mailers/koi/application_mailer.rb +8 -0
- data/app/models/admin/credential.rb +14 -0
- data/app/models/admin/user.rb +51 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/concerns/koi/model/archivable.rb +55 -0
- data/app/models/url_rewrite.rb +25 -0
- data/app/views/admin/admin_users/_admin.html+row.erb +4 -0
- data/app/views/admin/admin_users/_authentication.html.erb +15 -0
- data/app/views/admin/admin_users/_fields.html.erb +4 -0
- data/app/views/admin/admin_users/edit.html.erb +11 -0
- data/app/views/admin/admin_users/index.html.erb +9 -0
- data/app/views/admin/admin_users/new.html.erb +11 -0
- data/app/views/admin/admin_users/show.html.erb +22 -0
- data/app/views/admin/credentials/new.html.erb +14 -0
- data/app/views/admin/dashboards/show.html.erb +1 -0
- data/app/views/admin/sessions/new.html.erb +19 -0
- data/app/views/admin/shared/icons/_close.html.erb +8 -0
- data/app/views/admin/shared/icons/_cross.html.erb +3 -0
- data/app/views/admin/shared/icons/_menu.html.erb +3 -0
- data/app/views/admin/shared/icons/_refresh.html.erb +8 -0
- data/app/views/admin/url_rewrites/_form_fields.html.erb +3 -0
- data/app/views/admin/url_rewrites/_url_rewrite.html+row.erb +7 -0
- data/app/views/admin/url_rewrites/edit.html.erb +12 -0
- data/app/views/admin/url_rewrites/index.html.erb +10 -0
- data/app/views/admin/url_rewrites/new.html.erb +11 -0
- data/app/views/admin/url_rewrites/show.html.erb +16 -0
- data/app/views/katalyst/content/asides/_aside.html+form.erb +18 -0
- data/app/views/katalyst/content/columns/_column.html+form.erb +18 -0
- data/app/views/katalyst/content/contents/_content.html+form.erb +20 -0
- data/app/views/katalyst/content/figures/_figure.html+form.erb +17 -0
- data/app/views/katalyst/content/groups/_group.html+form.erb +18 -0
- data/app/views/katalyst/content/items/_item.html+form.erb +18 -0
- data/app/views/katalyst/content/sections/_section.html+form.erb +18 -0
- data/app/views/katalyst/navigation/items/_button.html.erb +15 -0
- data/app/views/katalyst/navigation/items/_heading.html.erb +11 -0
- data/app/views/katalyst/navigation/items/_link.html.erb +13 -0
- data/app/views/katalyst/navigation/menus/edit.html.erb +12 -0
- data/app/views/katalyst/navigation/menus/new.html.erb +9 -0
- data/app/views/katalyst/navigation/menus/show.html.erb +18 -0
- data/app/views/layouts/koi/_environment.html.erb +4 -0
- data/app/views/layouts/koi/_flash.html.erb +8 -0
- data/app/views/layouts/koi/_header.html.erb +11 -0
- data/app/views/layouts/koi/_navigation.html.erb +13 -0
- data/app/views/layouts/koi/_navigation_collapse.html.erb +3 -0
- data/app/views/layouts/koi/_navigation_header.html.erb +6 -0
- data/app/views/layouts/koi/_navigation_item.html.erb +12 -0
- data/app/views/layouts/koi/application.html.erb +59 -0
- data/app/views/layouts/koi/login.html.erb +29 -0
- data/config/importmap.rb +9 -0
- data/config/initializers/flipper.rb +13 -0
- data/config/initializers/pagy.rb +1 -0
- data/config/initializers/time_formats.rb +5 -0
- data/config/locales/koi.en.yml +18 -0
- data/config/locales/pagy.en.yml +6 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20120220130849_devise_create_admins.rb +56 -0
- data/db/migrate/20130509235316_add_url_rewriter.rb +13 -0
- data/db/migrate/20230213053854_convert_devise_admins_to_rails.rb +7 -0
- data/db/migrate/20230412023411_create_admin_user_credentials.rb +20 -0
- data/db/migrate/20230531063707_update_admin_users.rb +37 -0
- data/db/migrate/20230602033610_add_archived_to_admin_users.rb +7 -0
- data/db/seeds.rb +9 -0
- data/lib/generators/koi/active_record/active_record_generator.rb +43 -0
- data/lib/generators/koi/admin/USAGE +8 -0
- data/lib/generators/koi/admin/admin_generator.rb +20 -0
- data/lib/generators/koi/admin_controller/USAGE +17 -0
- data/lib/generators/koi/admin_controller/admin_controller_generator.rb +51 -0
- data/lib/generators/koi/admin_controller/templates/controller.rb.tt +81 -0
- data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +135 -0
- data/lib/generators/koi/admin_route/admin_route_generator.rb +62 -0
- data/lib/generators/koi/admin_views/USAGE +12 -0
- data/lib/generators/koi/admin_views/admin_views_generator.rb +54 -0
- data/lib/generators/koi/admin_views/templates/_fields.html.erb.tt +3 -0
- data/lib/generators/koi/admin_views/templates/_record.html+row.erb.tt +10 -0
- data/lib/generators/koi/admin_views/templates/edit.html.erb.tt +12 -0
- data/lib/generators/koi/admin_views/templates/index.html.erb.tt +7 -0
- data/lib/generators/koi/admin_views/templates/new.html.erb.tt +11 -0
- data/lib/generators/koi/admin_views/templates/show.html.erb.tt +18 -0
- data/lib/govuk_design_system_formbuilder/concerns/file_element.rb +115 -0
- data/lib/govuk_design_system_formbuilder/elements/document.rb +59 -0
- data/lib/govuk_design_system_formbuilder/elements/image.rb +86 -0
- data/lib/katalyst/koi.rb +3 -0
- data/lib/koi/caching.rb +15 -0
- data/lib/koi/config.rb +11 -0
- data/lib/koi/engine.rb +40 -0
- data/lib/koi/form_builder.rb +76 -0
- data/lib/koi/menu/builder.rb +68 -0
- data/lib/koi/menu.rb +46 -0
- data/lib/koi/middleware/url_redirect.rb +44 -0
- data/lib/koi/release.rb +52 -0
- data/lib/koi/version.rb +5 -0
- data/lib/koi.rb +37 -0
- data/spec/factories/admins.rb +9 -0
- data/spec/factories/url_rewrites.rb +9 -0
- metadata +430 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
@use "../utils/typography" as *;
|
2
|
+
|
3
|
+
@function govuk-definition($size) {
|
4
|
+
@return (
|
5
|
+
null: (
|
6
|
+
font-size: font-size($size, mobile, px),
|
7
|
+
line-height: line-height($size, mobile, px),
|
8
|
+
),
|
9
|
+
tablet: (
|
10
|
+
font-size: font-size($size, tablet, px),
|
11
|
+
line-height: line-height($size, tablet, px),
|
12
|
+
),
|
13
|
+
print: (
|
14
|
+
font-size: font-size($size, print, px),
|
15
|
+
line-height: line-height($size, print, px),
|
16
|
+
)
|
17
|
+
);
|
18
|
+
}
|
19
|
+
|
20
|
+
$govuk-font-family: "Inter";
|
21
|
+
$govuk-text-colour: #{var(--site-text-color)};
|
22
|
+
$govuk-typography-scale: (
|
23
|
+
80: govuk-definition(h1),
|
24
|
+
48: govuk-definition(h2),
|
25
|
+
36: govuk-definition(h3),
|
26
|
+
27: govuk-definition(h4),
|
27
|
+
24: govuk-definition(h5),
|
28
|
+
19: govuk-definition(h6),
|
29
|
+
16: govuk-definition(paragraph),
|
30
|
+
14: govuk-definition(small),
|
31
|
+
);
|
32
|
+
|
33
|
+
$govuk-input-border-colour: #{var(--site-text-color)};
|
34
|
+
|
35
|
+
@import "katalyst/govuk/formbuilder";
|
36
|
+
|
37
|
+
.govuk-input,
|
38
|
+
.govuk-textarea {
|
39
|
+
color: $govuk-text-colour;
|
40
|
+
}
|
41
|
+
|
42
|
+
.govuk-hint {
|
43
|
+
max-width: var(--text-width);
|
44
|
+
}
|
45
|
+
|
46
|
+
// in the context of Koi admin forms, hang the error border in the gutter
|
47
|
+
// not a generally applicable style
|
48
|
+
.govuk-form-group--error {
|
49
|
+
position: relative;
|
50
|
+
margin-left: -18px;
|
51
|
+
padding-left: 13px;
|
52
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
// navigation and content already load trix, ensure they are loaded first
|
2
|
+
@use "content";
|
3
|
+
@use "navigation";
|
4
|
+
|
5
|
+
// overrides for default trix styles to match our button styles
|
6
|
+
trix-toolbar {
|
7
|
+
.trix-button-group {
|
8
|
+
border: 1px solid var(--site-text-color);
|
9
|
+
border-radius: 0;
|
10
|
+
}
|
11
|
+
|
12
|
+
.trix-button {
|
13
|
+
color: var(--site-secondary);
|
14
|
+
border-bottom: none;
|
15
|
+
}
|
16
|
+
|
17
|
+
.trix-button:not(:first-child) {
|
18
|
+
border-left: 1px solid var(--site-text-color);
|
19
|
+
}
|
20
|
+
|
21
|
+
.trix-button.trix-active {
|
22
|
+
background-color: var(--site-secondary-light);
|
23
|
+
color: var(--site-on-secondary);
|
24
|
+
border-bottom: none;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
// set a maximim width for the text editor and toolbar to match other inputs
|
29
|
+
trix-toolbar,
|
30
|
+
trix-editor {
|
31
|
+
max-width: var(--text-width);
|
32
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
// At a 16px base font size this will be 640px (to match govuk)
|
2
|
+
@mixin tablet-breakpoint {
|
3
|
+
@media screen and (max-width: 40rem) {
|
4
|
+
@content;
|
5
|
+
}
|
6
|
+
}
|
7
|
+
|
8
|
+
// At a 16px base font size this will be 1120px
|
9
|
+
@mixin laptop-breakpoint {
|
10
|
+
@media screen and (max-width: 70rem) {
|
11
|
+
@content;
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
@use "sass:math";
|
2
|
+
|
3
|
+
$-font-sizes: () !default;
|
4
|
+
$-line-heights: () !default;
|
5
|
+
|
6
|
+
@function font-size($size, $breakpoint: null, $unit: rem) {
|
7
|
+
$font-size: map-get($-font-sizes, $size);
|
8
|
+
|
9
|
+
@if $unit == rem {
|
10
|
+
@return $font-size;
|
11
|
+
} @else if $unit == px {
|
12
|
+
@return math.div($font-size, 1rem) * 16px;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
@function line-height($size, $breakpoint: null, $unit: em) {
|
17
|
+
$line-height: map-get($-line-heights, $size);
|
18
|
+
|
19
|
+
@if $unit == em {
|
20
|
+
@return $line-height;
|
21
|
+
} @else if $unit == px {
|
22
|
+
@return math.div($line-height, 1em) * 16px;
|
23
|
+
}
|
24
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Header
|
5
|
+
class EditComponent < ViewComponent::Base
|
6
|
+
attr_reader :model, :resource
|
7
|
+
|
8
|
+
delegate :with_breadcrumb, :with_action, to: :@header
|
9
|
+
|
10
|
+
def initialize(resource:, title: nil)
|
11
|
+
super
|
12
|
+
|
13
|
+
@resource = resource
|
14
|
+
@title = title
|
15
|
+
|
16
|
+
@header = HeaderComponent.new(title: self.title)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
render @header do |header|
|
21
|
+
# capture nested component
|
22
|
+
@header = header
|
23
|
+
|
24
|
+
# render block, if any (delegating slots to header)
|
25
|
+
content
|
26
|
+
|
27
|
+
# add our breadcrumbs and actions
|
28
|
+
add_index(header)
|
29
|
+
add_show(header)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def title
|
34
|
+
@title || "Edit #{resource.model_name.human.downcase}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource_title
|
38
|
+
title = Koi.config.resource_name_candidates.reduce(nil) do |name, key|
|
39
|
+
name || resource.respond_to?(key) && resource.public_send(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
title.presence || resource.model_name.human
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_index(header)
|
46
|
+
header.with_breadcrumb(resource.model_name.human.pluralize, url_for(action: :index))
|
47
|
+
rescue ActionController::UrlGenerationError
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_show(header)
|
52
|
+
header.with_breadcrumb(resource_title, url_for(action: :show))
|
53
|
+
rescue ActionController::UrlGenerationError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Header
|
5
|
+
class IndexComponent < ViewComponent::Base
|
6
|
+
attr_reader :model
|
7
|
+
|
8
|
+
delegate :with_breadcrumb, :with_action, to: :@header
|
9
|
+
|
10
|
+
def initialize(model:, title: model.model_name.human.pluralize)
|
11
|
+
super
|
12
|
+
|
13
|
+
@header = HeaderComponent.new(title:)
|
14
|
+
@model = model
|
15
|
+
@title = title
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
render @header.with_content(content || "")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Header
|
5
|
+
class NewComponent < ViewComponent::Base
|
6
|
+
attr_reader :model
|
7
|
+
|
8
|
+
delegate :with_breadcrumb, :with_action, to: :@header
|
9
|
+
|
10
|
+
def initialize(model:, title: nil)
|
11
|
+
super
|
12
|
+
|
13
|
+
@model = model
|
14
|
+
@title = title
|
15
|
+
|
16
|
+
@header = HeaderComponent.new(title: self.title)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
render @header do |header|
|
21
|
+
# render block, if any (delegating slots to header)
|
22
|
+
content
|
23
|
+
|
24
|
+
# add our breadcrumbs and actions
|
25
|
+
add_index(header)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def title
|
30
|
+
@title || "New #{model.model_name.human.downcase}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_index(header)
|
34
|
+
header.with_breadcrumb(model.model_name.human.pluralize, url_for(action: :index))
|
35
|
+
rescue ActionController::UrlGenerationError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Header
|
5
|
+
class ShowComponent < ViewComponent::Base
|
6
|
+
attr_reader :resource
|
7
|
+
|
8
|
+
delegate :with_breadcrumb, :with_action, to: :@header
|
9
|
+
|
10
|
+
def initialize(resource:, title: nil)
|
11
|
+
super
|
12
|
+
|
13
|
+
@title = title
|
14
|
+
@resource = resource
|
15
|
+
|
16
|
+
@header = HeaderComponent.new(title: self.title)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
render @header do |header|
|
21
|
+
# render block, if any (delegating slots to header)
|
22
|
+
content
|
23
|
+
|
24
|
+
# add our breadcrumbs and actions
|
25
|
+
add_index(header)
|
26
|
+
add_edit(header)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def title
|
31
|
+
title = Koi.config.resource_name_candidates.reduce(@title) do |name, key|
|
32
|
+
name || resource.respond_to?(key) && resource.public_send(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
title.presence || resource.model_name.human
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_index(header)
|
39
|
+
header.with_breadcrumb(resource.model_name.human.pluralize, url_for(action: :index))
|
40
|
+
rescue ActionController::UrlGenerationError
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_edit(header)
|
45
|
+
header.with_action("Edit", url_for(action: :edit))
|
46
|
+
rescue ActionController::UrlGenerationError
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% content_for :title, @title %>
|
2
|
+
<header>
|
3
|
+
<h1 class="heading"><%= @title %></h1>
|
4
|
+
|
5
|
+
<div class="breadcrumbs" role="navigation" aria-label="Breadcrumbs">
|
6
|
+
<% breadcrumbs.each do |breadcrumb| %>
|
7
|
+
<%= breadcrumb %>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div class="actions" role="navigation" aria-label="Related pages">
|
12
|
+
<% actions.each do |action| %>
|
13
|
+
<%= action %>
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
</header>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
class HeaderComponent < ViewComponent::Base
|
5
|
+
renders_many :actions, "LinkComponent"
|
6
|
+
renders_many :breadcrumbs, "LinkComponent"
|
7
|
+
|
8
|
+
def initialize(title:)
|
9
|
+
super
|
10
|
+
|
11
|
+
@title = title
|
12
|
+
end
|
13
|
+
|
14
|
+
class LinkComponent < ViewComponent::Base
|
15
|
+
def initialize(name, path, **options)
|
16
|
+
super
|
17
|
+
|
18
|
+
@name = name
|
19
|
+
@path = path
|
20
|
+
@options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
link_to @name, @path, **@options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
class IndexTableComponent < ViewComponent::Base
|
5
|
+
attr_reader :table, :pagination, :id, :pagy_id
|
6
|
+
|
7
|
+
def initialize(collection:, id: "index-table", **options)
|
8
|
+
super
|
9
|
+
|
10
|
+
@id = id
|
11
|
+
@pagy_id = "#{id}-pagination"
|
12
|
+
@table = Katalyst::Turbo::TableComponent.new(collection:, id:, class: "index-table", caption: true, **options)
|
13
|
+
@pagination = Katalyst::Turbo::PagyNavComponent.new(collection:, id: pagy_id) if collection.paginate?
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
concat(table.render_in(view_context))
|
18
|
+
concat(pagination.render_in(view_context)) if pagination
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class AdminUsersController < ApplicationController
|
5
|
+
before_action :set_admin, only: %i[show edit update destroy]
|
6
|
+
|
7
|
+
attr_reader :admin
|
8
|
+
|
9
|
+
def index
|
10
|
+
collection = Collection.new.with_params(params).apply(Admin::User.strict_loading)
|
11
|
+
table = Koi::IndexTableComponent.new(collection:)
|
12
|
+
|
13
|
+
respond_to do |format|
|
14
|
+
format.turbo_stream { render(table) } if self_referred?
|
15
|
+
format.html { render :index, locals: { table:, collection: } }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def show
|
20
|
+
render :show, locals: { admin: }
|
21
|
+
end
|
22
|
+
|
23
|
+
def new
|
24
|
+
@admin = Admin::User.new
|
25
|
+
|
26
|
+
render :new, locals: { admin: }
|
27
|
+
end
|
28
|
+
|
29
|
+
def edit
|
30
|
+
render :edit, locals: { admin: }
|
31
|
+
end
|
32
|
+
|
33
|
+
def create
|
34
|
+
admin = Admin::User.new(admin_user_params)
|
35
|
+
|
36
|
+
if admin.save
|
37
|
+
redirect_to admin_admin_user_path(admin)
|
38
|
+
else
|
39
|
+
render :new, locals: { admin: }, status: :unprocessable_entity
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def update
|
44
|
+
if admin.update(admin_user_params)
|
45
|
+
redirect_to action: :show
|
46
|
+
else
|
47
|
+
render :edit, locals: { admin: }, status: :unprocessable_entity
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
admin.destroy
|
53
|
+
|
54
|
+
redirect_to admin_admin_users_path
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def set_admin
|
60
|
+
@admin = Admin::User.with_archived.find(params[:id])
|
61
|
+
end
|
62
|
+
|
63
|
+
def admin_user_params
|
64
|
+
params.require(:admin).permit(:name, :email, :password, :archived)
|
65
|
+
end
|
66
|
+
|
67
|
+
class Collection < Katalyst::Tables::Collection::Base
|
68
|
+
attribute :search, :string, default: ""
|
69
|
+
attribute :scope, :string, default: "active"
|
70
|
+
|
71
|
+
config.sorting = :name
|
72
|
+
config.paginate = true
|
73
|
+
|
74
|
+
def filter
|
75
|
+
self.items = items.admin_search(search) if search.present?
|
76
|
+
|
77
|
+
self.items = case scope&.to_sym
|
78
|
+
when :archived
|
79
|
+
items.archived
|
80
|
+
when :all
|
81
|
+
items.with_archived
|
82
|
+
else
|
83
|
+
items
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class CachesController < ApplicationController
|
5
|
+
def destroy
|
6
|
+
Rails.logger.warn("[CACHE CLEAR] - Cleaning entire cache manually by #{current_admin} request")
|
7
|
+
Rails.cache.clear
|
8
|
+
redirect_back(fallback_location: admin_dashboard_path)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class CredentialsController < ApplicationController
|
5
|
+
include Koi::Controller::HasWebauthn
|
6
|
+
|
7
|
+
layout "kpop"
|
8
|
+
|
9
|
+
def new
|
10
|
+
unless current_admin.webauthn_id
|
11
|
+
current_admin.update!(webauthn_id: WebAuthn.generate_user_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
options = webauthn_relying_party.options_for_registration(
|
15
|
+
user: {
|
16
|
+
id: current_admin.webauthn_id,
|
17
|
+
name: current_admin.email,
|
18
|
+
display_name: current_admin.to_s,
|
19
|
+
},
|
20
|
+
exclude: current_admin.credentials.map(&:external_id),
|
21
|
+
)
|
22
|
+
|
23
|
+
# Store the newly generated challenge somewhere so you can have it
|
24
|
+
# for the verification phase.
|
25
|
+
session[:creation_challenge] = options.challenge
|
26
|
+
|
27
|
+
render :new, locals: { options: }
|
28
|
+
end
|
29
|
+
|
30
|
+
def create
|
31
|
+
redirect_to(action: :new) if session[:creation_challenge].blank?
|
32
|
+
|
33
|
+
webauthn_credential = webauthn_relying_party.verify_registration(
|
34
|
+
JSON.parse(credential_params[:response]),
|
35
|
+
session.delete(:creation_challenge),
|
36
|
+
)
|
37
|
+
|
38
|
+
credential = current_admin.credentials.find_or_initialize_by(
|
39
|
+
external_id: webauthn_credential.id,
|
40
|
+
)
|
41
|
+
|
42
|
+
credential.update!(
|
43
|
+
nickname: credential_params[:nickname],
|
44
|
+
public_key: webauthn_credential.public_key,
|
45
|
+
sign_count: webauthn_credential.sign_count,
|
46
|
+
)
|
47
|
+
|
48
|
+
redirect_to admin_admin_user_path(current_admin), status: :see_other
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
credential = current_admin.credentials.find(params[:id])
|
53
|
+
credential.destroy!
|
54
|
+
|
55
|
+
redirect_to admin_admin_user_path(current_admin), status: :see_other
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def credential_params
|
61
|
+
params.require(:admin_credential).permit(:nickname, :response)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class SessionsController < ApplicationController
|
5
|
+
include Koi::Controller::HasWebauthn
|
6
|
+
|
7
|
+
skip_before_action :authenticate_admin, only: %i[new create]
|
8
|
+
|
9
|
+
layout "koi/login"
|
10
|
+
|
11
|
+
def new
|
12
|
+
return redirect_to admin_dashboard_path if admin_signed_in?
|
13
|
+
|
14
|
+
render :new, locals: { admin_user: Admin::User.new }
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
if (admin_user = webauthn_authenticate!)
|
19
|
+
record_sign_in!(admin_user)
|
20
|
+
|
21
|
+
session[:admin_user_id] = admin_user.id
|
22
|
+
|
23
|
+
redirect_to admin_dashboard_path, notice: "You have been logged in"
|
24
|
+
elsif (admin_user = Admin::User.authenticate_by(session_params.slice(:email, :password)))
|
25
|
+
record_sign_in!(admin_user)
|
26
|
+
|
27
|
+
session[:admin_user_id] = admin_user.id
|
28
|
+
|
29
|
+
redirect_to admin_dashboard_path, notice: "You have been logged in"
|
30
|
+
else
|
31
|
+
admin_user = Admin::User.new(session_params.slice(:email, :password))
|
32
|
+
admin_user.errors.add(:email, "Invalid email or password")
|
33
|
+
|
34
|
+
render :new, status: :unprocessable_entity, locals: { admin_user: }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy
|
39
|
+
record_sign_out!(current_admin_user)
|
40
|
+
|
41
|
+
session[:admin_user_id] = nil
|
42
|
+
|
43
|
+
redirect_to admin_dashboard_path, notice: "You have been logged out"
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def session_params
|
49
|
+
params.require(:admin).permit(:email, :password, :response)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_last_sign_in(admin_user)
|
53
|
+
return if admin_user.current_sign_in_at.blank?
|
54
|
+
|
55
|
+
admin_user.last_sign_in_at = admin_user.current_sign_in_at
|
56
|
+
admin_user.last_sign_in_ip = admin_user.current_sign_in_ip
|
57
|
+
end
|
58
|
+
|
59
|
+
def record_sign_in!(admin_user)
|
60
|
+
update_last_sign_in(admin_user)
|
61
|
+
|
62
|
+
admin_user.current_sign_in_at = Time.current
|
63
|
+
admin_user.current_sign_in_ip = request.remote_ip
|
64
|
+
admin_user.sign_in_count = admin_user.sign_in_count + 1
|
65
|
+
|
66
|
+
admin_user.save!
|
67
|
+
end
|
68
|
+
|
69
|
+
def record_sign_out!(admin_user)
|
70
|
+
update_last_sign_in(admin_user)
|
71
|
+
|
72
|
+
admin_user.current_sign_in_at = nil
|
73
|
+
admin_user.current_sign_in_ip = nil
|
74
|
+
|
75
|
+
admin_user.save!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|