katalyst-koi 5.3.1 → 5.5.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -12
  3. data/app/assets/builds/katalyst/koi.esm.js +9 -19
  4. data/app/assets/builds/katalyst/koi.js +9 -19
  5. data/app/assets/builds/katalyst/koi.min.js +1 -1
  6. data/app/assets/builds/katalyst/koi.min.js.map +1 -1
  7. data/app/assets/stylesheets/koi/blocks/index.css +1 -0
  8. data/app/assets/stylesheets/koi/{login.css → blocks/roadblock.css} +8 -5
  9. data/app/controllers/admin/admin_users_controller.rb +3 -2
  10. data/app/controllers/admin/credentials_controller.rb +33 -51
  11. data/app/controllers/admin/device_authorizations_controller.rb +58 -0
  12. data/app/controllers/admin/device_tokens_controller.rb +18 -0
  13. data/app/controllers/admin/otps_controller.rb +6 -16
  14. data/app/controllers/admin/profiles_controller.rb +33 -0
  15. data/app/controllers/admin/sessions_controller.rb +12 -6
  16. data/app/controllers/admin/tokens_controller.rb +6 -2
  17. data/app/controllers/concerns/koi/controller/has_admin_users.rb +8 -11
  18. data/app/controllers/concerns/koi/controller/has_webauthn.rb +54 -9
  19. data/app/controllers/concerns/koi/controller.rb +7 -0
  20. data/app/javascript/koi/controllers/webauthn_authentication_controller.js +1 -1
  21. data/app/javascript/koi/controllers/webauthn_registration_controller.js +8 -18
  22. data/app/jobs/admin/device_authorizations_cleanup_job.rb +9 -0
  23. data/app/models/admin/credential.rb +4 -0
  24. data/app/models/admin/device_authorization.rb +113 -0
  25. data/app/models/admin/user.rb +1 -0
  26. data/app/models/koi/current.rb +8 -0
  27. data/app/views/admin/admin_users/edit.html.erb +3 -1
  28. data/app/views/admin/admin_users/show.html.erb +3 -0
  29. data/app/views/admin/credentials/_credentials.html.erb +8 -5
  30. data/app/views/admin/credentials/new.html.erb +32 -41
  31. data/app/views/admin/credentials/show.html.erb +19 -0
  32. data/app/views/admin/device_authorizations/show.html.erb +38 -0
  33. data/app/views/admin/otps/_form.html.erb +1 -1
  34. data/app/views/admin/{admin_users/_form.html+self.erb → profiles/_form.html.erb} +1 -1
  35. data/app/views/admin/{admin_users/edit.html+self.erb → profiles/edit.html.erb} +0 -1
  36. data/app/views/admin/{admin_users/show.html+self.erb → profiles/show.html.erb} +15 -10
  37. data/app/views/admin/sessions/new.html.erb +26 -27
  38. data/app/views/admin/sessions/otp.html.erb +13 -5
  39. data/app/views/admin/sessions/password.html.erb +16 -8
  40. data/app/views/admin/tokens/show.html.erb +12 -8
  41. data/app/views/layouts/koi/_application_navigation.html.erb +13 -9
  42. data/app/views/layouts/koi/application.html.erb +19 -10
  43. data/config/locales/koi.en.yml +0 -1
  44. data/config/routes.rb +17 -9
  45. data/db/migrate/20260413014834_create_admin_device_authorizations.rb +20 -0
  46. data/lib/generators/koi/helpers/resource_helpers.rb +1 -1
  47. data/lib/koi/config.rb +2 -1
  48. data/lib/koi/engine.rb +1 -0
  49. data/lib/koi/middleware/admin_authentication.rb +54 -10
  50. data/spec/factories/admin_device_authorizations.rb +29 -0
  51. metadata +30 -10
  52. data/app/views/admin/credentials/_credentials.html+self.erb +0 -12
  53. data/app/views/admin/credentials/create.turbo_stream.erb +0 -5
  54. data/app/views/admin/credentials/destroy.turbo_stream.erb +0 -5
  55. data/app/views/layouts/koi/login.html.erb +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fff0aa99c8c482aaf2d05c7d93da90e802d044a69f8cff1c50a17a6cb6ce868
4
- data.tar.gz: 7d321a7d00a22b2a4959f78b64e23a951d21dacf7fd06f5e3639a1867cbc94c0
3
+ metadata.gz: 9a64f8ea56e122d169ec52b8c304c79a1b81289bba216d89d2b97c9f0aaf9e24
4
+ data.tar.gz: e07ac3a269c0cf2958f33fb9e3da25dec86a84d27f25f3d0314ecc4d4fed5f4f
5
5
  SHA512:
6
- metadata.gz: ddcdd05da615485835c3d65c3cd914019ff56c5ea08bdec8a0ab1165f916c8e94bce30937be8c58ed0edfc4fa472755cd6aed3c6c8d6017d5596c5e3057254a8
7
- data.tar.gz: ce0d0003503723e1276d3ef948b1dc1202239e35bd6e24946d35298acf5ffdbab18f8d617bd1a8a94fabf9861ee5029f207dfe21b510225ce4ba2609c6089a03
6
+ metadata.gz: c337a6f84e5ce47739dc9242207b9eafd2a0fbf6c9bf3f5b89338910175b3598dc575037849fb1a844e1a1608b54cb9e08e891286047b1548bbe127484ae446a
7
+ data.tar.gz: 84b32b36bd70147f24f0b975df0b722ba37f860185c1df483c1947c55ec17c74d1332e16b29d30b95f1f0f59021dced986db12ae005f7962349f3cd30df74246
data/README.md CHANGED
@@ -1,25 +1,19 @@
1
- # Koi
1
+ # Katalyst Koi
2
2
 
3
3
  Koi is a framework for building Rails admin functionality.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ In most cases, Koi is installed using `https://github.com/katalyst/koi-template`.
8
8
 
9
- ```ruby
10
- gem "katalyst-koi"
11
- ```
9
+ ## API Access
12
10
 
13
- And then execute:
14
-
15
- ```sh
16
- bundle install
17
- ```
11
+ See [docs/api-access.md](docs/api-access.md).
18
12
 
19
13
  ## Contributing
20
14
 
21
- Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/katalyst-tables.
15
+ Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/koi.
22
16
 
23
17
  ## License
24
18
 
25
- Katalyst Tables is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
19
+ Koi is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -399,46 +399,36 @@ class WebauthnAuthenticationController extends Controller {
399
399
  get options() {
400
400
  return {
401
401
  publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
402
- this.optionsValue.publicKey,
402
+ this.optionsValue,
403
403
  ),
404
404
  };
405
405
  }
406
406
  }
407
407
 
408
408
  class WebauthnRegistrationController extends Controller {
409
+ static targets = ["response"];
409
410
  static values = {
410
411
  options: Object,
411
- response: Object,
412
412
  };
413
- static targets = ["intro", "nickname", "response"];
414
413
 
415
414
  submit(e) {
416
- if (
417
- this.responseTarget.value === "" &&
418
- e.submitter.formMethod !== "dialog"
419
- ) {
420
- e.preventDefault();
421
- this.createCredential().then();
422
- }
415
+ if (this.responseTarget.value) return;
416
+
417
+ e.preventDefault();
418
+ this.createCredential().then(() => {
419
+ e.target.submit();
420
+ });
423
421
  }
424
422
 
425
423
  async createCredential() {
426
424
  const credential = await navigator.credentials.create(this.options);
427
-
428
- this.responseValue = credential.toJSON();
429
425
  this.responseTarget.value = JSON.stringify(credential.toJSON());
430
426
  }
431
427
 
432
- responseValueChanged(response) {
433
- const responsePresent = response !== "";
434
- this.introTarget.toggleAttribute("hidden", responsePresent);
435
- this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
436
- }
437
-
438
428
  get options() {
439
429
  return {
440
430
  publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(
441
- this.optionsValue.publicKey,
431
+ this.optionsValue,
442
432
  ),
443
433
  };
444
434
  }
@@ -399,46 +399,36 @@ class WebauthnAuthenticationController extends Controller {
399
399
  get options() {
400
400
  return {
401
401
  publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
402
- this.optionsValue.publicKey,
402
+ this.optionsValue,
403
403
  ),
404
404
  };
405
405
  }
406
406
  }
407
407
 
