katalyst-koi 4.0.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 +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,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class UrlRewritesController < ApplicationController
|
|
5
|
+
before_action :set_url_rewrite, only: %i[show edit update destroy]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
collection = Collection.new.with_params(params).apply(UrlRewrite.strict_loading)
|
|
9
|
+
table = Koi::IndexTableComponent.new(collection:)
|
|
10
|
+
|
|
11
|
+
respond_to do |format|
|
|
12
|
+
format.turbo_stream { render(table) } if self_referred?
|
|
13
|
+
format.html { render :index, locals: { table:, collection: } }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
render :show, locals: { url_rewrite: @url_rewrite }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def new
|
|
22
|
+
@url_rewrite = UrlRewrite.new
|
|
23
|
+
render :new, locals: { url_rewrite: @url_rewrite }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def edit
|
|
27
|
+
render :edit, locals: { url_rewrite: @url_rewrite }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create
|
|
31
|
+
@url_rewrite = UrlRewrite.new(url_rewrite_params)
|
|
32
|
+
|
|
33
|
+
if @url_rewrite.save
|
|
34
|
+
redirect_to admin_url_rewrite_path(@url_rewrite)
|
|
35
|
+
else
|
|
36
|
+
render :new, status: :unprocessable_entity, locals: { url_rewrite: @url_rewrite }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update
|
|
41
|
+
@url_rewrite.attributes = url_rewrite_params
|
|
42
|
+
|
|
43
|
+
if @url_rewrite.save
|
|
44
|
+
redirect_to admin_url_rewrite_path(@url_rewrite)
|
|
45
|
+
else
|
|
46
|
+
render :edit, status: :unprocessable_entity, locals: { url_rewrite: @url_rewrite }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def destroy
|
|
51
|
+
@url_rewrite.destroy!
|
|
52
|
+
|
|
53
|
+
redirect_to admin_url_rewrites_path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def url_rewrite_params
|
|
59
|
+
params.require(:url_rewrite).permit(:from, :to, :active)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_url_rewrite
|
|
63
|
+
@url_rewrite = UrlRewrite.find(params[:id])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Collection < Katalyst::Tables::Collection::Base
|
|
67
|
+
attribute :search, :string, default: ""
|
|
68
|
+
attribute :scope, :string, default: "active"
|
|
69
|
+
|
|
70
|
+
config.sorting = "from"
|
|
71
|
+
config.paginate = true
|
|
72
|
+
|
|
73
|
+
def filter
|
|
74
|
+
self.items = items.admin_search(search) if search.present?
|
|
75
|
+
|
|
76
|
+
self.items = case scope&.to_sym
|
|
77
|
+
when :active
|
|
78
|
+
items.where(active: true)
|
|
79
|
+
when :inactive
|
|
80
|
+
items.where(active: false)
|
|
81
|
+
else
|
|
82
|
+
items
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module Controller
|
|
5
|
+
module HasAdminUsers
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
helper_method :admin_signed_in?
|
|
10
|
+
helper_method :current_admin_user
|
|
11
|
+
helper_method :current_admin
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def admin_signed_in?
|
|
15
|
+
current_admin_user.present?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_admin_user
|
|
19
|
+
@current_admin_user ||= Admin::User.find(session[:admin_user_id]) if session[:admin_user_id].present?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @deprecated Use current_admin_user instead
|
|
23
|
+
alias_method :current_admin, :current_admin_user
|
|
24
|
+
|
|
25
|
+
module Test
|
|
26
|
+
# Include in view specs to stub out the current admin user
|
|
27
|
+
module ViewHelper
|
|
28
|
+
extend ActiveSupport::Concern
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
before do
|
|
32
|
+
view.singleton_class.module_eval do
|
|
33
|
+
def admin_signed_in?
|
|
34
|
+
current_admin_user.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def current_admin_user
|
|
38
|
+
respond_to?(:admin_user) ? admin_user : nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :current_admin, :current_admin_user
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module Controller
|
|
5
|
+
module HasWebauthn
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
helper_method :webauthn_auth_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def webauthn_relying_party
|
|
13
|
+
@webauthn_relying_party ||=
|
|
14
|
+
WebAuthn::RelyingParty.new(
|
|
15
|
+
name: "Koi Admin",
|
|
16
|
+
origin: request.base_url,
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def webauthn_auth_options
|
|
21
|
+
options = webauthn_relying_party.options_for_authentication(
|
|
22
|
+
allow: Admin::Credential.pluck(:external_id),
|
|
23
|
+
)
|
|
24
|
+
session[:authentication_challenge] = options.challenge
|
|
25
|
+
|
|
26
|
+
options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def webauthn_authenticate!
|
|
30
|
+
return if session_params[:response].blank?
|
|
31
|
+
|
|
32
|
+
webauthn_credential, stored_credential = webauthn_relying_party.verify_authentication(
|
|
33
|
+
JSON.parse(session_params[:response]),
|
|
34
|
+
session[:authentication_challenge],
|
|
35
|
+
) do |credential|
|
|
36
|
+
Admin::Credential.find_by!(external_id: credential.id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
stored_credential.update!(sign_count: webauthn_credential.sign_count)
|
|
40
|
+
|
|
41
|
+
stored_credential.admin
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module Controller
|
|
5
|
+
module IsAdminController
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def authenticate_local_admins(value)
|
|
10
|
+
Koi::Controller::IsAdminController.authenticate_local_admins = value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
included do
|
|
15
|
+
include HasAdminUsers
|
|
16
|
+
include Katalyst::Tables::Backend
|
|
17
|
+
include Pagy::Backend
|
|
18
|
+
|
|
19
|
+
default_form_builder "Koi::FormBuilder"
|
|
20
|
+
|
|
21
|
+
helper Katalyst::GOVUK::Formbuilder::Frontend
|
|
22
|
+
helper Katalyst::Navigation::FrontendHelper
|
|
23
|
+
helper Katalyst::Tables::Frontend
|
|
24
|
+
helper Pagy::Frontend
|
|
25
|
+
helper IndexActionsHelper
|
|
26
|
+
helper :all
|
|
27
|
+
|
|
28
|
+
layout "koi/application"
|
|
29
|
+
|
|
30
|
+
before_action :authenticate_local_admin, if: -> { Koi::Controller::IsAdminController.authenticate_local_admins }
|
|
31
|
+
before_action :authenticate_admin, unless: :admin_signed_in?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
attr_accessor :authenticate_local_admins
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
def authenticate_local_admin
|
|
41
|
+
return if admin_signed_in? || !Rails.env.development?
|
|
42
|
+
|
|
43
|
+
session[:admin_user_id] =
|
|
44
|
+
Admin::User.where(email: %W[#{ENV['USER']}@katalyst.com.au admin@katalyst.com.au]).first&.id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def authenticate_admin
|
|
48
|
+
redirect_to new_admin_session_path, status: :temporary_redirect
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Katalyst
|
|
4
|
+
module Content
|
|
5
|
+
module Editor
|
|
6
|
+
class Errors < Base
|
|
7
|
+
def build(**options)
|
|
8
|
+
turbo_frame_tag dom_id(container, :errors) do
|
|
9
|
+
form_builder.govuk_error_summary(**options)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def form_builder
|
|
16
|
+
Koi::FormBuilder.new(container.model_name.param_key, container, self, {})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Katalyst
|
|
4
|
+
module Navigation
|
|
5
|
+
module Editor
|
|
6
|
+
class Errors < Base
|
|
7
|
+
def build(**options)
|
|
8
|
+
turbo_frame_tag dom_id(menu, :errors) do
|
|
9
|
+
form_builder.govuk_error_summary(**options)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def form_builder
|
|
16
|
+
Koi::FormBuilder.new(menu.model_name.param_key, menu, self, {})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Naming/MethodName
|
|
4
|
+
module Koi
|
|
5
|
+
module DateHelper
|
|
6
|
+
# @deprecated
|
|
7
|
+
def date_format(date, format)
|
|
8
|
+
date.strftime format.gsub(/yyyy/, "%Y")
|
|
9
|
+
.gsub(/yy/, "%y")
|
|
10
|
+
.gsub(/Month/, "%B")
|
|
11
|
+
.gsub(/M/, "%b")
|
|
12
|
+
.gsub(/mm/, "%m")
|
|
13
|
+
.gsub(/m/, "%-m")
|
|
14
|
+
.gsub(/Day/, "%A")
|
|
15
|
+
.gsub(/D/, "%a")
|
|
16
|
+
.gsub(/dd/, "%d")
|
|
17
|
+
.gsub(/d/, "%-d")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @deprecated
|
|
21
|
+
def date_Month_d_yyyy(date)
|
|
22
|
+
date.strftime "%B %-d, %Y"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @deprecated
|
|
26
|
+
def date_d_Month_yyyy(date)
|
|
27
|
+
date.strftime "%-d %B %Y"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @deprecated
|
|
31
|
+
def date_d_M_yy(date)
|
|
32
|
+
date.strftime "%-d %b %y"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
# rubocop:enable Naming/MethodName
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module DefinitionListHelper
|
|
5
|
+
def definition_list(**options, &)
|
|
6
|
+
DefinitionListBuilder.new(self, options).render(&)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class DefinitionListBuilder
|
|
11
|
+
delegate_missing_to :@context
|
|
12
|
+
|
|
13
|
+
def initialize(context, options = {})
|
|
14
|
+
@context = context
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(&block)
|
|
19
|
+
tag.dl class: @options.delete(:class) do
|
|
20
|
+
concat(capture { yield self }) if block
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def items_with(model:, attributes:, **options)
|
|
25
|
+
capture do
|
|
26
|
+
attributes.each do |attribute|
|
|
27
|
+
concat item(model, attribute, **options)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def item(object, attribute, **options, &)
|
|
33
|
+
Definition.new(@context, object, attribute, **@options, **options).render(&)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Definition
|
|
37
|
+
attr_reader :object, :attribute, :options
|
|
38
|
+
|
|
39
|
+
delegate_missing_to :@context
|
|
40
|
+
|
|
41
|
+
def initialize(context, object, attribute, options = {})
|
|
42
|
+
@context = context
|
|
43
|
+
@object = object
|
|
44
|
+
@attribute = attribute
|
|
45
|
+
@options = options
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render(&)
|
|
49
|
+
return unless render?
|
|
50
|
+
|
|
51
|
+
term_tag + definition_tag(&)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def render?
|
|
57
|
+
!(options.fetch(:skip_blank, true) && attribute_value.blank? && attribute_value != false)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def term_tag
|
|
61
|
+
tag.dt(label)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def definition_tag(&block)
|
|
65
|
+
if block
|
|
66
|
+
tag.dd { yield attribute_value }
|
|
67
|
+
else
|
|
68
|
+
case attribute_value
|
|
69
|
+
when Array
|
|
70
|
+
tag.dd(attribute_value.join(", "))
|
|
71
|
+
when ActiveStorage::Attached::One
|
|
72
|
+
tag.dd(attribute_value.attached? ? link_to(attribute_value.filename, url_for(attribute_value)) : "")
|
|
73
|
+
when Date, Time, DateTime
|
|
74
|
+
tag.dd(l(attribute_value, format: :display))
|
|
75
|
+
when TrueClass, FalseClass
|
|
76
|
+
tag.dd(attribute_value ? "Yes" : "No")
|
|
77
|
+
else
|
|
78
|
+
tag.dd(attribute_value.to_s)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def label
|
|
84
|
+
options.dig(:label, :text) || object.class.human_attribute_name(attribute)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def attribute_value
|
|
88
|
+
@attribute_value ||= object.public_send(attribute)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module IndexActionsHelper
|
|
5
|
+
def koi_index_actions(search: false, create: false, &block)
|
|
6
|
+
IndexActionsBuilder.new(self, search:, create:).render(&block)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class IndexActionsBuilder
|
|
11
|
+
delegate_missing_to :@context
|
|
12
|
+
|
|
13
|
+
def initialize(context, search:, create:)
|
|
14
|
+
@context = context
|
|
15
|
+
@search = search
|
|
16
|
+
@create = create
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def search?
|
|
20
|
+
!!@search
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create?
|
|
24
|
+
!!@create
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(&)
|
|
28
|
+
tag.nav class: "index-actions", data: { controller: "index-actions", action: actions } do
|
|
29
|
+
form_with(**search_options,
|
|
30
|
+
data: { controller: "search",
|
|
31
|
+
turbo_stream: "",
|
|
32
|
+
action: <<~ACTIONS,
|
|
33
|
+
input->index-actions#update
|
|
34
|
+
change->index-actions#update
|
|
35
|
+
submit->index-actions#submit
|
|
36
|
+
ACTIONS
|
|
37
|
+
}) do |form|
|
|
38
|
+
concat(links(form, &))
|
|
39
|
+
concat(sort_input(form))
|
|
40
|
+
concat(search(form)) if create? || search?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def search(form)
|
|
46
|
+
tag.div class: "actions-group" do
|
|
47
|
+
concat(search_button(form)) if search?
|
|
48
|
+
concat(create_button(form)) if create?
|
|
49
|
+
concat(search_input(form)) if search?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort_input(form)
|
|
54
|
+
form.hidden_field(:sort, value: params[:sort], data: { index_actions_target: "sort" })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Hidden button to trigger search, avoids triggering create instead.
|
|
58
|
+
def search_button(form)
|
|
59
|
+
form.submit("Search", hidden: "")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_button(_form)
|
|
63
|
+
tag.button(t("koi.labels.new", default: "New"),
|
|
64
|
+
type: :submit,
|
|
65
|
+
formaction: @create == true ? url_for(action: :new) : new_polymorphic_path(@create),
|
|
66
|
+
class: "button--primary",
|
|
67
|
+
data: { index_actions_target: "create" })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def search_input(form)
|
|
71
|
+
form.search_field(:search,
|
|
72
|
+
placeholder: t("koi.labels.search", default: "Search"),
|
|
73
|
+
value: params[:search],
|
|
74
|
+
data: { index_actions_target: "search" })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def links(form, &)
|
|
78
|
+
tag.div(class: "actions-group") do
|
|
79
|
+
yield(form) if block_given?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def actions
|
|
84
|
+
[
|
|
85
|
+
("shortcut:cancel@document->index-actions#clear" if search?),
|
|
86
|
+
("shortcut:create@document->index-actions#create" if create?),
|
|
87
|
+
("shortcut:search@document->index-actions#search" if search?),
|
|
88
|
+
"shortcut:page-prev@document->index-actions#prevPage",
|
|
89
|
+
"shortcut:page-next@document->index-actions#nextPage",
|
|
90
|
+
].compact.join(" ")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def search_options
|
|
94
|
+
options = { url: request.path, method: :get, scope: "" }
|
|
95
|
+
options.merge!(@search) if @search.is_a?(Hash)
|
|
96
|
+
options
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class Credential < ApplicationRecord
|
|
5
|
+
self.table_name = :admin_credentials
|
|
6
|
+
|
|
7
|
+
belongs_to :admin, class_name: "Admin::User", inverse_of: :credentials
|
|
8
|
+
|
|
9
|
+
validates :external_id, :public_key, :nickname, :sign_count, presence: true
|
|
10
|
+
validates :external_id, uniqueness: true
|
|
11
|
+
validates :sign_count,
|
|
12
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class User < ApplicationRecord
|
|
5
|
+
include Koi::Model::Archivable
|
|
6
|
+
|
|
7
|
+
def self.model_name
|
|
8
|
+
ActiveModel::Name.new(self, nil, "Admin")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
has_secure_password :password
|
|
12
|
+
|
|
13
|
+
has_many :credentials, inverse_of: :admin, class_name: "Admin::Credential", dependent: :destroy
|
|
14
|
+
|
|
15
|
+
validates :name, :email, presence: true
|
|
16
|
+
validates :email, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
17
|
+
|
|
18
|
+
scope :alphabetical, -> { order(name: :asc) }
|
|
19
|
+
|
|
20
|
+
if "PgSearch::Model".safe_constantize
|
|
21
|
+
include PgSearch::Model
|
|
22
|
+
|
|
23
|
+
pg_search_scope :admin_search, against: %i[email name], using: { tsearch: { prefix: true } }
|
|
24
|
+
else
|
|
25
|
+
scope :admin_search, ->(query) do
|
|
26
|
+
where("email LIKE :query OR name LIKE :query", query: "%#{query}%")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# TODO(sfn) remove once Rails 7.1 is released
|
|
31
|
+
# https://edgeapi.rubyonrails.org/classes/ActiveRecord/SecurePassword/ClassMethods.html#method-i-authenticate_by
|
|
32
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
33
|
+
def self.authenticate_by(attributes)
|
|
34
|
+
passwords, identifiers = attributes.to_h.partition do |name, _value|
|
|
35
|
+
!has_attribute?(name) && has_attribute?("#{name}_digest")
|
|
36
|
+
end.map(&:to_h)
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, "One or more password arguments are required" if passwords.empty?
|
|
39
|
+
raise ArgumentError, "One or more finder arguments are required" if identifiers.empty?
|
|
40
|
+
|
|
41
|
+
if (record = find_by(identifiers))
|
|
42
|
+
record if passwords.count { |name, value| record.public_send(:"authenticate_#{name}", value) } == passwords.size
|
|
43
|
+
else
|
|
44
|
+
new(passwords)
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Koi
|
|
4
|
+
module Model
|
|
5
|
+
module Archivable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
scope :not_archived, -> { where(archived_at: nil) }
|
|
10
|
+
scope :archived, -> { unscope(where: :archived_at).where.not(archived_at: nil) }
|
|
11
|
+
scope :with_archived, -> { unscope(where: :archived_at) }
|
|
12
|
+
|
|
13
|
+
default_scope { not_archived }
|
|
14
|
+
|
|
15
|
+
alias_method :archived?, :archived
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns true iff the record has been archived.
|
|
19
|
+
def archived
|
|
20
|
+
archived_at.present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Update archived status based on given boolean value.
|
|
24
|
+
def archived=(archived)
|
|
25
|
+
if ActiveRecord::Type::Boolean.new.cast(archived)
|
|
26
|
+
archive
|
|
27
|
+
else
|
|
28
|
+
restore
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Mark a record as archived. It will no longer appear in default queries.
|
|
33
|
+
def archive
|
|
34
|
+
self.archived_at = Time.current
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Archive a record immediately, without validation.
|
|
38
|
+
def archive!
|
|
39
|
+
archive
|
|
40
|
+
save!(validate: false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mark a record as no longer archived. It will appear in default queries.
|
|
44
|
+
def restore
|
|
45
|
+
self.archived_at = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Restore a record immediately, without validation.
|
|
49
|
+
def restore!
|
|
50
|
+
restore
|
|
51
|
+
save!(validate: false)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UrlRewrite < ApplicationRecord
|
|
4
|
+
validates :from, :to, presence: true
|
|
5
|
+
validates :from, format: { with: %r{\A/.*\z}, message: "should start with /" }
|
|
6
|
+
|
|
7
|
+
scope :active, -> { where(active: true) }
|
|
8
|
+
scope :alphabetical, -> { order(from: :asc) }
|
|
9
|
+
|
|
10
|
+
def to_s
|
|
11
|
+
"#{from} -> #{to}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
scope :admin_search, ->(query) do
|
|
15
|
+
where(arel_table[:from].matches("%#{query}%").or(arel_table[:to].matches("%#{query}%")))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.redirect_path_for(path)
|
|
19
|
+
active.find_by(from: path).try(:to)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def title
|
|
23
|
+
from.delete_prefix("/")
|
|
24
|
+
end
|
|
25
|
+
end
|