katalyst-koi 4.15.1 → 4.17.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/koi/admin.css +1 -1
- data/app/assets/stylesheets/koi/components/_query.scss +4 -3
- data/app/assets/stylesheets/koi/layouts/_header.scss +18 -20
- data/app/assets/stylesheets/koi/pages/_login.scss +1 -1
- data/app/assets/stylesheets/koi/themes/_govuk.scss +37 -33
- data/app/assets/stylesheets/koi/utils/_typography.scss +20 -2
- data/app/controllers/admin/admin_users_controller.rb +1 -1
- data/app/controllers/admin/credentials_controller.rb +1 -1
- data/app/controllers/admin/otps_controller.rb +1 -1
- data/app/controllers/admin/sessions_controller.rb +2 -29
- data/app/controllers/admin/tokens_controller.rb +18 -44
- data/app/controllers/admin/url_rewrites_controller.rb +1 -1
- data/app/controllers/admin/well_knowns_controller.rb +71 -0
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +6 -5
- data/app/controllers/concerns/koi/controller/records_authentication.rb +35 -0
- data/app/controllers/well_knowns_controller.rb +15 -0
- data/app/models/admin/user.rb +2 -0
- data/app/models/well_known.rb +25 -0
- data/app/views/admin/sessions/password.html.erb +3 -0
- data/app/views/admin/tokens/create.turbo_stream.erb +2 -0
- data/app/views/admin/tokens/show.html.erb +3 -1
- data/app/views/admin/well_knowns/_fields.html.erb +6 -0
- data/app/views/admin/well_knowns/edit.html.erb +12 -0
- data/app/views/admin/well_knowns/index.html.erb +15 -0
- data/app/views/admin/well_knowns/new.html.erb +11 -0
- data/app/views/admin/well_knowns/show.html.erb +19 -0
- data/config/routes.rb +4 -1
- data/db/migrate/20250204060748_create_well_knowns.rb +14 -0
- data/lib/generators/koi/admin_controller/templates/controller.rb.tt +2 -2
- data/lib/generators/koi/admin_route/admin_route_generator.rb +2 -1
- data/lib/generators/koi/admin_route/templates/initializer.rb.tt +1 -0
- data/spec/factories/well_knowns.rb +10 -0
- metadata +16 -10
- data/app/controllers/concerns/koi/controller/json_web_token.rb +0 -22
@@ -1,3 +1,4 @@
|
|
1
|
+
@use "sass:meta";
|
1
2
|
@use "sass:string";
|
2
3
|
@use "../utils/typography";
|
3
4
|
|
@@ -77,7 +78,7 @@ $text-icon-color: #888;
|
|
77
78
|
}
|
78
79
|
|
79
80
|
li.suggestion.attribute::before {
|
80
|
-
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 14 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13 3.1C13 4.2598 10.3137 5.2 7 5.2C3.68629 5.2 1 4.2598 1 3.1M13 3.1C13 1.9402 10.3137 1 7 1C3.68629 1 1 1.9402 1 3.1M13 3.1V12.9C13 14.062 10.3333 15 7 15C3.66667 15 1 14.062 1 12.9V3.1M13 8C13 9.162 10.3333 10.1 7 10.1C3.66667 10.1 1 9.162 1 8' stroke='%23#{string.slice(inspect($text-icon-color), 2)}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
81
|
+
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 14 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13 3.1C13 4.2598 10.3137 5.2 7 5.2C3.68629 5.2 1 4.2598 1 3.1M13 3.1C13 1.9402 10.3137 1 7 1C3.68629 1 1 1.9402 1 3.1M13 3.1V12.9C13 14.062 10.3333 15 7 15C3.66667 15 1 14.062 1 12.9V3.1M13 8C13 9.162 10.3333 10.1 7 10.1C3.66667 10.1 1 9.162 1 8' stroke='%23#{string.slice(meta.inspect($text-icon-color), 2)}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
81
82
|
no-repeat;
|
82
83
|
top: 2px;
|
83
84
|
}
|
@@ -85,13 +86,13 @@ $text-icon-color: #888;
|
|
85
86
|
li.suggestion.database_value::before,
|
86
87
|
li.suggestion.constant_value::before,
|
87
88
|
li.suggestion.custom_value::before {
|
88
|
-
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 16 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.6111 5.16702L7.99998 8.99998M7.99998 8.99998L1.38886 5.16702M7.99998 8.99998L8 16.711M15 12.2943V5.70573C15 5.42761 15 5.28856 14.9607 5.16453C14.926 5.05481 14.8692 4.9541 14.7942 4.86912C14.7094 4.77307 14.5929 4.70553 14.3599 4.57047L8.60436 1.23354C8.38378 1.10565 8.27348 1.04171 8.15668 1.01664C8.05331 0.994453 7.94669 0.994453 7.84332 1.01664C7.72652 1.04171 7.61623 1.10565 7.39564 1.23354L1.64009 4.57047C1.40713 4.70554 1.29064 4.77307 1.20583 4.86912C1.13079 4.9541 1.074 5.05481 1.03927 5.16453C1 5.28856 1 5.42762 1 5.70573V12.2943C1 12.5724 1 12.7114 1.03927 12.8355C1.074 12.9452 1.13079 13.0459 1.20583 13.1309C1.29064 13.2269 1.40713 13.2945 1.64009 13.4295L7.39564 16.7665C7.61623 16.8943 7.72652 16.9583 7.84332 16.9834C7.94669 17.0055 8.05331 17.0055 8.15668 16.9834C8.27348 16.9583 8.38377 16.8943 8.60436 16.7665L14.3599 13.4295C14.5929 13.2945 14.7094 13.2269 14.7942 13.1309C14.8692 13.0459 14.926 12.9452 14.9607 12.8355C15 12.7114 15 12.5724 15 12.2943Z' stroke='%23#{string.slice(inspect($text-icon-color), 2)}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
89
|
+
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 16 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.6111 5.16702L7.99998 8.99998M7.99998 8.99998L1.38886 5.16702M7.99998 8.99998L8 16.711M15 12.2943V5.70573C15 5.42761 15 5.28856 14.9607 5.16453C14.926 5.05481 14.8692 4.9541 14.7942 4.86912C14.7094 4.77307 14.5929 4.70553 14.3599 4.57047L8.60436 1.23354C8.38378 1.10565 8.27348 1.04171 8.15668 1.01664C8.05331 0.994453 7.94669 0.994453 7.84332 1.01664C7.72652 1.04171 7.61623 1.10565 7.39564 1.23354L1.64009 4.57047C1.40713 4.70554 1.29064 4.77307 1.20583 4.86912C1.13079 4.9541 1.074 5.05481 1.03927 5.16453C1 5.28856 1 5.42762 1 5.70573V12.2943C1 12.5724 1 12.7114 1.03927 12.8355C1.074 12.9452 1.13079 13.0459 1.20583 13.1309C1.29064 13.2269 1.40713 13.2945 1.64009 13.4295L7.39564 16.7665C7.61623 16.8943 7.72652 16.9583 7.84332 16.9834C7.94669 17.0055 8.05331 17.0055 8.15668 16.9834C8.27348 16.9583 8.38377 16.8943 8.60436 16.7665L14.3599 13.4295C14.5929 13.2945 14.7094 13.2269 14.7942 13.1309C14.8692 13.0459 14.926 12.9452 14.9607 12.8355C15 12.7114 15 12.5724 15 12.2943Z' stroke='%23#{string.slice(meta.inspect($text-icon-color), 2)}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
89
90
|
no-repeat;
|
90
91
|
top: 2px;
|
91
92
|
}
|
92
93
|
|
93
94
|
li.suggestion.search_value::before {
|
94
|
-
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 21L15.0001 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z' stroke='%23#{string.slice(inspect($text-icon-color), 2)}' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
95
|
+
background: url("data:image/svg+xml,%3Csvg width='14' height='16' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 21L15.0001 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z' stroke='%23#{string.slice(meta.inspect($text-icon-color), 2)}' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A")
|
95
96
|
no-repeat;
|
96
97
|
top: 2px;
|
97
98
|
}
|
@@ -1,30 +1,34 @@
|
|
1
|
-
|
1
|
+
$inset: 2rem !default;
|
2
|
+
|
3
|
+
$title: 4rem;
|
4
|
+
$breadcrumbs: 3rem;
|
5
|
+
$actions: 3rem;
|
6
|
+
$height: $title + $breadcrumbs + $actions;
|
7
|
+
|
8
|
+
@mixin list($separator, $margin: 1em, $padding: 0.5em) {
|
2
9
|
display: flex;
|
10
|
+
align-items: center;
|
3
11
|
list-style-position: outside;
|
12
|
+
margin-inline: $inset;
|
4
13
|
|
5
14
|
> * {
|
6
15
|
display: list-item;
|
7
|
-
margin-
|
16
|
+
margin-inline-start: $margin;
|
17
|
+
padding-inline-start: $padding;
|
8
18
|
}
|
9
19
|
|
10
20
|
> *::marker {
|
11
|
-
content:
|
21
|
+
content: $separator;
|
12
22
|
color: rgba(0, 0, 0, 0.4);
|
13
23
|
}
|
14
24
|
|
15
25
|
> *:first-child {
|
16
26
|
display: block;
|
17
|
-
margin-
|
27
|
+
margin-inline-start: 0;
|
28
|
+
padding-inline-start: 0;
|
18
29
|
}
|
19
30
|
}
|
20
31
|
|
21
|
-
$inset: 2rem !default;
|
22
|
-
|
23
|
-
$title: 4rem;
|
24
|
-
$breadcrumbs: 3rem;
|
25
|
-
$actions: 3rem;
|
26
|
-
$height: $title + $breadcrumbs + $actions;
|
27
|
-
|
28
32
|
%header {
|
29
33
|
border-bottom: 1px solid #ececec;
|
30
34
|
color: #b2b2b2;
|
@@ -44,20 +48,14 @@ $height: $title + $breadcrumbs + $actions;
|
|
44
48
|
|
45
49
|
.breadcrumbs {
|
46
50
|
grid-area: breadcrumbs;
|
47
|
-
display: flex;
|
48
|
-
align-items: center;
|
49
|
-
margin: 0 $inset;
|
50
51
|
|
51
|
-
@include list("›
|
52
|
+
@include list("›");
|
52
53
|
}
|
53
54
|
|
54
55
|
.actions {
|
55
56
|
grid-area: actions;
|
56
|
-
|
57
|
-
align-items: center;
|
58
|
-
margin: 0 $inset;
|
57
|
+
gap: 0;
|
59
58
|
|
60
|
-
@include list("|", 0.
|
61
|
-
list-style-type: square;
|
59
|
+
@include list("|", $margin: 0.8em);
|
62
60
|
}
|
63
61
|
}
|
@@ -1,42 +1,24 @@
|
|
1
1
|
@use "../utils/typography" as *;
|
2
2
|
|
3
|
-
@
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
),
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
),
|
13
|
-
|
14
|
-
|
15
|
-
|
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),
|
3
|
+
@use "katalyst/govuk/formbuilder" with (
|
4
|
+
$govuk-font-family: "Inter",
|
5
|
+
$govuk-text-colour: #{var(--site-text-color)},
|
6
|
+
$govuk-typography-scale: (
|
7
|
+
80: govuk-definition(h1),
|
8
|
+
48: govuk-definition(h2),
|
9
|
+
36: govuk-definition(h3),
|
10
|
+
27: govuk-definition(h4),
|
11
|
+
24: govuk-definition(h5),
|
12
|
+
19: govuk-definition(h6),
|
13
|
+
16: govuk-definition(paragraph),
|
14
|
+
14: govuk-definition(small),
|
15
|
+
),
|
16
|
+
$govuk-input-border-colour: #{var(--site-text-color)}
|
31
17
|
);
|
32
18
|
|
33
|
-
$govuk-input-border-colour: #{var(--site-text-color)};
|
34
|
-
|
35
|
-
@import "katalyst/govuk/formbuilder";
|
36
|
-
|
37
19
|
.govuk-input,
|
38
20
|
.govuk-textarea {
|
39
|
-
color:
|
21
|
+
color: var(--site-text-color);
|
40
22
|
}
|
41
23
|
|
42
24
|
.govuk-hint {
|
@@ -50,3 +32,25 @@ $govuk-input-border-colour: #{var(--site-text-color)};
|
|
50
32
|
margin-left: -18px;
|
51
33
|
padding-left: 13px;
|
52
34
|
}
|
35
|
+
|
36
|
+
// add some koi styling to the govuk show/hide password button
|
37
|
+
[data-govuk-password-input-init] {
|
38
|
+
max-width: var(--text-width);
|
39
|
+
|
40
|
+
.govuk-password-input__wrapper {
|
41
|
+
gap: 0.5rem;
|
42
|
+
}
|
43
|
+
|
44
|
+
.govuk-button {
|
45
|
+
min-width: 4em;
|
46
|
+
background: none;
|
47
|
+
cursor: pointer;
|
48
|
+
border: 2px solid var(--site-text-color);
|
49
|
+
}
|
50
|
+
|
51
|
+
.govuk-button:focus-visible,
|
52
|
+
.govuk-button:hover {
|
53
|
+
color: var(--site-primary);
|
54
|
+
border-color: var(--site-primary);
|
55
|
+
}
|
56
|
+
}
|
@@ -1,10 +1,11 @@
|
|
1
|
+
@use "sass:map";
|
1
2
|
@use "sass:math";
|
2
3
|
|
3
4
|
$-font-sizes: () !default;
|
4
5
|
$-line-heights: () !default;
|
5
6
|
|
6
7
|
@function font-size($size, $breakpoint: null, $unit: rem) {
|
7
|
-
$font-size: map
|
8
|
+
$font-size: map.get($-font-sizes, $size);
|
8
9
|
|
9
10
|
@if $unit == rem {
|
10
11
|
@return $font-size;
|
@@ -14,7 +15,7 @@ $-line-heights: () !default;
|
|
14
15
|
}
|
15
16
|
|
16
17
|
@function line-height($size, $breakpoint: null, $unit: em) {
|
17
|
-
$line-height: map
|
18
|
+
$line-height: map.get($-line-heights, $size);
|
18
19
|
|
19
20
|
@if $unit == em {
|
20
21
|
@return $line-height;
|
@@ -22,3 +23,20 @@ $-line-heights: () !default;
|
|
22
23
|
@return math.div($line-height, 1em) * 16px;
|
23
24
|
}
|
24
25
|
}
|
26
|
+
|
27
|
+
@function govuk-definition($size) {
|
28
|
+
@return (
|
29
|
+
null: (
|
30
|
+
font-size: font-size($size, mobile, px),
|
31
|
+
line-height: line-height($size, mobile, px),
|
32
|
+
),
|
33
|
+
tablet: (
|
34
|
+
font-size: font-size($size, tablet, px),
|
35
|
+
line-height: line-height($size, tablet, px),
|
36
|
+
),
|
37
|
+
print: (
|
38
|
+
font-size: font-size($size, print, px),
|
39
|
+
line-height: line-height($size, print, px),
|
40
|
+
)
|
41
|
+
);
|
42
|
+
}
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Admin
|
4
4
|
class SessionsController < ApplicationController
|
5
5
|
include Koi::Controller::HasWebauthn
|
6
|
+
include Koi::Controller::RecordsAuthentication
|
6
7
|
|
7
8
|
before_action :redirect_authenticated, only: %i[new], if: :admin_signed_in?
|
8
9
|
before_action :authenticate_local_admin, only: %i[new], if: :authenticate_local_admins?
|
@@ -102,35 +103,7 @@ module Admin
|
|
102
103
|
end
|
103
104
|
|
104
105
|
def session_params
|
105
|
-
params.
|
106
|
-
end
|
107
|
-
|
108
|
-
def update_last_sign_in(admin_user)
|
109
|
-
return if admin_user.current_sign_in_at.blank?
|
110
|
-
|
111
|
-
admin_user.last_sign_in_at = admin_user.current_sign_in_at
|
112
|
-
admin_user.last_sign_in_ip = admin_user.current_sign_in_ip
|
113
|
-
end
|
114
|
-
|
115
|
-
def record_sign_in!(admin_user)
|
116
|
-
update_last_sign_in(admin_user)
|
117
|
-
|
118
|
-
admin_user.current_sign_in_at = Time.current
|
119
|
-
admin_user.current_sign_in_ip = request.remote_ip
|
120
|
-
admin_user.sign_in_count = admin_user.sign_in_count + 1
|
121
|
-
|
122
|
-
admin_user.save!
|
123
|
-
end
|
124
|
-
|
125
|
-
def record_sign_out!(admin_user)
|
126
|
-
return unless admin_user
|
127
|
-
|
128
|
-
update_last_sign_in(admin_user)
|
129
|
-
|
130
|
-
admin_user.current_sign_in_at = nil
|
131
|
-
admin_user.current_sign_in_ip = nil
|
132
|
-
|
133
|
-
admin_user.save!
|
106
|
+
params.expect(admin: %i[email password token response])
|
134
107
|
end
|
135
108
|
end
|
136
109
|
end
|
@@ -2,66 +2,40 @@
|
|
2
2
|
|
3
3
|
module Admin
|
4
4
|
class TokensController < ApplicationController
|
5
|
-
include Koi::Controller::
|
5
|
+
include Koi::Controller::RecordsAuthentication
|
6
6
|
|
7
|
-
before_action :
|
8
|
-
|
9
|
-
|
7
|
+
before_action :set_admin_user, only: %i[create]
|
8
|
+
|
9
|
+
attr_reader :admin_user
|
10
10
|
|
11
11
|
def show
|
12
|
-
|
12
|
+
if (@admin_user = Admin::User.find_by_token_for(:password_reset, params[:token]))
|
13
|
+
render locals: { admin_user:, token: params[:token] }, layout: "koi/login"
|
14
|
+
else
|
15
|
+
redirect_to(new_admin_session_path, status: :see_other, notice: I18n.t("koi.auth.token_invalid"))
|
16
|
+
end
|
13
17
|
end
|
14
18
|
|
15
19
|
def create
|
16
|
-
|
17
|
-
|
18
|
-
render locals: { token: }
|
20
|
+
render locals: { token: admin_user.generate_token_for(:password_reset) }
|
19
21
|
end
|
20
22
|
|
21
23
|
def update
|
22
|
-
|
23
|
-
|
24
|
-
redirect_to admin_admin_user_path(@admin), status: :see_other, notice: t("koi.auth.token_consumed")
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
24
|
+
if (@admin_user = Admin::User.find_by_token_for(:password_reset, params[:token]))
|
25
|
+
record_sign_in!(admin_user)
|
28
26
|
|
29
|
-
|
30
|
-
@admin = Admin::User.find(params[:admin_user_id])
|
31
|
-
end
|
32
|
-
|
33
|
-
def set_token
|
34
|
-
@token = decode_token(params[:token])
|
35
|
-
|
36
|
-
# constant time token validation requires that we always try to retrieve a user
|
37
|
-
@admin = Admin::User.find_by(id: @token&.fetch(:admin_id) || "")
|
38
|
-
end
|
27
|
+
session[:admin_user_id] = admin_user.id
|
39
28
|
|
40
|
-
|
41
|
-
return false unless @token.present? && @admin.present?
|
42
|
-
|
43
|
-
# Ensure that the user has not signed in since the token was generated
|
44
|
-
if @admin.current_sign_in_at.present?
|
45
|
-
@admin.current_sign_in_at.to_i < @token[:iat]
|
46
|
-
elsif @admin.last_sign_in_at.present?
|
47
|
-
@admin.last_sign_in_at.to_i < @token[:iat]
|
29
|
+
redirect_to admin_admin_user_path(admin_user), status: :see_other, notice: t("koi.auth.token_consumed")
|
48
30
|
else
|
49
|
-
|
31
|
+
redirect_to(new_admin_session_path, status: :see_other, notice: I18n.t("koi.auth.token_invalid"))
|
50
32
|
end
|
51
33
|
end
|
52
34
|
|
53
|
-
|
54
|
-
redirect_to(new_admin_session_path, status: :see_other, notice: I18n.t("koi.auth.token_invalid"))
|
55
|
-
end
|
56
|
-
|
57
|
-
def sign_in_admin(admin)
|
58
|
-
admin.current_sign_in_at = Time.current
|
59
|
-
admin.current_sign_in_ip = request.remote_ip
|
60
|
-
admin.sign_in_count = 1
|
35
|
+
private
|
61
36
|
|
62
|
-
|
63
|
-
|
64
|
-
session[:admin_user_id] = admin.id
|
37
|
+
def set_admin_user
|
38
|
+
@admin_user = Admin::User.find(params[:admin_user_id])
|
65
39
|
end
|
66
40
|
end
|
67
41
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class WellKnownsController < ApplicationController
|
5
|
+
before_action :set_well_known, only: %i[show edit update destroy]
|
6
|
+
|
7
|
+
def index
|
8
|
+
collection = Collection.new.with_params(params).apply(::WellKnown.strict_loading.all)
|
9
|
+
|
10
|
+
render locals: { collection: }
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
render locals: { well_known: @well_known }
|
15
|
+
end
|
16
|
+
|
17
|
+
def new
|
18
|
+
render locals: { well_known: ::WellKnown.new }
|
19
|
+
end
|
20
|
+
|
21
|
+
def edit
|
22
|
+
render locals: { well_known: @well_known }
|
23
|
+
end
|
24
|
+
|
25
|
+
def create
|
26
|
+
@well_known = ::WellKnown.new(well_known_params)
|
27
|
+
|
28
|
+
if @well_known.save
|
29
|
+
redirect_to [:admin, @well_known], status: :see_other
|
30
|
+
else
|
31
|
+
render :new, locals: { well_known: @well_known }, status: :unprocessable_content
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def update
|
36
|
+
if @well_known.update(well_known_params)
|
37
|
+
redirect_to action: :show, status: :see_other
|
38
|
+
else
|
39
|
+
render :edit, locals: { well_known: @well_known }, status: :unprocessable_content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy
|
44
|
+
@well_known.destroy!
|
45
|
+
|
46
|
+
redirect_to action: :index, status: :see_other
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Only allow a list of trusted parameters through.
|
52
|
+
def well_known_params
|
53
|
+
params.expect(well_known: %i[name purpose content_type content])
|
54
|
+
end
|
55
|
+
|
56
|
+
# Use callbacks to share common setup or constraints between actions.
|
57
|
+
def set_well_known
|
58
|
+
@well_known = ::WellKnown.find(params[:id])
|
59
|
+
end
|
60
|
+
|
61
|
+
class Collection < Admin::Collection
|
62
|
+
config.sorting = :name
|
63
|
+
config.paginate = true
|
64
|
+
|
65
|
+
attribute :name, :string
|
66
|
+
attribute :purpose, :string
|
67
|
+
attribute :content_type, :string
|
68
|
+
attribute :content, :string
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -12,15 +12,14 @@ module Koi
|
|
12
12
|
def webauthn_relying_party
|
13
13
|
@webauthn_relying_party ||=
|
14
14
|
WebAuthn::RelyingParty.new(
|
15
|
-
name:
|
16
|
-
|
15
|
+
name: Koi.config.admin_name,
|
16
|
+
allowed_origins: [request.base_url],
|
17
17
|
)
|
18
18
|
end
|
19
19
|
|
20
20
|
def webauthn_auth_options
|
21
|
-
options = webauthn_relying_party.options_for_authentication
|
22
|
-
|
23
|
-
)
|
21
|
+
options = webauthn_relying_party.options_for_authentication
|
22
|
+
|
24
23
|
session[:authentication_challenge] = options.challenge
|
25
24
|
|
26
25
|
options
|
@@ -42,6 +41,8 @@ module Koi
|
|
42
41
|
)
|
43
42
|
|
44
43
|
stored_credential.admin
|
44
|
+
rescue ActiveRecord::RecordNotFound
|
45
|
+
false
|
45
46
|
end
|
46
47
|
end
|
47
48
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Controller
|
5
|
+
module RecordsAuthentication
|
6
|
+
def update_last_sign_in(admin_user)
|
7
|
+
return if admin_user.current_sign_in_at.blank?
|
8
|
+
|
9
|
+
admin_user.last_sign_in_at = admin_user.current_sign_in_at
|
10
|
+
admin_user.last_sign_in_ip = admin_user.current_sign_in_ip
|
11
|
+
end
|
12
|
+
|
13
|
+
def record_sign_in!(admin_user)
|
14
|
+
update_last_sign_in(admin_user)
|
15
|
+
|
16
|
+
admin_user.current_sign_in_at = Time.current
|
17
|
+
admin_user.current_sign_in_ip = request.remote_ip
|
18
|
+
admin_user.sign_in_count += 1
|
19
|
+
|
20
|
+
admin_user.save!
|
21
|
+
end
|
22
|
+
|
23
|
+
def record_sign_out!(admin_user)
|
24
|
+
return unless admin_user
|
25
|
+
|
26
|
+
update_last_sign_in(admin_user)
|
27
|
+
|
28
|
+
admin_user.current_sign_in_at = nil
|
29
|
+
admin_user.current_sign_in_ip = nil
|
30
|
+
|
31
|
+
admin_user.save!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class WellKnownsController < ApplicationController
|
4
|
+
before_action :set_well_known
|
5
|
+
|
6
|
+
def show
|
7
|
+
render renderable: @well_known
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def set_well_known
|
13
|
+
@well_known = WellKnown.find_by!(name: params[:name])
|
14
|
+
end
|
15
|
+
end
|
data/app/models/admin/user.rb
CHANGED
@@ -12,6 +12,8 @@ module Admin
|
|
12
12
|
# disable validations for password_digest
|
13
13
|
has_secure_password validations: false
|
14
14
|
|
15
|
+
generates_token_for(:password_reset, expires_in: 30.minutes) { current_sign_in_at }
|
16
|
+
|
15
17
|
has_many :credentials, inverse_of: :admin, class_name: "Admin::Credential", dependent: :destroy
|
16
18
|
|
17
19
|
validates :name, :email, presence: true
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class WellKnown < ApplicationRecord
|
4
|
+
CONTENT_TYPES = {
|
5
|
+
text: "text/plain",
|
6
|
+
json: "application/json",
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
enum :content_type, CONTENT_TYPES
|
10
|
+
|
11
|
+
validates :name, :purpose, :content_type, presence: true
|
12
|
+
validates :name, uniqueness: { case_sensitive: true }
|
13
|
+
|
14
|
+
scope :admin_search, ->(query) do
|
15
|
+
where(arel_table[:name].matches("%#{query}%"))
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_in(view_context)
|
19
|
+
view_context.render(plain: content)
|
20
|
+
end
|
21
|
+
|
22
|
+
def format
|
23
|
+
content_type.to_sym
|
24
|
+
end
|
25
|
+
end
|
@@ -8,4 +8,7 @@
|
|
8
8
|
<%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %>
|
9
9
|
<%= hidden_field_tag(:redirect, params[:redirect]) %>
|
10
10
|
<%= form.admin_save "Next" %>
|
11
|
+
|
12
|
+
<%# init govuk js to provide the show/hide button %>
|
13
|
+
<%= govuk_formbuilder_init %>
|
11
14
|
<% end %>
|
@@ -1,3 +1,5 @@
|
|
1
|
+
<%# locals: (token:) %>
|
2
|
+
|
1
3
|
<%= turbo_stream.replace "invite" do %>
|
2
4
|
<div class="action copy-to-clipboard govuk-input__wrapper" data-controller="clipboard" data-clipboard-supported-class="clipboard--supported">
|
3
5
|
<%= text_field_tag :invite_link, admin_session_token_url(token), readonly: true, data: { clipboard_target: "source" } %>
|
@@ -1,8 +1,10 @@
|
|
1
|
+
<%# locals: (admin_user:, token:) %>
|
2
|
+
|
1
3
|
<%= render "layouts/koi/navigation_header" %>
|
2
4
|
|
3
5
|
<%= form_with(url: admin_session_token_path(token), method: :patch) do |form| %>
|
4
6
|
<p>Welcome to Koi Admin</p>
|
5
|
-
<%= render Koi::SummaryListComponent.new(model:
|
7
|
+
<%= render Koi::SummaryListComponent.new(model: admin_user, class: "item-table") do |builder| %>
|
6
8
|
<%= builder.text :name %>
|
7
9
|
<%= builder.text :email %>
|
8
10
|
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<% content_for :header do %>
|
2
|
+
<%= render(Koi::Header::EditComponent.new(resource: well_known)) %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<%= form_with(model: well_known, url: admin_well_known_path(well_known)) do |form| %>
|
6
|
+
<%= render "fields", form: %>
|
7
|
+
|
8
|
+
<div class="actions">
|
9
|
+
<%= form.admin_save %>
|
10
|
+
<%= form.admin_delete %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<% content_for :header do %>
|
2
|
+
<%= render(Koi::Header::IndexComponent.new(model: WellKnown)) do |component| %>
|
3
|
+
<% component.with_action "New", new_admin_well_known_path %>
|
4
|
+
<% end %>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%= table_query_with(collection:) %>
|
8
|
+
|
9
|
+
<%= table_with(collection:) do |row| %>
|
10
|
+
<% row.select %>
|
11
|
+
<% row.link :name %>
|
12
|
+
<% row.text :purpose %>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<%= table_pagination_with(collection:) %>
|