408
408
  class WebauthnRegistrationController extends Controller {
409
+ static targets = ["response"];
409
410
  static values = {
410
411
  options: Object,
411
- response: Object,
412
412
  };
413
- static targets = ["intro", "nickname", "response"];
414
413
 
415
414
  submit(e) {
416
- if (
417
- this.responseTarget.value === "" &&
418
- e.submitter.formMethod !== "dialog"
419
- ) {
420
- e.preventDefault();
421
- this.createCredential().then();
422
- }
415
+ if (this.responseTarget.value) return;
416
+
417
+ e.preventDefault();
418
+ this.createCredential().then(() => {
419
+ e.target.submit();
420
+ });
423
421
  }
424
422
 
425
423
  async createCredential() {
426
424
  const credential = await navigator.credentials.create(this.options);
427
-
428
- this.responseValue = credential.toJSON();
429
425
  this.responseTarget.value = JSON.stringify(credential.toJSON());
430
426
  }
431
427
 
432
- responseValueChanged(response) {
433
- const responsePresent = response !== "";
434
- this.introTarget.toggleAttribute("hidden", responsePresent);
435
- this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
436
- }
437
-
438
428
  get options() {
439
429
  return {
440
430
  publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(
441
- this.optionsValue.publicKey,
431
+ this.optionsValue,
442
432
  ),
443
433
  };
444
434
  }
@@ -1,2 +1,2 @@
1
- import"@hotwired/turbo-rails";import e,{initAll as t}from"@katalyst/govuk-formbuilder";import"@rails/actiontext";import"trix";import{Application as i,Controller as r}from"@hotwired/stimulus";import s from"@katalyst/content";import o from"@katalyst/navigation";import n from"@katalyst/tables";const a=i.start();a.load(s),a.load(e),a.load(o),a.load(n);const l=[{identifier:"clipboard",controllerConstructor:class extends r{static targets=["source"];static classes=["supported"];connect(){"clipboard"in navigator&&this.element.classList.add(this.supportedClass)}copy(e){e.preventDefault(),navigator.clipboard.writeText(this.sourceTarget.value),this.element.classList.add("copied"),setTimeout(()=>{this.element.classList.remove("copied")},2e3)}}},{identifier:"flash",controllerConstructor:class extends r{close(e){e.target.closest("li").remove(),0===this.element.children.length&&this.element.remove()}}},{identifier:"keyboard",controllerConstructor:class extends r{static values={mapping:String,depth:{type:Number,default:2}};event(e){if(function(e){if(!(e instanceof HTMLElement))return!1;const t=e.nodeName.toLowerCase(),i=(e.getAttribute("type")||"").toLowerCase();return"select"===t||"textarea"===t||"trix-editor"===t||"input"===t&&"submit"!==i&&"reset"!==i&&"checkbox"!==i&&"radio"!==i&&"file"!==i||e.isContentEditable}(e.target)||this.#e(e))return;const t=this.describeEvent(e);this.buffer=[...this.buffer||[],t].slice(0-this.depthValue);const i=this.buffer.reduceRight((e,t)=>"string"==typeof e||void 0===e?e:e[t],this.mappings);if("string"!=typeof i)return;this.buffer=[],e.preventDefault();const r=new CustomEvent(i,{detail:{cause:e},bubbles:!0});e.target.dispatchEvent(r)}describeEvent(e){return[e.ctrlKey&&"C",e.metaKey&&"M",e.altKey&&"A",e.shiftKey&&"S",e.code].filter(e=>e).join("-")}get mappings(){const e=this.mappingValue.replaceAll(/\s+/g," ").split(" ").filter(e=>e.length>0),t={};return e.forEach(e=>this.#t(t,e)),Object.defineProperty(this,"mappings",{value:t,writable:!1}),t}#t(e,t){const[i,r]=t.split("->"),s=i.split("+"),o=s.shift();(e=s.reduceRight((e,t)=>e[t]||={},e))[o]=r}#e(e){switch(e.code){case"ControlLeft":case"ControlRight":case"MetaLeft":case"MetaRight":case"ShiftLeft":case"ShiftRight":case"AltLeft":case"AltRight":return!0;default:return!1}}}},{identifier:"modal",controllerConstructor:class extends r{static targets=["dialog"];connect(){this.element.addEventListener("turbo:submit-end",this.onSubmit)}disconnect(){this.element.removeEventListener("turbo:submit-end",this.onSubmit)}outside(e){"DIALOG"===e.target.tagName&&this.dismiss()}dismiss(){this.dialogTarget&&(this.dialogTarget.open||this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}dialogTargetConnected(e){e.showModal()}onSubmit=e=>{e.detail.success&&"closeDialog"in e.detail.formSubmission?.submitter?.dataset&&(this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}}},{identifier:"navigation",controllerConstructor:class extends r{static targets=["filter"];filter(){const e=this.filterTarget.value;this.clearFilter(e),e.length>0&&this.applyFilter(e)}go(){this.element.querySelector("li:not([hidden]) > a").click()}clear(){0===this.filterTarget.value.length&&this.filterTarget.blur()}applyFilter(e){this.links.filter(t=>!this.prefixSearch(e.toLowerCase(),t.innerText.toLowerCase())).forEach(e=>{e.toggleAttribute("hidden",!0)}),this.menus.filter(e=>!e.matches("li:has(li:not([hidden]) > a)")).forEach(e=>{e.toggleAttribute("hidden",!0)})}clearFilter(e){this.element.querySelectorAll("li").forEach(e=>{e.toggleAttribute("hidden",!1)})}prefixSearch(e,t){const i=t.length,r=e.length;if(r>i)return!1;if(r===i)return e===t;e:for(let s=0,o=0;s<r;s++){const r=e.charCodeAt(s);if(32!==r){for(;o<i;){if(t.charCodeAt(o++)===r)continue e;for(;o<i&&32!==t.charCodeAt(o++););}return!1}for(;o<i&&32!==t.charCodeAt(o++););}return!0}toggle(){this.element.open?this.close():this.open()}open(){this.element.open||this.element.showModal()}close(){this.element.open&&this.element.close()}click(e){e.target===this.element&&this.close()}onMorphAttribute=e=>{if(e.target===this.element&&"open"===e.detail.attributeName)e.preventDefault()};get links(){return Array.from(this.element.querySelectorAll("li:has(> a)"))}get menus(){return Array.from(this.element.querySelectorAll("li:has(> ul)"))}}},{identifier:"navigation-toggle",controllerConstructor:class extends r{trigger(){this.dispatch("toggle",{prefix:"navigation",bubbles:!0})}}},{identifier:"pagy-nav",controllerConstructor:class extends r{connect(){document.addEventListener("shortcut:page-prev",this.prevPage),document.addEventListener("shortcut:page-next",this.nextPage)}disconnect(){document.removeEventListener("shortcut:page-prev",this.prevPage),document.removeEventListener("shortcut:page-next",this.nextPage)}nextPage=()=>{this.element.querySelector("a:last-child").click()};prevPage=()=>{this.element.querySelector("a:first-child").click()}}},{identifier:"sluggable",controllerConstructor:class extends r{static targets=["source","slug"];static values={slug:String};sourceChanged(e){""===this.slugValue&&(this.slugTarget.value=this.sourceTarget.value.toLowerCase().replace(/'/g,"-").replace(/[^-\w\s]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,""))}slugChanged(e){this.slugValue=this.slugTarget.value}}},{identifier:"webauthn-authentication",controllerConstructor:class extends r{static targets=["response"];static values={options:Object};async authenticate(){const e=await navigator.credentials.get(this.options);this.responseTarget.value=JSON.stringify(e.toJSON()),this.element.requestSubmit()}get options(){return{publicKey:PublicKeyCredential.parseRequestOptionsFromJSON(this.optionsValue.publicKey)}}}},{identifier:"webauthn-registration",controllerConstructor:class extends r{static values={options:Object,response:Object};static targets=["intro","nickname","response"];submit(e){""===this.responseTarget.value&&"dialog"!==e.submitter.formMethod&&(e.preventDefault(),this.createCredential().then())}async createCredential(){const e=await navigator.credentials.create(this.options);this.responseValue=e.toJSON(),this.responseTarget.value=JSON.stringify(e.toJSON())}responseValueChanged(e){const t=""!==e;this.introTarget.toggleAttribute("hidden",t),this.nicknameTarget.toggleAttribute("hidden",!t)}get options(){return{publicKey:PublicKeyCredential.parseCreationOptionsFromJSON(this.optionsValue.publicKey)}}}}];await import("controllers/hw_combobox_controller").then(({default:e})=>{l.push({identifier:"hw-combobox",controllerConstructor:e})}).catch(()=>null),a.load(l);class c extends HTMLElement{constructor(){super(),this.setAttribute("role","toolbar")}}function u(){document.body.classList.toggle("js-enabled",!0),document.body.classList.toggle("govuk-frontend-supported","noModule"in HTMLScriptElement.prototype),t()}customElements.define("koi-toolbar",c),window.addEventListener("turbo:load",u),window.Turbo&&u();
1
+ import"@hotwired/turbo-rails";import e,{initAll as t}from"@katalyst/govuk-formbuilder";import"@rails/actiontext";import"trix";import{Application as i,Controller as r}from"@hotwired/stimulus";import s from"@katalyst/content";import o from"@katalyst/navigation";import n from"@katalyst/tables";const a=i.start();a.load(s),a.load(e),a.load(o),a.load(n);const l=[{identifier:"clipboard",controllerConstructor:class extends r{static targets=["source"];static classes=["supported"];connect(){"clipboard"in navigator&&this.element.classList.add(this.supportedClass)}copy(e){e.preventDefault(),navigator.clipboard.writeText(this.sourceTarget.value),this.element.classList.add("copied"),setTimeout(()=>{this.element.classList.remove("copied")},2e3)}}},{identifier:"flash",controllerConstructor:class extends r{close(e){e.target.closest("li").remove(),0===this.element.children.length&&this.element.remove()}}},{identifier:"keyboard",controllerConstructor:class extends r{static values={mapping:String,depth:{type:Number,default:2}};event(e){if(function(e){if(!(e instanceof HTMLElement))return!1;const t=e.nodeName.toLowerCase(),i=(e.getAttribute("type")||"").toLowerCase();return"select"===t||"textarea"===t||"trix-editor"===t||"input"===t&&"submit"!==i&&"reset"!==i&&"checkbox"!==i&&"radio"!==i&&"file"!==i||e.isContentEditable}(e.target)||this.#e(e))return;const t=this.describeEvent(e);this.buffer=[...this.buffer||[],t].slice(0-this.depthValue);const i=this.buffer.reduceRight((e,t)=>"string"==typeof e||void 0===e?e:e[t],this.mappings);if("string"!=typeof i)return;this.buffer=[],e.preventDefault();const r=new CustomEvent(i,{detail:{cause:e},bubbles:!0});e.target.dispatchEvent(r)}describeEvent(e){return[e.ctrlKey&&"C",e.metaKey&&"M",e.altKey&&"A",e.shiftKey&&"S",e.code].filter(e=>e).join("-")}get mappings(){const e=this.mappingValue.replaceAll(/\s+/g," ").split(" ").filter(e=>e.length>0),t={};return e.forEach(e=>this.#t(t,e)),Object.defineProperty(this,"mappings",{value:t,writable:!1}),t}#t(e,t){const[i,r]=t.split("->"),s=i.split("+"),o=s.shift();(e=s.reduceRight((e,t)=>e[t]||={},e))[o]=r}#e(e){switch(e.code){case"ControlLeft":case"ControlRight":case"MetaLeft":case"MetaRight":case"ShiftLeft":case"ShiftRight":case"AltLeft":case"AltRight":return!0;default:return!1}}}},{identifier:"modal",controllerConstructor:class extends r{static targets=["dialog"];connect(){this.element.addEventListener("turbo:submit-end",this.onSubmit)}disconnect(){this.element.removeEventListener("turbo:submit-end",this.onSubmit)}outside(e){"DIALOG"===e.target.tagName&&this.dismiss()}dismiss(){this.dialogTarget&&(this.dialogTarget.open||this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}dialogTargetConnected(e){e.showModal()}onSubmit=e=>{e.detail.success&&"closeDialog"in e.detail.formSubmission?.submitter?.dataset&&(this.dialogTarget.close(),this.element.removeAttribute("src"),this.dialogTarget.remove())}}},{identifier:"navigation",controllerConstructor:class extends r{static targets=["filter"];filter(){const e=this.filterTarget.value;this.clearFilter(e),e.length>0&&this.applyFilter(e)}go(){this.element.querySelector("li:not([hidden]) > a").click()}clear(){0===this.filterTarget.value.length&&this.filterTarget.blur()}applyFilter(e){this.links.filter(t=>!this.prefixSearch(e.toLowerCase(),t.innerText.toLowerCase())).forEach(e=>{e.toggleAttribute("hidden",!0)}),this.menus.filter(e=>!e.matches("li:has(li:not([hidden]) > a)")).forEach(e=>{e.toggleAttribute("hidden",!0)})}clearFilter(e){this.element.querySelectorAll("li").forEach(e=>{e.toggleAttribute("hidden",!1)})}prefixSearch(e,t){const i=t.length,r=e.length;if(r>i)return!1;if(r===i)return e===t;e:for(let s=0,o=0;s<r;s++){const r=e.charCodeAt(s);if(32!==r){for(;o<i;){if(t.charCodeAt(o++)===r)continue e;for(;o<i&&32!==t.charCodeAt(o++););}return!1}for(;o<i&&32!==t.charCodeAt(o++););}return!0}toggle(){this.element.open?this.close():this.open()}open(){this.element.open||this.element.showModal()}close(){this.element.open&&this.element.close()}click(e){e.target===this.element&&this.close()}onMorphAttribute=e=>{if(e.target===this.element&&"open"===e.detail.attributeName)e.preventDefault()};get links(){return Array.from(this.element.querySelectorAll("li:has(> a)"))}get menus(){return Array.from(this.element.querySelectorAll("li:has(> ul)"))}}},{identifier:"navigation-toggle",controllerConstructor:class extends r{trigger(){this.dispatch("toggle",{prefix:"navigation",bubbles:!0})}}},{identifier:"pagy-nav",controllerConstructor:class extends r{connect(){document.addEventListener("shortcut:page-prev",this.prevPage),document.addEventListener("shortcut:page-next",this.nextPage)}disconnect(){document.removeEventListener("shortcut:page-prev",this.prevPage),document.removeEventListener("shortcut:page-next",this.nextPage)}nextPage=()=>{this.element.querySelector("a:last-child").click()};prevPage=()=>{this.element.querySelector("a:first-child").click()}}},{identifier:"sluggable",controllerConstructor:class extends r{static targets=["source","slug"];static values={slug:String};sourceChanged(e){""===this.slugValue&&(this.slugTarget.value=this.sourceTarget.value.toLowerCase().replace(/'/g,"-").replace(/[^-\w\s]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,""))}slugChanged(e){this.slugValue=this.slugTarget.value}}},{identifier:"webauthn-authentication",controllerConstructor:class extends r{static targets=["response"];static values={options:Object};async authenticate(){const e=await navigator.credentials.get(this.options);this.responseTarget.value=JSON.stringify(e.toJSON()),this.element.requestSubmit()}get options(){return{publicKey:PublicKeyCredential.parseRequestOptionsFromJSON(this.optionsValue)}}}},{identifier:"webauthn-registration",controllerConstructor:class extends r{static targets=["response"];static values={options:Object};submit(e){this.responseTarget.value||(e.preventDefault(),this.createCredential().then(()=>{e.target.submit()}))}async createCredential(){const e=await navigator.credentials.create(this.options);this.responseTarget.value=JSON.stringify(e.toJSON())}get options(){return{publicKey:PublicKeyCredential.parseCreationOptionsFromJSON(this.optionsValue)}}}}];await import("controllers/hw_combobox_controller").then(({default:e})=>{l.push({identifier:"hw-combobox",controllerConstructor:e})}).catch(()=>null),a.load(l);class c extends HTMLElement{constructor(){super(),this.setAttribute("role","toolbar")}}function u(){document.body.classList.toggle("js-enabled",!0),document.body.classList.toggle("govuk-frontend-supported","noModule"in HTMLScriptElement.prototype),t()}customElements.define("koi-toolbar",c),window.addEventListener("turbo:load",u),window.Turbo&&u();
2
2
  //# sourceMappingURL=koi.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"koi.min.js","sources":["../../../javascript/koi/controllers/application.js","../../../javascript/koi/controllers/index.js","../../../javascript/koi/controllers/clipboard_controller.js","../../../javascript/koi/controllers/flash_controller.js","../../../javascript/koi/controllers/keyboard_controller.js","../../../javascript/koi/controllers/modal_controller.js","../../../javascript/koi/controllers/navigation_controller.js","../../../javascript/koi/controllers/navigation_toggle_controller.js","../../../javascript/koi/controllers/pagy_nav_controller.js","../../../javascript/koi/controllers/sluggable_controller.js","../../../javascript/koi/controllers/webauthn_authentication_controller.js","../../../javascript/koi/controllers/webauthn_registration_controller.js","../../../javascript/koi/elements/toolbar.js","../../../javascript/koi/application.js"],"sourcesContent":["import { Application } from \"@hotwired/stimulus\";\n\nconst application = Application.start();\n\nexport { application };\n","import { application } from \"./application\";\n\nimport content from \"@katalyst/content\";\napplication.load(content);\n\nimport govuk from \"@katalyst/govuk-formbuilder\";\napplication.load(govuk);\n\nimport navigation from \"@katalyst/navigation\";\napplication.load(navigation);\n\nimport tables from \"@katalyst/tables\";\napplication.load(tables);\n\nimport ClipboardController from \"./clipboard_controller\";\nimport FlashController from \"./flash_controller\";\nimport KeyboardController from \"./keyboard_controller\";\nimport ModalController from \"./modal_controller\";\nimport NavigationController from \"./navigation_controller\";\nimport NavigationToggleController from \"./navigation_toggle_controller\";\nimport PagyNavController from \"./pagy_nav_controller\";\nimport SluggableController from \"./sluggable_controller\";\nimport WebauthnAuthenticationController from \"./webauthn_authentication_controller\";\nimport WebauthnRegistrationController from \"./webauthn_registration_controller\";\n\nconst Definitions = [\n {\n identifier: \"clipboard\",\n controllerConstructor: ClipboardController,\n },\n {\n identifier: \"flash\",\n controllerConstructor: FlashController,\n },\n {\n identifier: \"keyboard\",\n controllerConstructor: KeyboardController,\n },\n {\n identifier: \"modal\",\n controllerConstructor: ModalController,\n },\n {\n identifier: \"navigation\",\n controllerConstructor: NavigationController,\n },\n {\n identifier: \"navigation-toggle\",\n controllerConstructor: NavigationToggleController,\n },\n {\n identifier: \"pagy-nav\",\n controllerConstructor: PagyNavController,\n },\n {\n identifier: \"sluggable\",\n controllerConstructor: SluggableController,\n },\n {\n identifier: \"webauthn-authentication\",\n controllerConstructor: WebauthnAuthenticationController,\n },\n {\n identifier: \"webauthn-registration\",\n controllerConstructor: WebauthnRegistrationController,\n },\n];\n\n// dynamically attempt to load hw_combobox_controller, this is an optional dependency\nawait import(\"controllers/hw_combobox_controller\")\n .then(({ default: HwComboboxController }) => {\n Definitions.push({\n identifier: \"hw-combobox\",\n controllerConstructor: HwComboboxController,\n });\n })\n .catch(() => null);\n\napplication.load(Definitions);\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ClipboardController extends Controller {\n static targets = [\"source\"];\n\n static classes = [\"supported\"];\n\n connect() {\n if (\"clipboard\" in navigator) {\n this.element.classList.add(this.supportedClass);\n }\n }\n\n copy(event) {\n event.preventDefault();\n navigator.clipboard.writeText(this.sourceTarget.value);\n\n this.element.classList.add(\"copied\");\n setTimeout(() => {\n this.element.classList.remove(\"copied\");\n }, 2000);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class FlashController extends Controller {\n close(e) {\n e.target.closest(\"li\").remove();\n\n // remove the flash container if there are no more flashes\n if (this.element.children.length === 0) {\n this.element.remove();\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nconst DEBUG = false;\n\nexport default class KeyboardController extends Controller {\n static values = {\n mapping: String,\n depth: { type: Number, default: 2 },\n };\n\n event(cause) {\n if (isFormField(cause.target) || this.#ignore(cause)) return;\n\n const key = this.describeEvent(cause);\n\n this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);\n\n if (DEBUG) console.debug(\"[keyboard] buffer:\", ...this.buffer);\n\n // test whether the tail of the buffer matches any of the configured chords\n const action = this.buffer.reduceRight((mapping, key) => {\n if (typeof mapping === \"string\" || typeof mapping === \"undefined\") {\n return mapping;\n } else {\n return mapping[key];\n }\n }, this.mappings);\n\n // if we don't have a string we may have a miss or an incomplete chord\n if (typeof action !== \"string\") return;\n\n // clear the buffer and prevent the key from being consumed elsewhere\n this.buffer = [];\n cause.preventDefault();\n\n if (DEBUG) console.debug(\"[keyboard] event: %s\", action);\n\n // fire the configured event\n const event = new CustomEvent(action, {\n detail: { cause: cause },\n bubbles: true,\n });\n cause.target.dispatchEvent(event);\n }\n\n /**\n * @param event KeyboardEvent input event to describe\n * @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)\n */\n describeEvent(event) {\n return [\n event.ctrlKey && \"C\",\n event.metaKey && \"M\",\n event.altKey && \"A\",\n event.shiftKey && \"S\",\n event.code,\n ]\n .filter((w) => w)\n .join(\"-\");\n }\n\n /**\n * Build a tree for efficiently looking up key chords, where the last key in the sequence\n * is the first key in tree.\n */\n get mappings() {\n const inputs = this.mappingValue\n .replaceAll(/\\s+/g, \" \")\n .split(\" \")\n .filter((f) => f.length > 0);\n const mappings = {};\n\n inputs.forEach((mapping) => this.#parse(mappings, mapping));\n\n // memoize the result\n Object.defineProperty(this, \"mappings\", {\n value: mappings,\n writable: false,\n });\n\n return mappings;\n }\n\n /**\n * Parse a key chord pattern and an event and store it in the inverted tree lookup structure.\n *\n * @param mappings inverted tree lookup for key chords\n * @param mapping input definition, e.g. \"C-KeyC+C-KeyV->paste\"\n */\n #parse(mappings, mapping) {\n const [pattern, event] = mapping.split(\"->\");\n const keys = pattern.split(\"+\");\n const first = keys.shift();\n\n mappings = keys.reduceRight(\n (mappings, key) => (mappings[key] ||= {}),\n mappings,\n );\n mappings[first] = event;\n }\n\n /**\n * Ignore modifier keys, as they will be captured in normal key presses.\n *\n * @param event KeyboardEvent\n * @returns {boolean} true if key event should be ignored\n */\n #ignore(event) {\n switch (event.code) {\n case \"ControlLeft\":\n case \"ControlRight\":\n case \"MetaLeft\":\n case \"MetaRight\":\n case \"ShiftLeft\":\n case \"ShiftRight\":\n case \"AltLeft\":\n case \"AltRight\":\n return true;\n default:\n return false;\n }\n }\n}\n\n/**\n * Detect input nodes where we should not listen for events.\n *\n * Credit: github.com\n */\nfunction isFormField(element) {\n if (!(element instanceof HTMLElement)) {\n return false;\n }\n\n const name = element.nodeName.toLowerCase();\n const type = (element.getAttribute(\"type\") || \"\").toLowerCase();\n return (\n name === \"select\" ||\n name === \"textarea\" ||\n name === \"trix-editor\" ||\n (name === \"input\" &&\n type !== \"submit\" &&\n type !== \"reset\" &&\n type !== \"checkbox\" &&\n type !== \"radio\" &&\n type !== \"file\") ||\n element.isContentEditable\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ModalController extends Controller {\n static targets = [\"dialog\"];\n\n connect() {\n this.element.addEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n disconnect() {\n this.element.removeEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n outside(e) {\n if (e.target.tagName === \"DIALOG\") this.dismiss();\n }\n\n dismiss() {\n if (!this.dialogTarget) return;\n if (!this.dialogTarget.open) this.dialogTarget.close();\n\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n\n dialogTargetConnected(dialog) {\n dialog.showModal();\n }\n\n onSubmit = (event) => {\n if (\n event.detail.success &&\n \"closeDialog\" in event.detail.formSubmission?.submitter?.dataset\n ) {\n this.dialogTarget.close();\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationController extends Controller {\n static targets = [\"filter\"];\n\n filter() {\n const filter = this.filterTarget.value;\n this.clearFilter(filter);\n\n if (filter.length > 0) {\n this.applyFilter(filter);\n }\n }\n\n go() {\n this.element.querySelector(\"li:not([hidden]) > a\").click();\n }\n\n clear() {\n if (this.filterTarget.value.length === 0) this.filterTarget.blur();\n }\n\n applyFilter(filter) {\n // hide items that don't match the search filter\n this.links\n .filter(\n (li) =>\n !this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),\n )\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n\n this.menus\n .filter((li) => !li.matches(\"li:has(li:not([hidden]) > a)\"))\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n }\n\n clearFilter(filter) {\n this.element.querySelectorAll(\"li\").forEach((li) => {\n li.toggleAttribute(\"hidden\", false);\n });\n }\n\n prefixSearch(needle, haystack) {\n const haystackLength = haystack.length;\n const needleLength = needle.length;\n if (needleLength > haystackLength) {\n return false;\n }\n if (needleLength === haystackLength) {\n return needle === haystack;\n }\n outer: for (let i = 0, j = 0; i < needleLength; i++) {\n const needleChar = needle.charCodeAt(i);\n if (needleChar === 32) {\n // skip ahead to next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n continue;\n }\n while (j < haystackLength) {\n if (haystack.charCodeAt(j++) === needleChar) continue outer;\n // skip ahead to the next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n }\n return false;\n }\n return true;\n }\n\n toggle() {\n this.element.open ? this.close() : this.open();\n }\n\n open() {\n if (!this.element.open) this.element.showModal();\n }\n\n close() {\n if (this.element.open) this.element.close();\n }\n\n click(e) {\n if (e.target === this.element) this.close();\n }\n\n onMorphAttribute = (e) => {\n if (e.target !== this.element) return;\n\n switch (e.detail.attributeName) {\n case \"open\":\n e.preventDefault();\n }\n };\n\n get links() {\n return Array.from(this.element.querySelectorAll(\"li:has(> a)\"));\n }\n\n get menus() {\n return Array.from(this.element.querySelectorAll(\"li:has(> ul)\"));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationToggleController extends Controller {\n trigger() {\n this.dispatch(\"toggle\", { prefix: \"navigation\", bubbles: true });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class PagyNavController extends Controller {\n connect() {\n document.addEventListener(\"shortcut:page-prev\", this.prevPage);\n document.addEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n disconnect() {\n document.removeEventListener(\"shortcut:page-prev\", this.prevPage);\n document.removeEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n nextPage = () => {\n this.element.querySelector(\"a:last-child\").click();\n };\n\n prevPage = () => {\n this.element.querySelector(\"a:first-child\").click();\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Connect an input (e.g. title) to slug.\n */\nexport default class SluggableController extends Controller {\n static targets = [\"source\", \"slug\"];\n static values = {\n slug: String,\n };\n\n sourceChanged(e) {\n if (this.slugValue === \"\") {\n this.slugTarget.value = parameterize(this.sourceTarget.value);\n }\n }\n\n slugChanged(e) {\n this.slugValue = this.slugTarget.value;\n }\n}\n\nfunction parameterize(input) {\n return input\n .toLowerCase()\n .replace(/'/g, \"-\")\n .replace(/[^-\\w\\s]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnAuthenticationController extends Controller {\n static targets = [\"response\"];\n static values = {\n options: Object,\n };\n\n async authenticate() {\n const credential = await navigator.credentials.get(this.options);\n\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n\n this.element.requestSubmit();\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(\n this.optionsValue.publicKey,\n ),\n };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnRegistrationController extends Controller {\n static values = {\n options: Object,\n response: Object,\n };\n static targets = [\"intro\", \"nickname\", \"response\"];\n\n submit(e) {\n if (\n this.responseTarget.value === \"\" &&\n e.submitter.formMethod !== \"dialog\"\n ) {\n e.preventDefault();\n this.createCredential().then();\n }\n }\n\n async createCredential() {\n const credential = await navigator.credentials.create(this.options);\n\n this.responseValue = credential.toJSON();\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n }\n\n responseValueChanged(response) {\n const responsePresent = response !== \"\";\n this.introTarget.toggleAttribute(\"hidden\", responsePresent);\n this.nicknameTarget.toggleAttribute(\"hidden\", !responsePresent);\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(\n this.optionsValue.publicKey,\n ),\n };\n }\n}\n","class KoiToolbarElement extends HTMLElement {\n constructor() {\n super();\n\n this.setAttribute(\"role\", \"toolbar\");\n }\n}\n\ncustomElements.define(\"koi-toolbar\", KoiToolbarElement);\n","import \"@hotwired/turbo-rails\";\nimport { initAll } from \"@katalyst/govuk-formbuilder\";\nimport \"@rails/actiontext\";\nimport \"trix\";\n\nimport \"./controllers\";\nimport \"./elements\";\n\n/** Initialize GOVUK */\nfunction initGOVUK() {\n document.body.classList.toggle(\"js-enabled\", true);\n document.body.classList.toggle(\n \"govuk-frontend-supported\",\n \"noModule\" in HTMLScriptElement.prototype,\n );\n initAll();\n}\n\nwindow.addEventListener(\"turbo:load\", initGOVUK);\nif (window.Turbo) initGOVUK();\n"],"names":["application","Application","start","load","content","govuk","navigation","tables","Definitions","identifier","controllerConstructor","Controller","static","connect","navigator","this","element","classList","add","supportedClass","copy","event","preventDefault","clipboard","writeText","sourceTarget","value","setTimeout","remove","close","e","target","closest","children","length","mapping","String","depth","type","Number","default","cause","HTMLElement","name","nodeName","toLowerCase","getAttribute","isContentEditable","isFormField","ignore","key","describeEvent","buffer","slice","depthValue","action","reduceRight","mappings","CustomEvent","detail","bubbles","dispatchEvent","ctrlKey","metaKey","altKey","shiftKey","code","filter","w","join","inputs","mappingValue","replaceAll","split","f","forEach","parse","Object","defineProperty","writable","pattern","keys","first","shift","addEventListener","onSubmit","disconnect","removeEventListener","outside","tagName","dismiss","dialogTarget","open","removeAttribute","dialogTargetConnected","dialog","showModal","success","formSubmission","submitter","dataset","filterTarget","clearFilter","applyFilter","go","querySelector","click","clear","blur","links","li","prefixSearch","innerText","toggleAttribute","menus","matches","querySelectorAll","needle","haystack","haystackLength","needleLength","outer","i","j","needleChar","charCodeAt","toggle","onMorphAttribute","attributeName","Array","from","trigger","dispatch","prefix","document","prevPage","nextPage","slug","sourceChanged","slugValue","slugTarget","replace","slugChanged","options","authenticate","credential","credentials","get","responseTarget","JSON","stringify","toJSON","requestSubmit","publicKey","PublicKeyCredential","parseRequestOptionsFromJSON","optionsValue","response","submit","formMethod","createCredential","then","create","responseValue","responseValueChanged","responsePresent","introTarget","nicknameTarget","parseCreationOptionsFromJSON","import","HwComboboxController","push","catch","KoiToolbarElement","constructor","super","setAttribute","initGOVUK","body","HTMLScriptElement","prototype","initAll","customElements","define","window","Turbo"],"mappings":"oSAEA,MAAMA,EAAcC,EAAYC,QCChCF,EAAYG,KAAKC,GAGjBJ,EAAYG,KAAKE,GAGjBL,EAAYG,KAAKG,GAGjBN,EAAYG,KAAKI,GAajB,MAAMC,EAAc,CAClB,CACEC,WAAY,YACZC,sBC1BW,cAAkCC,EAC/CC,eAAiB,CAAC,UAElBA,eAAiB,CAAC,aAElB,OAAAC,GACM,cAAeC,WACjBC,KAAKC,QAAQC,UAAUC,IAAIH,KAAKI,eAEpC,CAEA,IAAAC,CAAKC,GACHA,EAAMC,iBACNR,UAAUS,UAAUC,UAAUT,KAAKU,aAAaC,OAEhDX,KAAKC,QAAQC,UAAUC,IAAI,UAC3BS,WAAW,KACTZ,KAAKC,QAAQC,UAAUW,OAAO,WAC7B,IACL,IDSA,CACEnB,WAAY,QACZC,sBE9BW,cAA8BC,EAC3C,KAAAkB,CAAMC,GACJA,EAAEC,OAAOC,QAAQ,MAAMJ,SAGc,IAAjCb,KAAKC,QAAQiB,SAASC,QACxBnB,KAAKC,QAAQY,QAEjB,IFwBA,CACEnB,WAAY,WACZC,sBGhCW,cAAiCC,EAC9CC,cAAgB,CACduB,QAASC,OACTC,MAAO,CAAEC,KAAMC,OAAQC,QAAS,IAGlC,KAAAnB,CAAMoB,GACJ,GAsHJ,SAAqBzB,GACnB,KAAMA,aAAmB0B,aACvB,OAAO,EAGT,MAAMC,EAAO3B,EAAQ4B,SAASC,cACxBP,GAAQtB,EAAQ8B,aAAa,SAAW,IAAID,cAClD,MACW,WAATF,GACS,aAATA,GACS,gBAATA,GACU,UAATA,GACU,WAATL,GACS,UAATA,GACS,aAATA,GACS,UAATA,GACS,SAATA,GACFtB,EAAQ+B,iBAEZ,CAzIQC,CAAYP,EAAMV,SAAWhB,MAAKkC,EAAQR,GAAQ,OAEtD,MAAMS,EAAMnC,KAAKoC,cAAcV,GAE/B1B,KAAKqC,OAAS,IAAKrC,KAAKqC,QAAU,GAAKF,GAAKG,MAAM,EAAItC,KAAKuC,YAK3D,MAAMC,EAASxC,KAAKqC,OAAOI,YAAY,CAACrB,EAASe,IACxB,iBAAZf,QAA2C,IAAZA,EACjCA,EAEAA,EAAQe,GAEhBnC,KAAK0C,UAGR,GAAsB,iBAAXF,EAAqB,OAGhCxC,KAAKqC,OAAS,GACdX,EAAMnB,iBAKN,MAAMD,EAAQ,IAAIqC,YAAYH,EAAQ,CACpCI,OAAQ,CAAElB,MAAOA,GACjBmB,SAAS,IAEXnB,EAAMV,OAAO8B,cAAcxC,EAC7B,CAMA,aAAA8B,CAAc9B,GACZ,MAAO,CACLA,EAAMyC,SAAW,IACjBzC,EAAM0C,SAAW,IACjB1C,EAAM2C,QAAU,IAChB3C,EAAM4C,UAAY,IAClB5C,EAAM6C,MAELC,OAAQC,GAAMA,GACdC,KAAK,IACV,CAMA,YAAIZ,GACF,MAAMa,EAASvD,KAAKwD,aACjBC,WAAW,OAAQ,KACnBC,MAAM,KACNN,OAAQO,GAAMA,EAAExC,OAAS,GACtBuB,EAAW,CAAA,EAUjB,OARAa,EAAOK,QAASxC,GAAYpB,MAAK6D,EAAOnB,EAAUtB,IAGlD0C,OAAOC,eAAe/D,KAAM,WAAY,CACtCW,MAAO+B,EACPsB,UAAU,IAGLtB,CACT,CAQA,EAAAmB,CAAOnB,EAAUtB,GACf,MAAO6C,EAAS3D,GAASc,EAAQsC,MAAM,MACjCQ,EAAOD,EAAQP,MAAM,KACrBS,EAAQD,EAAKE,SAEnB1B,EAAWwB,EAAKzB,YACd,CAACC,EAAUP,IAASO,EAASP,KAAS,CAAA,EACtCO,IAEOyB,GAAS7D,CACpB,CAQA,EAAA4B,CAAQ5B,GACN,OAAQA,EAAM6C,MACZ,IAAK,cACL,IAAK,eACL,IAAK,WACL,IAAK,YACL,IAAK,YACL,IAAK,aACL,IAAK,UACL,IAAK,WACH,OAAO,EACT,QACE,OAAO,EAEb,IHnFA,CACEzD,WAAY,QACZC,sBItCW,cAA8BC,EAC3CC,eAAiB,CAAC,UAElB,OAAAC,GACEE,KAAKC,QAAQoE,iBAAiB,mBAAoBrE,KAAKsE,SACzD,CAEA,UAAAC,GACEvE,KAAKC,QAAQuE,oBAAoB,mBAAoBxE,KAAKsE,SAC5D,CAEA,OAAAG,CAAQ1D,GACmB,WAArBA,EAAEC,OAAO0D,SAAsB1E,KAAK2E,SAC1C,CAEA,OAAAA,GACO3E,KAAK4E,eACL5E,KAAK4E,aAAaC,MAAM7E,KAAK4E,aAAa9D,QAE/Cd,KAAKC,QAAQ6E,gBAAgB,OAC7B9E,KAAK4E,aAAa/D,SACpB,CAEA,qBAAAkE,CAAsBC,GACpBA,EAAOC,WACT,CAEAX,SAAYhE,IAERA,EAAMsC,OAAOsC,SACb,gBAAiB5E,EAAMsC,OAAOuC,gBAAgBC,WAAWC,UAEzDrF,KAAK4E,aAAa9D,QAClBd,KAAKC,QAAQ6E,gBAAgB,OAC7B9E,KAAK4E,aAAa/D,aJMtB,CACEnB,WAAY,aACZC,sBK1CW,cAAmCC,EAChDC,eAAiB,CAAC,UAElB,MAAAuD,GACE,MAAMA,EAASpD,KAAKsF,aAAa3E,MACjCX,KAAKuF,YAAYnC,GAEbA,EAAOjC,OAAS,GAClBnB,KAAKwF,YAAYpC,EAErB,CAEA,EAAAqC,GACEzF,KAAKC,QAAQyF,cAAc,wBAAwBC,OACrD,CAEA,KAAAC,GACyC,IAAnC5F,KAAKsF,aAAa3E,MAAMQ,QAAcnB,KAAKsF,aAAaO,MAC9D,CAEA,WAAAL,CAAYpC,GAEVpD,KAAK8F,MACF1C,OACE2C,IACE/F,KAAKgG,aAAa5C,EAAOtB,cAAeiE,EAAGE,UAAUnE,gBAEzD8B,QAASmC,IACRA,EAAGG,gBAAgB,UAAU,KAGjClG,KAAKmG,MACF/C,OAAQ2C,IAAQA,EAAGK,QAAQ,iCAC3BxC,QAASmC,IACRA,EAAGG,gBAAgB,UAAU,IAEnC,CAEA,WAAAX,CAAYnC,GACVpD,KAAKC,QAAQoG,iBAAiB,MAAMzC,QAASmC,IAC3CA,EAAGG,gBAAgB,UAAU,IAEjC,CAEA,YAAAF,CAAaM,EAAQC,GACnB,MAAMC,EAAiBD,EAASpF,OAC1BsF,EAAeH,EAAOnF,OAC5B,GAAIsF,EAAeD,EACjB,OAAO,EAET,GAAIC,IAAiBD,EACnB,OAAOF,IAAWC,EAEpBG,EAAO,IAAK,IAAIC,EAAI,EAAGC,EAAI,EAAGD,EAAIF,EAAcE,IAAK,CACnD,MAAME,EAAaP,EAAOQ,WAAWH,GACrC,GAAmB,KAAfE,EAAJ,CAKA,KAAOD,EAAIJ,GAAgB,CACzB,GAAID,EAASO,WAAWF,OAASC,EAAY,SAASH,EAEtD,KAAOE,EAAIJ,GAA+C,KAA7BD,EAASO,WAAWF,OACnD,CACA,OAAO,CANP,CAFE,KAAOA,EAAIJ,GAA+C,KAA7BD,EAASO,WAAWF,OASrD,CACA,OAAO,CACT,CAEA,MAAAG,GACE/G,KAAKC,QAAQ4E,KAAO7E,KAAKc,QAAUd,KAAK6E,MAC1C,CAEA,IAAAA,GACO7E,KAAKC,QAAQ4E,MAAM7E,KAAKC,QAAQgF,WACvC,CAEA,KAAAnE,GACMd,KAAKC,QAAQ4E,MAAM7E,KAAKC,QAAQa,OACtC,CAEA,KAAA6E,CAAM5E,GACAA,EAAEC,SAAWhB,KAAKC,SAASD,KAAKc,OACtC,CAEAkG,iBAAoBjG,IAClB,GAAIA,EAAEC,SAAWhB,KAAKC,SAGf,SADCc,EAAE6B,OAAOqE,cAEblG,EAAER,kBAIR,SAAIuF,GACF,OAAOoB,MAAMC,KAAKnH,KAAKC,QAAQoG,iBAAiB,eAClD,CAEA,SAAIF,GACF,OAAOe,MAAMC,KAAKnH,KAAKC,QAAQoG,iBAAiB,gBAClD,ILzDA,CACE3G,WAAY,oBACZC,sBM9CW,cAAyCC,EACtD,OAAAwH,GACEpH,KAAKqH,SAAS,SAAU,CAAEC,OAAQ,aAAczE,SAAS,GAC3D,IN6CA,CACEnD,WAAY,WACZC,sBOlDW,cAAgCC,EAC7C,OAAAE,GACEyH,SAASlD,iBAAiB,qBAAsBrE,KAAKwH,UACrDD,SAASlD,iBAAiB,qBAAsBrE,KAAKyH,SACvD,CAEA,UAAAlD,GACEgD,SAAS/C,oBAAoB,qBAAsBxE,KAAKwH,UACxDD,SAAS/C,oBAAoB,qBAAsBxE,KAAKyH,SAC1D,CAEAA,SAAW,KACTzH,KAAKC,QAAQyF,cAAc,gBAAgBC,SAG7C6B,SAAW,KACTxH,KAAKC,QAAQyF,cAAc,iBAAiBC,WPoC9C,CACEjG,WAAY,YACZC,sBQnDW,cAAkCC,EAC/CC,eAAiB,CAAC,SAAU,QAC5BA,cAAgB,CACd6H,KAAMrG,QAGR,aAAAsG,CAAc5G,GACW,KAAnBf,KAAK4H,YACP5H,KAAK6H,WAAWlH,MAAqBX,KAAKU,aAAaC,MAWxDmB,cACAgG,QAAQ,KAAM,KACdA,QAAQ,YAAa,IACrBA,QAAQ,cAAe,KACvBA,QAAQ,WAAY,IAbvB,CAEA,WAAAC,CAAYhH,GACVf,KAAK4H,UAAY5H,KAAK6H,WAAWlH,KACnC,IRuCA,CACEjB,WAAY,0BACZC,sBS1DW,cAA+CC,EAC5DC,eAAiB,CAAC,YAClBA,cAAgB,CACdmI,QAASlE,QAGX,kBAAMmE,GACJ,MAAMC,QAAmBnI,UAAUoI,YAAYC,IAAIpI,KAAKgI,SAExDhI,KAAKqI,eAAe1H,MAAQ2H,KAAKC,UAAUL,EAAWM,UAEtDxI,KAAKC,QAAQwI,eACf,CAEA,WAAIT,GACF,MAAO,CACLU,UAAWC,oBAAoBC,4BAC7B5I,KAAK6I,aAAaH,WAGxB,ITwCA,CACEhJ,WAAY,wBACZC,sBU9DW,cAA6CC,EAC1DC,cAAgB,CACdmI,QAASlE,OACTgF,SAAUhF,QAEZjE,eAAiB,CAAC,QAAS,WAAY,YAEvC,MAAAkJ,CAAOhI,GAE2B,KAA9Bf,KAAKqI,eAAe1H,OACO,WAA3BI,EAAEqE,UAAU4D,aAEZjI,EAAER,iBACFP,KAAKiJ,mBAAmBC,OAE5B,CAEA,sBAAMD,GACJ,MAAMf,QAAmBnI,UAAUoI,YAAYgB,OAAOnJ,KAAKgI,SAE3DhI,KAAKoJ,cAAgBlB,EAAWM,SAChCxI,KAAKqI,eAAe1H,MAAQ2H,KAAKC,UAAUL,EAAWM,SACxD,CAEA,oBAAAa,CAAqBP,GACnB,MAAMQ,EAA+B,KAAbR,EACxB9I,KAAKuJ,YAAYrD,gBAAgB,SAAUoD,GAC3CtJ,KAAKwJ,eAAetD,gBAAgB,UAAWoD,EACjD,CAEA,WAAItB,GACF,MAAO,CACLU,UAAWC,oBAAoBc,6BAC7BzJ,KAAK6I,aAAaH,WAGxB,WV+BIgB,OAAO,sCACVR,KAAK,EAAGzH,QAASkI,MAChBlK,EAAYmK,KAAK,CACflK,WAAY,cACZC,sBAAuBgK,MAG1BE,MAAM,IAAM,MAEf5K,EAAYG,KAAKK,GW9EjB,MAAMqK,UAA0BnI,YAC9B,WAAAoI,GACEC,QAEAhK,KAAKiK,aAAa,OAAQ,UAC5B,ECIF,SAASC,IACP3C,SAAS4C,KAAKjK,UAAU6G,OAAO,cAAc,GAC7CQ,SAAS4C,KAAKjK,UAAU6G,OACtB,2BACA,aAAcqD,kBAAkBC,WAElCC,GACF,CDRAC,eAAeC,OAAO,cAAeV,GCUrCW,OAAOpG,iBAAiB,aAAc6F,GAClCO,OAAOC,OAAOR"}
1
+ {"version":3,"file":"koi.min.js","sources":["../../../javascript/koi/controllers/application.js","../../../javascript/koi/controllers/index.js","../../../javascript/koi/controllers/clipboard_controller.js","../../../javascript/koi/controllers/flash_controller.js","../../../javascript/koi/controllers/keyboard_controller.js","../../../javascript/koi/controllers/modal_controller.js","../../../javascript/koi/controllers/navigation_controller.js","../../../javascript/koi/controllers/navigation_toggle_controller.js","../../../javascript/koi/controllers/pagy_nav_controller.js","../../../javascript/koi/controllers/sluggable_controller.js","../../../javascript/koi/controllers/webauthn_authentication_controller.js","../../../javascript/koi/controllers/webauthn_registration_controller.js","../../../javascript/koi/elements/toolbar.js","../../../javascript/koi/application.js"],"sourcesContent":["import { Application } from \"@hotwired/stimulus\";\n\nconst application = Application.start();\n\nexport { application };\n","import { application } from \"./application\";\n\nimport content from \"@katalyst/content\";\napplication.load(content);\n\nimport govuk from \"@katalyst/govuk-formbuilder\";\napplication.load(govuk);\n\nimport navigation from \"@katalyst/navigation\";\napplication.load(navigation);\n\nimport tables from \"@katalyst/tables\";\napplication.load(tables);\n\nimport ClipboardController from \"./clipboard_controller\";\nimport FlashController from \"./flash_controller\";\nimport KeyboardController from \"./keyboard_controller\";\nimport ModalController from \"./modal_controller\";\nimport NavigationController from \"./navigation_controller\";\nimport NavigationToggleController from \"./navigation_toggle_controller\";\nimport PagyNavController from \"./pagy_nav_controller\";\nimport SluggableController from \"./sluggable_controller\";\nimport WebauthnAuthenticationController from \"./webauthn_authentication_controller\";\nimport WebauthnRegistrationController from \"./webauthn_registration_controller\";\n\nconst Definitions = [\n {\n identifier: \"clipboard\",\n controllerConstructor: ClipboardController,\n },\n {\n identifier: \"flash\",\n controllerConstructor: FlashController,\n },\n {\n identifier: \"keyboard\",\n controllerConstructor: KeyboardController,\n },\n {\n identifier: \"modal\",\n controllerConstructor: ModalController,\n },\n {\n identifier: \"navigation\",\n controllerConstructor: NavigationController,\n },\n {\n identifier: \"navigation-toggle\",\n controllerConstructor: NavigationToggleController,\n },\n {\n identifier: \"pagy-nav\",\n controllerConstructor: PagyNavController,\n },\n {\n identifier: \"sluggable\",\n controllerConstructor: SluggableController,\n },\n {\n identifier: \"webauthn-authentication\",\n controllerConstructor: WebauthnAuthenticationController,\n },\n {\n identifier: \"webauthn-registration\",\n controllerConstructor: WebauthnRegistrationController,\n },\n];\n\n// dynamically attempt to load hw_combobox_controller, this is an optional dependency\nawait import(\"controllers/hw_combobox_controller\")\n .then(({ default: HwComboboxController }) => {\n Definitions.push({\n identifier: \"hw-combobox\",\n controllerConstructor: HwComboboxController,\n });\n })\n .catch(() => null);\n\napplication.load(Definitions);\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ClipboardController extends Controller {\n static targets = [\"source\"];\n\n static classes = [\"supported\"];\n\n connect() {\n if (\"clipboard\" in navigator) {\n this.element.classList.add(this.supportedClass);\n }\n }\n\n copy(event) {\n event.preventDefault();\n navigator.clipboard.writeText(this.sourceTarget.value);\n\n this.element.classList.add(\"copied\");\n setTimeout(() => {\n this.element.classList.remove(\"copied\");\n }, 2000);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class FlashController extends Controller {\n close(e) {\n e.target.closest(\"li\").remove();\n\n // remove the flash container if there are no more flashes\n if (this.element.children.length === 0) {\n this.element.remove();\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nconst DEBUG = false;\n\nexport default class KeyboardController extends Controller {\n static values = {\n mapping: String,\n depth: { type: Number, default: 2 },\n };\n\n event(cause) {\n if (isFormField(cause.target) || this.#ignore(cause)) return;\n\n const key = this.describeEvent(cause);\n\n this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);\n\n if (DEBUG) console.debug(\"[keyboard] buffer:\", ...this.buffer);\n\n // test whether the tail of the buffer matches any of the configured chords\n const action = this.buffer.reduceRight((mapping, key) => {\n if (typeof mapping === \"string\" || typeof mapping === \"undefined\") {\n return mapping;\n } else {\n return mapping[key];\n }\n }, this.mappings);\n\n // if we don't have a string we may have a miss or an incomplete chord\n if (typeof action !== \"string\") return;\n\n // clear the buffer and prevent the key from being consumed elsewhere\n this.buffer = [];\n cause.preventDefault();\n\n if (DEBUG) console.debug(\"[keyboard] event: %s\", action);\n\n // fire the configured event\n const event = new CustomEvent(action, {\n detail: { cause: cause },\n bubbles: true,\n });\n cause.target.dispatchEvent(event);\n }\n\n /**\n * @param event KeyboardEvent input event to describe\n * @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)\n */\n describeEvent(event) {\n return [\n event.ctrlKey && \"C\",\n event.metaKey && \"M\",\n event.altKey && \"A\",\n event.shiftKey && \"S\",\n event.code,\n ]\n .filter((w) => w)\n .join(\"-\");\n }\n\n /**\n * Build a tree for efficiently looking up key chords, where the last key in the sequence\n * is the first key in tree.\n */\n get mappings() {\n const inputs = this.mappingValue\n .replaceAll(/\\s+/g, \" \")\n .split(\" \")\n .filter((f) => f.length > 0);\n const mappings = {};\n\n inputs.forEach((mapping) => this.#parse(mappings, mapping));\n\n // memoize the result\n Object.defineProperty(this, \"mappings\", {\n value: mappings,\n writable: false,\n });\n\n return mappings;\n }\n\n /**\n * Parse a key chord pattern and an event and store it in the inverted tree lookup structure.\n *\n * @param mappings inverted tree lookup for key chords\n * @param mapping input definition, e.g. \"C-KeyC+C-KeyV->paste\"\n */\n #parse(mappings, mapping) {\n const [pattern, event] = mapping.split(\"->\");\n const keys = pattern.split(\"+\");\n const first = keys.shift();\n\n mappings = keys.reduceRight(\n (mappings, key) => (mappings[key] ||= {}),\n mappings,\n );\n mappings[first] = event;\n }\n\n /**\n * Ignore modifier keys, as they will be captured in normal key presses.\n *\n * @param event KeyboardEvent\n * @returns {boolean} true if key event should be ignored\n */\n #ignore(event) {\n switch (event.code) {\n case \"ControlLeft\":\n case \"ControlRight\":\n case \"MetaLeft\":\n case \"MetaRight\":\n case \"ShiftLeft\":\n case \"ShiftRight\":\n case \"AltLeft\":\n case \"AltRight\":\n return true;\n default:\n return false;\n }\n }\n}\n\n/**\n * Detect input nodes where we should not listen for events.\n *\n * Credit: github.com\n */\nfunction isFormField(element) {\n if (!(element instanceof HTMLElement)) {\n return false;\n }\n\n const name = element.nodeName.toLowerCase();\n const type = (element.getAttribute(\"type\") || \"\").toLowerCase();\n return (\n name === \"select\" ||\n name === \"textarea\" ||\n name === \"trix-editor\" ||\n (name === \"input\" &&\n type !== \"submit\" &&\n type !== \"reset\" &&\n type !== \"checkbox\" &&\n type !== \"radio\" &&\n type !== \"file\") ||\n element.isContentEditable\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ModalController extends Controller {\n static targets = [\"dialog\"];\n\n connect() {\n this.element.addEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n disconnect() {\n this.element.removeEventListener(\"turbo:submit-end\", this.onSubmit);\n }\n\n outside(e) {\n if (e.target.tagName === \"DIALOG\") this.dismiss();\n }\n\n dismiss() {\n if (!this.dialogTarget) return;\n if (!this.dialogTarget.open) this.dialogTarget.close();\n\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n\n dialogTargetConnected(dialog) {\n dialog.showModal();\n }\n\n onSubmit = (event) => {\n if (\n event.detail.success &&\n \"closeDialog\" in event.detail.formSubmission?.submitter?.dataset\n ) {\n this.dialogTarget.close();\n this.element.removeAttribute(\"src\");\n this.dialogTarget.remove();\n }\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationController extends Controller {\n static targets = [\"filter\"];\n\n filter() {\n const filter = this.filterTarget.value;\n this.clearFilter(filter);\n\n if (filter.length > 0) {\n this.applyFilter(filter);\n }\n }\n\n go() {\n this.element.querySelector(\"li:not([hidden]) > a\").click();\n }\n\n clear() {\n if (this.filterTarget.value.length === 0) this.filterTarget.blur();\n }\n\n applyFilter(filter) {\n // hide items that don't match the search filter\n this.links\n .filter(\n (li) =>\n !this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),\n )\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n\n this.menus\n .filter((li) => !li.matches(\"li:has(li:not([hidden]) > a)\"))\n .forEach((li) => {\n li.toggleAttribute(\"hidden\", true);\n });\n }\n\n clearFilter(filter) {\n this.element.querySelectorAll(\"li\").forEach((li) => {\n li.toggleAttribute(\"hidden\", false);\n });\n }\n\n prefixSearch(needle, haystack) {\n const haystackLength = haystack.length;\n const needleLength = needle.length;\n if (needleLength > haystackLength) {\n return false;\n }\n if (needleLength === haystackLength) {\n return needle === haystack;\n }\n outer: for (let i = 0, j = 0; i < needleLength; i++) {\n const needleChar = needle.charCodeAt(i);\n if (needleChar === 32) {\n // skip ahead to next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n continue;\n }\n while (j < haystackLength) {\n if (haystack.charCodeAt(j++) === needleChar) continue outer;\n // skip ahead to the next space in the haystack\n while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}\n }\n return false;\n }\n return true;\n }\n\n toggle() {\n this.element.open ? this.close() : this.open();\n }\n\n open() {\n if (!this.element.open) this.element.showModal();\n }\n\n close() {\n if (this.element.open) this.element.close();\n }\n\n click(e) {\n if (e.target === this.element) this.close();\n }\n\n onMorphAttribute = (e) => {\n if (e.target !== this.element) return;\n\n switch (e.detail.attributeName) {\n case \"open\":\n e.preventDefault();\n }\n };\n\n get links() {\n return Array.from(this.element.querySelectorAll(\"li:has(> a)\"));\n }\n\n get menus() {\n return Array.from(this.element.querySelectorAll(\"li:has(> ul)\"));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NavigationToggleController extends Controller {\n trigger() {\n this.dispatch(\"toggle\", { prefix: \"navigation\", bubbles: true });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class PagyNavController extends Controller {\n connect() {\n document.addEventListener(\"shortcut:page-prev\", this.prevPage);\n document.addEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n disconnect() {\n document.removeEventListener(\"shortcut:page-prev\", this.prevPage);\n document.removeEventListener(\"shortcut:page-next\", this.nextPage);\n }\n\n nextPage = () => {\n this.element.querySelector(\"a:last-child\").click();\n };\n\n prevPage = () => {\n this.element.querySelector(\"a:first-child\").click();\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Connect an input (e.g. title) to slug.\n */\nexport default class SluggableController extends Controller {\n static targets = [\"source\", \"slug\"];\n static values = {\n slug: String,\n };\n\n sourceChanged(e) {\n if (this.slugValue === \"\") {\n this.slugTarget.value = parameterize(this.sourceTarget.value);\n }\n }\n\n slugChanged(e) {\n this.slugValue = this.slugTarget.value;\n }\n}\n\nfunction parameterize(input) {\n return input\n .toLowerCase()\n .replace(/'/g, \"-\")\n .replace(/[^-\\w\\s]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnAuthenticationController extends Controller {\n static targets = [\"response\"];\n static values = {\n options: Object,\n };\n\n async authenticate() {\n const credential = await navigator.credentials.get(this.options);\n\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n\n this.element.requestSubmit();\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(\n this.optionsValue,\n ),\n };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class WebauthnRegistrationController extends Controller {\n static targets = [\"response\"];\n static values = {\n options: Object,\n };\n\n submit(e) {\n if (this.responseTarget.value) return;\n\n e.preventDefault();\n this.createCredential().then(() => {\n e.target.submit();\n });\n }\n\n async createCredential() {\n const credential = await navigator.credentials.create(this.options);\n this.responseTarget.value = JSON.stringify(credential.toJSON());\n }\n\n get options() {\n return {\n publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(\n this.optionsValue,\n ),\n };\n }\n}\n","class KoiToolbarElement extends HTMLElement {\n constructor() {\n super();\n\n this.setAttribute(\"role\", \"toolbar\");\n }\n}\n\ncustomElements.define(\"koi-toolbar\", KoiToolbarElement);\n","import \"@hotwired/turbo-rails\";\nimport { initAll } from \"@katalyst/govuk-formbuilder\";\nimport \"@rails/actiontext\";\nimport \"trix\";\n\nimport \"./controllers\";\nimport \"./elements\";\n\n/** Initialize GOVUK */\nfunction initGOVUK() {\n document.body.classList.toggle(\"js-enabled\", true);\n document.body.classList.toggle(\n \"govuk-frontend-supported\",\n \"noModule\" in HTMLScriptElement.prototype,\n );\n initAll();\n}\n\nwindow.addEventListener(\"turbo:load\", initGOVUK);\nif (window.Turbo) initGOVUK();\n"],"names":["application","Application","start","load","content","govuk","navigation","tables","Definitions","identifier","controllerConstructor","Controller","static","connect","navigator","this","element","classList","add","supportedClass","copy","event","preventDefault","clipboard","writeText","sourceTarget","value","setTimeout","remove","close","e","target","closest","children","length","mapping","String","depth","type","Number","default","cause","HTMLElement","name","nodeName","toLowerCase","getAttribute","isContentEditable","isFormField","ignore","key","describeEvent","buffer","slice","depthValue","action","reduceRight","mappings","CustomEvent","detail","bubbles","dispatchEvent","ctrlKey","metaKey","altKey","shiftKey","code","filter","w","join","inputs","mappingValue","replaceAll","split","f","forEach","parse","Object","defineProperty","writable","pattern","keys","first","shift","addEventListener","onSubmit","disconnect","removeEventListener","outside","tagName","dismiss","dialogTarget","open","removeAttribute","dialogTargetConnected","dialog","showModal","success","formSubmission","submitter","dataset","filterTarget","clearFilter","applyFilter","go","querySelector","click","clear","blur","links","li","prefixSearch","innerText","toggleAttribute","menus","matches","querySelectorAll","needle","haystack","haystackLength","needleLength","outer","i","j","needleChar","charCodeAt","toggle","onMorphAttribute","attributeName","Array","from","trigger","dispatch","prefix","document","prevPage","nextPage","slug","sourceChanged","slugValue","slugTarget","replace","slugChanged","options","authenticate","credential","credentials","get","responseTarget","JSON","stringify","toJSON","requestSubmit","publicKey","PublicKeyCredential","parseRequestOptionsFromJSON","optionsValue","submit","createCredential","then","create","parseCreationOptionsFromJSON","import","HwComboboxController","push","catch","KoiToolbarElement","constructor","super","setAttribute","initGOVUK","body","HTMLScriptElement","prototype","initAll","customElements","define","window","Turbo"],"mappings":"oSAEA,MAAMA,EAAcC,EAAYC,QCChCF,EAAYG,KAAKC,GAGjBJ,EAAYG,KAAKE,GAGjBL,EAAYG,KAAKG,GAGjBN,EAAYG,KAAKI,GAajB,MAAMC,EAAc,CAClB,CACEC,WAAY,YACZC,sBC1BW,cAAkCC,EAC/CC,eAAiB,CAAC,UAElBA,eAAiB,CAAC,aAElB,OAAAC,GACM,cAAeC,WACjBC,KAAKC,QAAQC,UAAUC,IAAIH,KAAKI,eAEpC,CAEA,IAAAC,CAAKC,GACHA,EAAMC,iBACNR,UAAUS,UAAUC,UAAUT,KAAKU,aAAaC,OAEhDX,KAAKC,QAAQC,UAAUC,IAAI,UAC3BS,WAAW,KACTZ,KAAKC,QAAQC,UAAUW,OAAO,WAC7B,IACL,IDSA,CACEnB,WAAY,QACZC,sBE9BW,cAA8BC,EAC3C,KAAAkB,CAAMC,GACJA,EAAEC,OAAOC,QAAQ,MAAMJ,SAGc,IAAjCb,KAAKC,QAAQiB,SAASC,QACxBnB,KAAKC,QAAQY,QAEjB,IFwBA,CACEnB,WAAY,WACZC,sBGhCW,cAAiCC,EAC9CC,cAAgB,CACduB,QAASC,OACTC,MAAO,CAAEC,KAAMC,OAAQC,QAAS,IAGlC,KAAAnB,CAAMoB,GACJ,GAsHJ,SAAqBzB,GACnB,KAAMA,aAAmB0B,aACvB,OAAO,EAGT,MAAMC,EAAO3B,EAAQ4B,SAASC,cACxBP,GAAQtB,EAAQ8B,aAAa,SAAW,IAAID,cAClD,MACW,WAATF,GACS,aAATA,GACS,gBAATA,GACU,UAATA,GACU,WAATL,GACS,UAATA,GACS,aAATA,GACS,UAATA,GACS,SAATA,GACFtB,EAAQ+B,iBAEZ,CAzIQC,CAAYP,EAAMV,SAAWhB,MAAKkC,EAAQR,GAAQ,OAEtD,MAAMS,EAAMnC,KAAKoC,cAAcV,GAE/B1B,KAAKqC,OAAS,IAAKrC,KAAKqC,QAAU,GAAKF,GAAKG,MAAM,EAAItC,KAAKuC,YAK3D,MAAMC,EAASxC,KAAKqC,OAAOI,YAAY,CAACrB,EAASe,IACxB,iBAAZf,QAA2C,IAAZA,EACjCA,EAEAA,EAAQe,GAEhBnC,KAAK0C,UAGR,GAAsB,iBAAXF,EAAqB,OAGhCxC,KAAKqC,OAAS,GACdX,EAAMnB,iBAKN,MAAMD,EAAQ,IAAIqC,YAAYH,EAAQ,CACpCI,OAAQ,CAAElB,MAAOA,GACjBmB,SAAS,IAEXnB,EAAMV,OAAO8B,cAAcxC,EAC7B,CAMA,aAAA8B,CAAc9B,GACZ,MAAO,CACLA,EAAMyC,SAAW,IACjBzC,EAAM0C,SAAW,IACjB1C,EAAM2C,QAAU,IAChB3C,EAAM4C,UAAY,IAClB5C,EAAM6C,MAELC,OAAQC,GAAMA,GACdC,KAAK,IACV,CAMA,YAAIZ,GACF,MAAMa,EAASvD,KAAKwD,aACjBC,WAAW,OAAQ,KACnBC,MAAM,KACNN,OAAQO,GAAMA,EAAExC,OAAS,GACtBuB,EAAW,CAAA,EAUjB,OARAa,EAAOK,QAASxC,GAAYpB,MAAK6D,EAAOnB,EAAUtB,IAGlD0C,OAAOC,eAAe/D,KAAM,WAAY,CACtCW,MAAO+B,EACPsB,UAAU,IAGLtB,CACT,CAQA,EAAAmB,CAAOnB,EAAUtB,GACf,MAAO6C,EAAS3D,GAASc,EAAQsC,MAAM,MACjCQ,EAAOD,EAAQP,MAAM,KACrBS,EAAQD,EAAKE,SAEnB1B,EAAWwB,EAAKzB,YACd,CAACC,EAAUP,IAASO,EAASP,KAAS,CAAA,EACtCO,IAEOyB,GAAS7D,CACpB,CAQA,EAAA4B,CAAQ5B,GACN,OAAQA,EAAM6C,MACZ,IAAK,cACL,IAAK,eACL,IAAK,WACL,IAAK,YACL,IAAK,YACL,IAAK,aACL,IAAK,UACL,IAAK,WACH,OAAO,EACT,QACE,OAAO,EAEb,IHnFA,CACEzD,WAAY,QACZC,sBItCW,cAA8BC,EAC3CC,eAAiB,CAAC,UAElB,OAAAC,GACEE,KAAKC,QAAQoE,iBAAiB,mBAAoBrE,KAAKsE,SACzD,CAEA,UAAAC,GACEvE,KAAKC,QAAQuE,oBAAoB,mBAAoBxE,KAAKsE,SAC5D,CAEA,OAAAG,CAAQ1D,GACmB,WAArBA,EAAEC,OAAO0D,SAAsB1E,KAAK2E,SAC1C,CAEA,OAAAA,GACO3E,KAAK4E,eACL5E,KAAK4E,aAAaC,MAAM7E,KAAK4E,aAAa9D,QAE/Cd,KAAKC,QAAQ6E,gBAAgB,OAC7B9E,KAAK4E,aAAa/D,SACpB,CAEA,qBAAAkE,CAAsBC,GACpBA,EAAOC,WACT,CAEAX,SAAYhE,IAERA,EAAMsC,OAAOsC,SACb,gBAAiB5E,EAAMsC,OAAOuC,gBAAgBC,WAAWC,UAEzDrF,KAAK4E,aAAa9D,QAClBd,KAAKC,QAAQ6E,gBAAgB,OAC7B9E,KAAK4E,aAAa/D,aJMtB,CACEnB,WAAY,aACZC,sBK1CW,cAAmCC,EAChDC,eAAiB,CAAC,UAElB,MAAAuD,GACE,MAAMA,EAASpD,KAAKsF,aAAa3E,MACjCX,KAAKuF,YAAYnC,GAEbA,EAAOjC,OAAS,GAClBnB,KAAKwF,YAAYpC,EAErB,CAEA,EAAAqC,GACEzF,KAAKC,QAAQyF,cAAc,wBAAwBC,OACrD,CAEA,KAAAC,GACyC,IAAnC5F,KAAKsF,aAAa3E,MAAMQ,QAAcnB,KAAKsF,aAAaO,MAC9D,CAEA,WAAAL,CAAYpC,GAEVpD,KAAK8F,MACF1C,OACE2C,IACE/F,KAAKgG,aAAa5C,EAAOtB,cAAeiE,EAAGE,UAAUnE,gBAEzD8B,QAASmC,IACRA,EAAGG,gBAAgB,UAAU,KAGjClG,KAAKmG,MACF/C,OAAQ2C,IAAQA,EAAGK,QAAQ,iCAC3BxC,QAASmC,IACRA,EAAGG,gBAAgB,UAAU,IAEnC,CAEA,WAAAX,CAAYnC,GACVpD,KAAKC,QAAQoG,iBAAiB,MAAMzC,QAASmC,IAC3CA,EAAGG,gBAAgB,UAAU,IAEjC,CAEA,YAAAF,CAAaM,EAAQC,GACnB,MAAMC,EAAiBD,EAASpF,OAC1BsF,EAAeH,EAAOnF,OAC5B,GAAIsF,EAAeD,EACjB,OAAO,EAET,GAAIC,IAAiBD,EACnB,OAAOF,IAAWC,EAEpBG,EAAO,IAAK,IAAIC,EAAI,EAAGC,EAAI,EAAGD,EAAIF,EAAcE,IAAK,CACnD,MAAME,EAAaP,EAAOQ,WAAWH,GACrC,GAAmB,KAAfE,EAAJ,CAKA,KAAOD,EAAIJ,GAAgB,CACzB,GAAID,EAASO,WAAWF,OAASC,EAAY,SAASH,EAEtD,KAAOE,EAAIJ,GAA+C,KAA7BD,EAASO,WAAWF,OACnD,CACA,OAAO,CANP,CAFE,KAAOA,EAAIJ,GAA+C,KAA7BD,EAASO,WAAWF,OASrD,CACA,OAAO,CACT,CAEA,MAAAG,GACE/G,KAAKC,QAAQ4E,KAAO7E,KAAKc,QAAUd,KAAK6E,MAC1C,CAEA,IAAAA,GACO7E,KAAKC,QAAQ4E,MAAM7E,KAAKC,QAAQgF,WACvC,CAEA,KAAAnE,GACMd,KAAKC,QAAQ4E,MAAM7E,KAAKC,QAAQa,OACtC,CAEA,KAAA6E,CAAM5E,GACAA,EAAEC,SAAWhB,KAAKC,SAASD,KAAKc,OACtC,CAEAkG,iBAAoBjG,IAClB,GAAIA,EAAEC,SAAWhB,KAAKC,SAGf,SADCc,EAAE6B,OAAOqE,cAEblG,EAAER,kBAIR,SAAIuF,GACF,OAAOoB,MAAMC,KAAKnH,KAAKC,QAAQoG,iBAAiB,eAClD,CAEA,SAAIF,GACF,OAAOe,MAAMC,KAAKnH,KAAKC,QAAQoG,iBAAiB,gBAClD,ILzDA,CACE3G,WAAY,oBACZC,sBM9CW,cAAyCC,EACtD,OAAAwH,GACEpH,KAAKqH,SAAS,SAAU,CAAEC,OAAQ,aAAczE,SAAS,GAC3D,IN6CA,CACEnD,WAAY,WACZC,sBOlDW,cAAgCC,EAC7C,OAAAE,GACEyH,SAASlD,iBAAiB,qBAAsBrE,KAAKwH,UACrDD,SAASlD,iBAAiB,qBAAsBrE,KAAKyH,SACvD,CAEA,UAAAlD,GACEgD,SAAS/C,oBAAoB,qBAAsBxE,KAAKwH,UACxDD,SAAS/C,oBAAoB,qBAAsBxE,KAAKyH,SAC1D,CAEAA,SAAW,KACTzH,KAAKC,QAAQyF,cAAc,gBAAgBC,SAG7C6B,SAAW,KACTxH,KAAKC,QAAQyF,cAAc,iBAAiBC,WPoC9C,CACEjG,WAAY,YACZC,sBQnDW,cAAkCC,EAC/CC,eAAiB,CAAC,SAAU,QAC5BA,cAAgB,CACd6H,KAAMrG,QAGR,aAAAsG,CAAc5G,GACW,KAAnBf,KAAK4H,YACP5H,KAAK6H,WAAWlH,MAAqBX,KAAKU,aAAaC,MAWxDmB,cACAgG,QAAQ,KAAM,KACdA,QAAQ,YAAa,IACrBA,QAAQ,cAAe,KACvBA,QAAQ,WAAY,IAbvB,CAEA,WAAAC,CAAYhH,GACVf,KAAK4H,UAAY5H,KAAK6H,WAAWlH,KACnC,IRuCA,CACEjB,WAAY,0BACZC,sBS1DW,cAA+CC,EAC5DC,eAAiB,CAAC,YAClBA,cAAgB,CACdmI,QAASlE,QAGX,kBAAMmE,GACJ,MAAMC,QAAmBnI,UAAUoI,YAAYC,IAAIpI,KAAKgI,SAExDhI,KAAKqI,eAAe1H,MAAQ2H,KAAKC,UAAUL,EAAWM,UAEtDxI,KAAKC,QAAQwI,eACf,CAEA,WAAIT,GACF,MAAO,CACLU,UAAWC,oBAAoBC,4BAC7B5I,KAAK6I,cAGX,ITwCA,CACEnJ,WAAY,wBACZC,sBU9DW,cAA6CC,EAC1DC,eAAiB,CAAC,YAClBA,cAAgB,CACdmI,QAASlE,QAGX,MAAAgF,CAAO/H,GACDf,KAAKqI,eAAe1H,QAExBI,EAAER,iBACFP,KAAK+I,mBAAmBC,KAAK,KAC3BjI,EAAEC,OAAO8H,WAEb,CAEA,sBAAMC,GACJ,MAAMb,QAAmBnI,UAAUoI,YAAYc,OAAOjJ,KAAKgI,SAC3DhI,KAAKqI,eAAe1H,MAAQ2H,KAAKC,UAAUL,EAAWM,SACxD,CAEA,WAAIR,GACF,MAAO,CACLU,UAAWC,oBAAoBO,6BAC7BlJ,KAAK6I,cAGX,WVyCIM,OAAO,sCACVH,KAAK,EAAGvH,QAAS2H,MAChB3J,EAAY4J,KAAK,CACf3J,WAAY,cACZC,sBAAuByJ,MAG1BE,MAAM,IAAM,MAEfrK,EAAYG,KAAKK,GW9EjB,MAAM8J,UAA0B5H,YAC9B,WAAA6H,GACEC,QAEAzJ,KAAK0J,aAAa,OAAQ,UAC5B,ECIF,SAASC,IACPpC,SAASqC,KAAK1J,UAAU6G,OAAO,cAAc,GAC7CQ,SAASqC,KAAK1J,UAAU6G,OACtB,2BACA,aAAc8C,kBAAkBC,WAElCC,GACF,CDRAC,eAAeC,OAAO,cAAeV,GCUrCW,OAAO7F,iBAAiB,aAAcsF,GAClCO,OAAOC,OAAOR"}
@@ -4,6 +4,7 @@
4
4
  @import url("button.css");
5
5
  @import url("flash.css");
6
6
  @import url("icon.css");
7
+ @import url("roadblock.css");
7
8
  @import url("modal.css");
8
9
  @import url("navigation.css");
9
10
  @import url("page-header.css");
@@ -1,7 +1,10 @@
1
- .login {
1
+ .roadblock {
2
+ min-width: min(max(50%, 20rem), 100%);
3
+ max-width: min(max(50%, 20rem), 100%);
4
+
2
5
  header {
3
6
  display: grid;
4
- grid-template-areas: "logo header" "logo site-name";
7
+ grid-template-areas: "icon heading" "icon sub-heading";
5
8
  grid-template-columns: auto 1fr;
6
9
  grid-template-rows: auto auto;
7
10
  column-gap: var(--space-s);
@@ -9,17 +12,17 @@
9
12
  .icon {
10
13
  --scale: 4;
11
14
  --offset: 2px;
12
- grid-area: logo;
15
+ grid-area: icon;
13
16
  align-self: center;
14
17
  }
15
18
 
16
19
  h1 {
17
- grid-area: header;
20
+ grid-area: heading;
18
21
  font-weight: var(--font-bold);
19
22
  }
20
23
 
21
24
  h2 {
22
- grid-area: site-name;
25
+ grid-area: sub-heading;
23
26
  font-weight: var(--font-medium);
24
27
  font-size: var(--size-step-1);
25
28
  color: var(--color-mid);
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Admin
4
4
  class AdminUsersController < ApplicationController
5
+ before_action :requires_session_authentication!
5
6
  before_action :set_admin_user, only: %i[show edit update destroy]
6
7
 
7
8
  attr_reader :admin_user
@@ -63,6 +64,8 @@ module Admin
63
64
  end
64
65
 
65
66
  def destroy
67
+ return redirect_back_or_to(admin_admin_users_path, status: :see_other) if admin_user == current_admin
68
+
66
69
  if admin_user.archived?
67
70
  admin_user.destroy!
68
71
 
@@ -78,8 +81,6 @@ module Admin
78
81
 
79
82
  def set_admin_user
80
83
  @admin_user = Admin::User.with_archived.find(params[:id])
81
-
82
- request.variant << :self if admin_user == current_admin_user
83
84
  end
84
85
 
85
86
  def admin_user_params
@@ -4,65 +4,44 @@ module Admin
4
4
  class CredentialsController < ApplicationController
5
5
  include Koi::Controller::HasWebauthn
6
6
 
7
- before_action :set_admin_user
7
+ before_action :requires_session_authentication!
8
+ before_action :set_admin_user, only: %i[new create]
9
+ before_action :set_credential, only: %i[show update destroy]
8
10
 
9
- attr_reader :admin_user
11
+ before_action :check_authorization!
10
12
 
11
- def new
12
- unless admin_user.webauthn_id
13
- admin_user.update!(webauthn_id: WebAuthn.generate_user_id)
14
- end
13
+ attr_reader :admin_user, :credential
15
14
 
16
- options = webauthn_relying_party.options_for_registration(
17
- user: {
18
- id: admin_user.webauthn_id,
19
- name: admin_user.email,
20
- display_name: admin_user.name,
21
- },
22
- exclude: admin_user.credentials.map(&:external_id),
23
- )
15
+ def show
16
+ render locals: { credential: }
17
+ end
24
18
 
25
- # Store the newly generated challenge somewhere so you can have it
26
- # for the verification phase.
27
- session[:creation_challenge] = options.challenge
19
+ def new
20
+ render locals: { admin_user: }
21
+ end
28
22
 
29
- credential = admin_user.credentials.new
23
+ def create
24
+ webauthn_register!(credential_params[:response])
30
25
 
31
- render locals: { admin_user:, credential:, options: }
26
+ if %r{/credentials/new$}.match?(request.referer)
27
+ redirect_to(admin_root_path, status: :see_other)
28
+ else
29
+ redirect_back_or_to(admin_profile_path, status: :see_other)
30
+ end
32
31
  end
33
32
 
34
- def create
35
- redirect_to(action: :new) if session[:creation_challenge].blank?
36
-
37
- webauthn_credential = webauthn_relying_party.verify_registration(
38
- JSON.parse(credential_params[:response]),
39
- session.delete(:creation_challenge),
40
- )
41
-
42
- credential = admin_user.credentials.find_or_initialize_by(
43
- external_id: webauthn_credential.id,
44
- )
45
-
46
- credential.update!(
47
- nickname: credential_params[:nickname],
48
- public_key: webauthn_credential.public_key,
49
- sign_count: webauthn_credential.sign_count,
50
- )
51
-
52
- respond_to do |format|
53
- format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
54
- format.turbo_stream { render locals: { admin_user: } }
33
+ def update
34
+ if credential.update(credential_params)
35
+ redirect_to(admin_profile_path, status: :see_other)
36
+ else
37
+ render :show, locals: { credential: }, status: :unprocessable_content
55
38
  end
56
39
  end
57
40
 
58
41
  def destroy
59
- credential = admin_user.credentials.find(params[:id])
60
42
  credential.destroy!
61
43
 
62
- respond_to do |format|
63
- format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
64
- format.turbo_stream { render locals: { admin_user: } }
65
- end
44
+ redirect_to(admin_profile_path, status: :see_other)
66
45
  end
67
46
 
68
47
  private
@@ -72,13 +51,16 @@ module Admin
72
51
  end
73
52
 
74
53
  def set_admin_user
75
- @admin_user = Admin::User.find(params[:admin_user_id])
54
+ @admin_user = current_admin
55
+ end
76
56
 
77
- if current_admin == admin_user
78
- request.variant = :self
79
- else
80
- head(:forbidden)
81
- end
57
+ def set_credential
58
+ @credential = Credential.find(params[:id])
59
+ @admin_user = @credential.admin
60
+ end
61
+
62
+ def check_authorization!
63
+ head(:forbidden) unless admin_user == current_admin
82
64
  end
83
65
  end
84
66
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class DeviceAuthorizationsController < ApplicationController
5
+ EXPIRES_IN = 10.minutes
6
+ INTERVAL = 5
7
+
8
+ rate_limit to: 3, within: 1.minute, only: :create
9
+ skip_before_action :verify_authenticity_token, only: :create
10
+
11
+ before_action :set_device_authorization, only: %i[show update]
12
+
13
+ attr_reader :device_authorization
14
+
15
+ delegate :admin_user, to: ::Koi::Current
16
+
17
+ def show
18
+ render locals: { device_authorization: }
19
+ end
20
+
21
+ def create
22
+ device_authorization, device_code = Admin::DeviceAuthorization.issue!(
23
+ requested_ip: request.remote_ip,
24
+ user_agent: request.user_agent,
25
+ )
26
+
27
+ render json: {
28
+ device_code:,
29
+ user_code: device_authorization.user_code,
30
+ verification_uri: admin_device_authorization_url(device_authorization.user_code),
31
+ verification_uri_complete: admin_device_authorization_url(device_authorization.user_code),
32
+ expires_in: EXPIRES_IN.to_i,
33
+ interval: INTERVAL,
34
+ }
35
+ end
36
+
37
+ def update
38
+ if device_authorization.actionable?
39
+ case params[:decision]
40
+ when "approve"
41
+ device_authorization.approve!(admin_user:)
42
+ else
43
+ device_authorization.deny!(admin_user:)
44
+ end
45
+
46
+ redirect_to(admin_device_authorization_path(device_authorization.user_code), status: :see_other)
47
+ else
48
+ render(:show, status: :unprocessable_content, locals: { device_authorization: })
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def set_device_authorization
55
+ @device_authorization = Admin::DeviceAuthorization.find_by!(user_code: params[:user_code])
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class DeviceTokensController < ApplicationController
5
+ GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
6
+
7
+ rate_limit to: 20, within: 1.minute, only: :create
8
+ skip_before_action :verify_authenticity_token, only: :create
9
+
10
+ def create
11
+ return render(json: { error: "invalid_request" }, status: :bad_request) unless params[:grant_type] == GRANT_TYPE
12
+
13
+ render json: Admin::DeviceAuthorization.issue_access_token!(device_code: params[:device_code])
14
+ rescue Admin::DeviceAuthorization::TokenError => e
15
+ render json: { error: e.code }, status: :bad_request
16
+ end
17
+ end
18
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Admin
4
4
  class OtpsController < ApplicationController
5
- before_action :set_admin_user
5
+ alias_method :admin_user, :current_admin
6
6
 
7
- attr_reader :admin_user
7
+ before_action :requires_session_authentication!
8
8
 
9
9
  def new
10
10
  admin_user.otp_secret = ROTP::Base32.random
@@ -16,14 +16,14 @@ module Admin
16
16
  admin_user.otp_secret = otp_params[:otp_secret]
17
17
 
18
18
  if admin_user.otp.verify(otp_params[:token])
19
- admin_user.save
19
+ admin_user.save!
20
20
 
21
- redirect_to admin_admin_user_path(admin_user), status: :see_other
21
+ redirect_to admin_profile_path, status: :see_other
22
22
  else
23
23
  admin_user.errors.add(:token, :invalid)
24
24
 
25
25
  respond_to do |format|
26
- format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
26
+ format.html { redirect_to admin_profile_path, status: :see_other }
27
27
  format.turbo_stream { render locals: { admin_user: }, status: :unprocessable_content }
28
28
  end
29
29
  end
@@ -32,7 +32,7 @@ module Admin
32
32
  def destroy
33
33
  admin_user.update!(otp_secret: nil)
34
34
 
35
- redirect_to admin_admin_user_path(admin_user), status: :see_other
35
+ redirect_to admin_profile_path, status: :see_other
36
36
  end
37
37
 
38
38
  private
@@ -40,15 +40,5 @@ module Admin
40
40
  def otp_params
41
41
  params.expect(admin_user: %i[otp_secret token])
42
42
  end
43
-
44
- def set_admin_user
45
- @admin_user = Admin::User.find(params[:admin_user_id])
46
-
47
- if current_admin == admin_user
48
- request.variant = :self
49
- else
50
- head(:forbidden)
51
- end
52
- end
53
43
  end
54
44
  end