kempo 4.7.0 → 4.9.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 (132) hide show
  1. package/README.md +10 -4
  2. package/bin/cli.js +38 -34
  3. package/dist/admin/components/EmailTemplates.js +25 -0
  4. package/dist/admin/components/EmailTemplates.js.map +1 -0
  5. package/dist/admin/components/Emails.js +49 -0
  6. package/dist/admin/components/Emails.js.map +1 -0
  7. package/dist/admin/components/Updates.js +117 -0
  8. package/dist/admin/components/Updates.js.map +1 -0
  9. package/dist/admin/components/settings/AdminSettings.js +7 -0
  10. package/dist/admin/components/settings/AdminSettings.js.map +1 -0
  11. package/dist/admin/components/settings/BooleanSetting.js +2 -0
  12. package/dist/admin/components/settings/BooleanSetting.js.map +1 -0
  13. package/dist/admin/components/settings/JsonSetting.js +2 -0
  14. package/dist/admin/components/settings/JsonSetting.js.map +1 -0
  15. package/dist/admin/components/settings/NumberSetting.js +2 -0
  16. package/dist/admin/components/settings/NumberSetting.js.map +1 -0
  17. package/dist/admin/components/settings/SecretSetting.js +2 -0
  18. package/dist/admin/components/settings/SecretSetting.js.map +1 -0
  19. package/dist/admin/components/settings/Setting.js +2 -0
  20. package/dist/admin/components/settings/Setting.js.map +1 -0
  21. package/dist/admin/components/settings/SettingEditor.js +2 -0
  22. package/dist/admin/components/settings/SettingEditor.js.map +1 -0
  23. package/dist/admin/components/settings/SettingsPanel.js +24 -0
  24. package/dist/admin/components/settings/SettingsPanel.js.map +1 -0
  25. package/dist/admin/components/settings/StringSetting.js +2 -0
  26. package/dist/admin/components/settings/StringSetting.js.map +1 -0
  27. package/dist/admin/components/settings/humanize.js +2 -0
  28. package/dist/admin/components/settings/humanize.js.map +1 -0
  29. package/dist/admin/emails/index.page.html +14 -0
  30. package/dist/admin/icons/mail.svg +1 -0
  31. package/dist/admin/nav.fragment.html +12 -0
  32. package/dist/admin/settings/[owner]/[name]/index.page.html +1 -1
  33. package/dist/admin/settings/index.page.html +6 -289
  34. package/dist/admin/updates/index.page.html +14 -0
  35. package/dist/kempo/api/email-templates/GET.js +2 -0
  36. package/dist/kempo/api/email-templates/GET.js.map +1 -0
  37. package/dist/kempo/api/email-templates/file/DELETE.js +2 -0
  38. package/dist/kempo/api/email-templates/file/DELETE.js.map +1 -0
  39. package/dist/kempo/api/email-templates/file/GET.js +2 -0
  40. package/dist/kempo/api/email-templates/file/GET.js.map +1 -0
  41. package/dist/kempo/api/email-templates/file/PUT.js +2 -0
  42. package/dist/kempo/api/email-templates/file/PUT.js.map +1 -0
  43. package/dist/kempo/api/email-templates/preview/GET.js +2 -0
  44. package/dist/kempo/api/email-templates/preview/GET.js.map +1 -0
  45. package/dist/kempo/api/emails/GET.js +2 -0
  46. package/dist/kempo/api/emails/GET.js.map +1 -0
  47. package/dist/kempo/api/emails/[id]/GET.js +2 -0
  48. package/dist/kempo/api/emails/[id]/GET.js.map +1 -0
  49. package/dist/kempo/api/emails/[id]/retry/POST.js +2 -0
  50. package/dist/kempo/api/emails/[id]/retry/POST.js.map +1 -0
  51. package/dist/kempo/api/emails/test/POST.js +2 -0
  52. package/dist/kempo/api/emails/test/POST.js.map +1 -0
  53. package/dist/kempo/api/settings/[owner]/[name]/POST.js +1 -1
  54. package/dist/kempo/api/settings/[owner]/[name]/POST.js.map +1 -1
  55. package/dist/kempo/api/updates/GET.js +2 -0
  56. package/dist/kempo/api/updates/GET.js.map +1 -0
  57. package/dist/kempo/api/updates/apply/POST.js +2 -0
  58. package/dist/kempo/api/updates/apply/POST.js.map +1 -0
  59. package/dist/kempo/api/updates/check/POST.js +2 -0
  60. package/dist/kempo/api/updates/check/POST.js.map +1 -0
  61. package/dist/kempo/api/updates/discard/POST.js +2 -0
  62. package/dist/kempo/api/updates/discard/POST.js.map +1 -0
  63. package/dist/kempo/api/updates/prepare/POST.js +2 -0
  64. package/dist/kempo/api/updates/prepare/POST.js.map +1 -0
  65. package/dist/kempo/api/updates/rollback/POST.js +2 -0
  66. package/dist/kempo/api/updates/rollback/POST.js.map +1 -0
  67. package/dist/kempo/fetch.js +1 -1
  68. package/dist/kempo/fetch.js.map +1 -1
  69. package/dist/kempo/sdk.js +1 -1
  70. package/dist/kempo/sdk.js.map +1 -1
  71. package/docker/.dockerignore +9 -0
  72. package/docker/Dockerfile +25 -0
  73. package/docker/docker-compose.prod.yml +39 -0
  74. package/docker/entrypoint.mjs +96 -0
  75. package/middleware/kempo.js +71 -0
  76. package/package.json +4 -3
  77. package/server/db/schema.js +44 -0
  78. package/server/db/upgrades/README.md +20 -0
  79. package/server/sdk.js +8 -0
  80. package/server/utils/email/defaultSender.js +2 -0
  81. package/server/utils/email/dispatchEmail.js +56 -0
  82. package/server/utils/email/emailSettings.js +21 -0
  83. package/server/utils/email/emailTemplatePaths.js +36 -0
  84. package/server/utils/email/getEmail.js +14 -0
  85. package/server/utils/email/getSenderEmail.js +11 -0
  86. package/server/utils/email/getSenderFrom.js +18 -0
  87. package/server/utils/email/isValidSender.js +4 -0
  88. package/server/utils/email/listEmailTemplates.js +23 -0
  89. package/server/utils/email/listEmails.js +35 -0
  90. package/server/utils/email/maintainEmails.js +46 -0
  91. package/server/utils/email/pruneEmails.js +29 -0
  92. package/server/utils/email/readEmailTemplate.js +28 -0
  93. package/server/utils/email/resetEmailTemplate.js +15 -0
  94. package/server/utils/email/retryEmail.js +21 -0
  95. package/server/utils/email/saveEmailTemplate.js +38 -0
  96. package/server/utils/email/sendEmail.js +22 -32
  97. package/server/utils/email/sendEmailFromTemplate.js +2 -2
  98. package/server/utils/extensions/checkExtensionUpdate.js +9 -36
  99. package/server/utils/extensions/reconcileExtensions.js +90 -0
  100. package/server/utils/extensions/updateExtension.js +14 -153
  101. package/server/utils/extensions/upgradeExtension.js +117 -0
  102. package/server/utils/extensions/writeSiteFile.js +66 -0
  103. package/server/utils/hooks/triggerHook.js +17 -4
  104. package/server/utils/settings/listSettings.js +10 -5
  105. package/server/utils/settings/setSetting.js +4 -0
  106. package/server/utils/settings/settingRules.js +21 -0
  107. package/server/utils/settings/systemSettings.js +41 -0
  108. package/server/utils/setup/runUpgrades.js +70 -0
  109. package/server/utils/setup/seedDatabase.js +5 -60
  110. package/server/utils/setup/seedIfNew.js +21 -0
  111. package/server/utils/setup/syncCoreSchema.js +130 -0
  112. package/server/utils/setup/systemPermissions.js +77 -0
  113. package/server/utils/updates/applyStagedUpdate.js +95 -0
  114. package/server/utils/updates/applyUpdate.js +76 -0
  115. package/server/utils/updates/backupDatabase.js +35 -0
  116. package/server/utils/updates/discardUpdate.js +11 -0
  117. package/server/utils/updates/extensionTargets.js +101 -0
  118. package/server/utils/updates/fetchManifest.js +26 -0
  119. package/server/utils/updates/getExtensionUpdates.js +70 -0
  120. package/server/utils/updates/getInstallInfo.js +25 -0
  121. package/server/utils/updates/getUpdateStatus.js +57 -0
  122. package/server/utils/updates/helpers.js +30 -0
  123. package/server/utils/updates/notifySchemaAttention.js +35 -0
  124. package/server/utils/updates/npm.js +41 -0
  125. package/server/utils/updates/prepareUpdate.js +109 -0
  126. package/server/utils/updates/releaseStatus.js +23 -0
  127. package/server/utils/updates/restart.js +64 -0
  128. package/server/utils/updates/rollbackUpdate.js +56 -0
  129. package/server/utils/updates/runUpdateCheck.js +52 -0
  130. package/server/utils/updates/runtime.js +15 -0
  131. package/server/utils/updates/siteLayout.js +200 -0
  132. package/server/utils/updates/starterFiles.js +118 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\NumberSetting.js","names":["html","SettingEditor","NumberSetting","change","event","text","target","value","number","Number","trim","isFinite","this","announce","renderControl","disabled","String","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A number setting: a number field. An empty or non-numeric entry is reported as a problem instead of being saved as NaN. */\nexport default class NumberSetting extends SettingEditor {\n change = event => {\n const text = event.target.value;\n const number = Number(text);\n if(text.trim() === '' || !Number.isFinite(number)) this.announce(this.value, 'Enter a number');\n else this.announce(number);\n };\n\n renderControl(){\n return html`<input type=\"number\" step=\"any\" ?disabled=${this.disabled} .value=${String(this.value ?? 0)} @input=${this.change}>`;\n }\n}\n\ncustomElements.define('k-setting-number', NumberSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzCE,OAASC,IACP,MAAMC,EAAOD,EAAME,OAAOC,MACpBC,EAASC,OAAOJ,GACH,KAAhBA,EAAKK,QAAkBD,OAAOE,SAASH,GACrCI,KAAKC,SAASL,GADgCI,KAAKC,SAASD,KAAKL,MAAO,mBAI/E,aAAAO,GACE,OAAOd,CAAI,6CAA6CY,KAAKG,mBAAmBC,OAAOJ,KAAKL,OAAS,aAAaK,KAAKT,SACzH,EAGFc,eAAeC,OAAO,mBAAoBhB","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import{html as e}from"/kempo-ui/lit-all.min.js";import t from"./SettingEditor.js";export default class s extends t{static properties={...t.properties,saved:{type:Boolean}};constructor(){super(),this.saved=!0,this.value=""}renderHint(){return e`${super.renderHint()}<small class="tc-muted d-b">${this.saved?"A value is saved. It is stored encrypted and never shown. Type a new one to replace it.":"Not set yet. It is stored encrypted and never shown again."}</small>`}renderControl(){return e`<input type="password" autocomplete="new-password" ?disabled=${this.disabled} .value=${this.value??""} @input=${e=>this.announce(e.target.value)}>`}}customElements.define("k-setting-secret",s);
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SecretSetting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\SecretSetting.js","names":["html","SettingEditor","SecretSetting","static","properties","saved","type","Boolean","constructor","super","this","value","renderHint","renderControl","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/*\n A secret setting (an API key, a signing secret). It is replace-only: the saved value is never sent to the browser, so the field starts\n empty and an empty field means \"keep what is saved\". Typing a new value announces it, and clearing the field again takes it back.\n*/\nexport default class SecretSetting extends SettingEditor {\n static properties = { ...SettingEditor.properties, saved: { type: Boolean } };\n\n constructor(){\n super();\n this.saved = true;\n this.value = '';\n }\n\n renderHint(){\n return html`${super.renderHint()}<small class=\"tc-muted d-b\">${this.saved ? 'A value is saved. It is stored encrypted and never shown. Type a new one to replace it.' : 'Not set yet. It is stored encrypted and never shown again.'}</small>`;\n }\n\n renderControl(){\n return html`<input type=\"password\" autocomplete=\"new-password\" ?disabled=${this.disabled} .value=${this.value ?? ''} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-secret', SecretSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAMX,MAAMC,UAAsBD,EACzCE,kBAAoB,IAAKF,EAAcG,WAAYC,MAAO,CAAEC,KAAMC,UAElE,WAAAC,GACEC,QACAC,KAAKL,OAAQ,EACbK,KAAKC,MAAQ,EACf,CAEA,UAAAC,GACE,OAAOZ,CAAI,GAAGS,MAAMG,2CAA2CF,KAAKL,MAAQ,0FAA4F,sEAC1K,CAEA,aAAAQ,GACE,OAAOb,CAAI,gEAAgEU,KAAKI,mBAAmBJ,KAAKC,OAAS,aAAaI,GAASL,KAAKM,SAASD,EAAME,OAAON,SACpK,EAGFO,eAAeC,OAAO,mBAAoBjB","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t}from"/kempo-ui/lit-all.min.js";import"./StringSetting.js";import"./NumberSetting.js";import"./BooleanSetting.js";import"./JsonSetting.js";import"./SecretSetting.js";export default class s extends e{static properties={setting:{attribute:!1},disabled:{type:Boolean}};constructor(){super(),this.setting=null,this.disabled=!1}render(){const{name:e,value:s,type:i,description:n}=this.setting??{};if(!e)return null;switch(i){case"number":return t`<k-setting-number name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-number>`;case"boolean":return t`<k-setting-boolean name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-boolean>`;case"json":return t`<k-setting-json name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-json>`;case"secret":return t`<k-setting-secret name=${e} description=${n??""} ?disabled=${this.disabled} .saved=${null!=s} .value=${""}></k-setting-secret>`;default:return t`<k-setting-string name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s??""}></k-setting-string>`}}}customElements.define("k-setting",s);
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\Setting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\Setting.js","names":["ShadowComponent","html","Setting","static","setting","attribute","disabled","type","Boolean","constructor","super","this","render","name","value","description","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport './StringSetting.js';\nimport './NumberSetting.js';\nimport './BooleanSetting.js';\nimport './JsonSetting.js';\nimport './SecretSetting.js';\n\n/*\n The editor for a setting, chosen by its type, served at /admin/components/settings/Setting.js:\n\n <k-setting .setting=${{ name: 'site_name', value: 'My Site', type: 'string', description: 'The name of the website' }}></k-setting>\n\n `name` is the setting's own name without its owner (`site_name`, not `system:site_name`); the readable title is made from it. An\n unknown type is edited as a string. The editor's `setting-change` event bubbles out of this element unchanged.\n*/\nexport default class Setting extends ShadowComponent {\n static properties = {\n setting: { attribute: false },\n disabled: { type: Boolean },\n };\n\n constructor(){\n super();\n this.setting = null;\n this.disabled = false;\n }\n\n render(){\n const { name, value, type, description } = this.setting ?? {};\n if(!name) return null;\n switch(type){\n case 'number': return html`<k-setting-number name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-number>`;\n case 'boolean': return html`<k-setting-boolean name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-boolean>`;\n case 'json': return html`<k-setting-json name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-json>`;\n case 'secret': return html`<k-setting-secret name=${name} description=${description ?? ''} ?disabled=${this.disabled} .saved=${value !== null && value !== undefined} .value=${''}></k-setting-secret>`;\n default: return html`<k-setting-string name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value ?? ''}></k-setting-string>`;\n }\n }\n}\n\ncustomElements.define('k-setting', Setting);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,2BACA,2BACA,4BACA,yBACA,oCAUQ,MAAMC,UAAgBF,EACnCG,kBAAoB,CAClBC,QAAS,CAAEC,WAAW,GACtBC,SAAU,CAAEC,KAAMC,UAGpB,WAAAC,GACEC,QACAC,KAAKP,QAAU,KACfO,KAAKL,UAAW,CAClB,CAEA,MAAAM,GACE,MAAMC,KAAEA,EAAIC,MAAEA,EAAKP,KAAEA,EAAIQ,YAAEA,GAAgBJ,KAAKP,SAAW,CAAC,EAC5D,IAAIS,EAAM,OAAO,KACjB,OAAON,GACL,IAAK,SAAU,OAAON,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,wBAC/H,IAAK,UAAW,OAAOb,CAAI,2BAA2BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,yBACjI,IAAK,OAAQ,OAAOb,CAAI,wBAAwBY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,sBAC3H,IAAK,SAAU,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,kBAAgD,yBAC/K,QAAS,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,GAAS,yBAEtI,EAGFE,eAAeC,OAAO,YAAaf","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as r}from"/kempo-ui/lit-all.min.js";import s from"./humanize.js";export default class i extends e{static properties={name:{type:String},label:{type:String},description:{type:String},value:{attribute:!1},disabled:{type:Boolean},problem:{state:!0}};constructor(){super(),this.name="",this.label="",this.description="",this.value="",this.disabled=!1,this.problem=""}get heading(){return this.label||s(this.name)}announce=(e,t="")=>{this.problem=t,this.dispatchEvent(new CustomEvent("setting-change",{detail:{name:this.name,value:e,valid:!t},bubbles:!0,composed:!0}))};renderControl(){return r}renderHint(){return this.description?t`<small class="tc-muted d-b">${this.description}</small>`:r}renderProblem(){return this.problem?t`<small class="tc-danger d-b" role="alert">${this.problem}</small>`:r}render(){return t`<label class="mb d-b">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`}}
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\SettingEditor.js","names":["ShadowComponent","html","nothing","humanize","SettingEditor","static","name","type","String","label","description","value","attribute","disabled","Boolean","problem","state","constructor","super","this","heading","announce","dispatchEvent","CustomEvent","detail","valid","bubbles","composed","renderControl","renderHint","renderProblem","render"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport humanize from './humanize.js';\n\n/*\n The base of the editors, one per setting type (StringSetting, NumberSetting, BooleanSetting, JsonSetting, SecretSetting). It draws\n the readable name, the description as a hint under it, and the problem if the value cannot be saved, and leaves the control itself\n to `renderControl()`.\n\n <k-setting-string name=\"site_name\" description=\"The name of the website\" .value=${'My Site'}></k-setting-string>\n\n A change is announced as a bubbling `setting-change` event with `{ name, value, valid }`; `value` is already the right type (a number\n is a Number, JSON is parsed). Use <k-setting> to get the editor for a type without choosing one.\n*/\nexport default class SettingEditor extends ShadowComponent {\n static properties = {\n name: { type: String },\n label: { type: String },\n description: { type: String },\n value: { attribute: false },\n disabled: { type: Boolean },\n problem: { state: true },\n };\n\n constructor(){\n super();\n this.name = '';\n this.label = '';\n this.description = '';\n this.value = '';\n this.disabled = false;\n this.problem = '';\n }\n\n /*\n Public\n */\n get heading(){\n return this.label || humanize(this.name);\n }\n\n /* Tell whoever is listening what the value is now. A `problem` sentence means it cannot be saved yet. */\n announce = (value, problem = '') => {\n this.problem = problem;\n this.dispatchEvent(new CustomEvent('setting-change', { detail: { name: this.name, value, valid: !problem }, bubbles: true, composed: true }));\n };\n\n /*\n Rendering\n */\n renderControl(){\n return nothing;\n }\n\n renderHint(){\n return this.description ? html`<small class=\"tc-muted d-b\">${this.description}</small>` : nothing;\n }\n\n renderProblem(){\n return this.problem ? html`<small class=\"tc-danger d-b\" role=\"alert\">${this.problem}</small>` : nothing;\n }\n\n render(){\n return html`<label class=\"mb d-b\">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`;\n }\n}\n"],"mappings":"OAAOA,MAAqB,yDACnBC,aAAMC,MAAe,kCACvBC,MAAc,+BAYN,MAAMC,UAAsBJ,EACzCK,kBAAoB,CAClBC,KAAM,CAAEC,KAAMC,QACdC,MAAO,CAAEF,KAAMC,QACfE,YAAa,CAAEH,KAAMC,QACrBG,MAAO,CAAEC,WAAW,GACpBC,SAAU,CAAEN,KAAMO,SAClBC,QAAS,CAAEC,OAAO,IAGpB,WAAAC,GACEC,QACAC,KAAKb,KAAO,GACZa,KAAKV,MAAQ,GACbU,KAAKT,YAAc,GACnBS,KAAKR,MAAQ,GACbQ,KAAKN,UAAW,EAChBM,KAAKJ,QAAU,EACjB,CAKA,WAAIK,GACF,OAAOD,KAAKV,OAASN,EAASgB,KAAKb,KACrC,CAGAe,SAAW,CAACV,EAAOI,EAAU,MAC3BI,KAAKJ,QAAUA,EACfI,KAAKG,cAAc,IAAIC,YAAY,iBAAkB,CAAEC,OAAQ,CAAElB,KAAMa,KAAKb,KAAMK,QAAOc,OAAQV,GAAWW,SAAS,EAAMC,UAAU,MAMvI,aAAAC,GACE,OAAO1B,CACT,CAEA,UAAA2B,GACE,OAAOV,KAAKT,YAAcT,CAAI,+BAA+BkB,KAAKT,sBAAwBR,CAC5F,CAEA,aAAA4B,GACE,OAAOX,KAAKJ,QAAUd,CAAI,6CAA6CkB,KAAKJ,kBAAoBb,CAClG,CAEA,MAAA6B,GACE,OAAO9B,CAAI,yBAAyBkB,KAAKC,UAAUD,KAAKU,eAAeV,KAAKS,0BAA0BT,KAAKW,iBAC7G","ignoreList":[]}
@@ -0,0 +1,24 @@
1
+ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Icon.js";import i from"/kempo-ui/components/Toast.js";import n from"/kempo-ui/components/Dialog.js";import{listSettings as a,setSetting as r,deleteSetting as o,getAllCurrentUserPermissions as l}from"/kempo/sdk.js";import"./Setting.js";import c from"./humanize.js";const u={system:"system:settings:update",custom:"system:custom-settings:manage"};let h=null;const d={string:"",number:0,boolean:!1,json:{},secret:""},m=e=>{if(null===e.value||void 0===e.value||"secret"===e.type)return e;try{if("number"===e.type)return{...e,value:Number(e.value)};if("boolean"===e.type)return{...e,value:!0===e.value||"true"===e.value};if("json"===e.type)return{...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value}}catch{return e}return e};export default class p extends e{static properties={owner:{type:String},heading:{type:String},settings:{state:!0},changes:{state:!0},canEdit:{state:!0},loading:{state:!0},busy:{state:!0},error:{state:!0}};constructor(){super(),this.owner="",this.heading="",this.settings=[],this.changes={},this.canEdit=!1,this.loading=!0,this.busy=!1,this.error=""}connectedCallback(){super.connectedCallback(),this.load()}shortName=e=>e.name.slice(this.owner.length+1);load=async()=>{const[[e,t],s]=await Promise.all([a({owner:this.owner,limit:1e3}),h??=l().then(([,e])=>new Set(e?.permissions??[]))]);this.canEdit=s.has(u[this.owner]??"system:extensions:manage"),this.loading=!1,e?this.error=e.msg||"The settings could not be loaded":(this.error="",this.settings=t.settings.map(m),this.changes={})};record=e=>{const{name:t,value:s,valid:i}=e.detail,n=this.settings.find(e=>this.shortName(e)===t),a=n&&("secret"===n.type?""===s:JSON.stringify(s)===JSON.stringify(n.value)),{[t]:r,...o}=this.changes;this.changes=a&&i?o:{...o,[t]:{value:s,valid:i}}};get dirty(){return Object.keys(this.changes).length>0}get blocked(){return Object.values(this.changes).some(e=>!e.valid)}save=async()=>{this.busy=!0;let e=0;for(const[t,s]of Object.entries(this.changes)){const n=this.settings.find(e=>this.shortName(e)===t),[a]=await r(this.owner,t,{value:s.value,type:n.type,isPublic:n.isPublic,description:n.description});a&&(e++,i.error(`${c(t)}: ${a.msg||"could not be saved"}`))}this.busy=!1,e||i.success("Settings saved"),await this.load()};create=()=>{const e=n.create(t`
2
+ <div class="p">
3
+ <label class="mb d-b">Name<input type="text" id="newName" placeholder="my_setting" required></label>
4
+ <label class="mb d-b">Type
5
+ <select id="newType">
6
+ <option value="string">String</option>
7
+ <option value="number">Number</option>
8
+ <option value="boolean">Boolean (true or false)</option>
9
+ <option value="json">JSON</option>
10
+ <option value="secret">Secret</option>
11
+ </select>
12
+ </label>
13
+ <label class="mb d-b">Value<input type="text" id="newValue"></label>
14
+ <label class="mb d-b">Description<input type="text" id="newDescription"></label>
15
+ <label class="mb d-b"><input type="checkbox" id="newPublic"> Public (anyone can read it, without signing in)</label>
16
+ </div>
17
+ `,{title:"Create a setting",confirmText:"Create",cancelText:"Cancel",confirmAction:async()=>{const t=t=>e.querySelector(t),s=t("#newName").value.trim(),n=t("#newType").value;if(!s)return void i.error("Give the setting a name");if(s.includes(":"))return void i.error('The name cannot contain ":"');let a;try{a=""===t("#newValue").value?d[n]:((e,t)=>{if("number"===t){const t=Number(e);if(""===String(e).trim()||!Number.isFinite(t))throw new Error("number");return t}return"boolean"===t?"true"===String(e).toLowerCase():"json"===t?JSON.parse(String(e||"{}")):String(e)})(t("#newValue").value,n)}catch{return void i.error(`That is not a valid ${n} value`)}const[o]=await r("custom",s,{value:a,type:n,isPublic:"secret"!==n&&t("#newPublic").checked,description:t("#newDescription").value.trim()});o?i.error(o.msg||"The setting could not be created"):(i.success("Setting created"),await this.load())}});e.querySelector("#newType").addEventListener("change",t=>{e.querySelector("#newValue").type="secret"===t.target.value?"password":"text"})};remove=e=>{n.confirm(`Delete ${c(this.shortName(e))}? This cannot be undone.`,async t=>{if(!t)return;const[s]=await o(this.owner,this.shortName(e));s?i.error(s.msg||"The setting could not be deleted"):(i.success("Setting deleted"),await this.load())},{title:"Delete setting",confirmText:"Delete",confirmClasses:"danger ml",cancelText:"Cancel",cancelClasses:""})};renderSetting(e){const s=this.shortName(e),i=Object.hasOwn(this.changes,s)&&this.changes[s].valid?this.changes[s].value:e.value,n=t`<k-setting .setting=${{name:s,value:i,type:e.type,description:e.description}} ?disabled=${!this.canEdit}></k-setting>`;return"custom"===this.owner&&this.canEdit?t`<div class="d-f"><div class="flex">${n}</div><div><button class="danger" aria-label=${`Delete ${c(s)}`} title="Delete" @click=${()=>this.remove(e)}><k-icon name="delete"></k-icon></button></div></div>`:n}render(){return this.loading?t`<k-spinner></k-spinner>`:this.error?t`<p class="tc-danger" role="alert">${this.error}</p>`:t`
18
+ ${this.heading?t`<h2 class="h4">${this.heading}</h2>`:s}
19
+ ${"custom"===this.owner&&this.canEdit?t`<p class="mb"><button @click=${this.create}><k-icon name="add"></k-icon> Create a setting</button></p>`:s}
20
+ ${this.canEdit?s:t`<p class="tc-muted mb">You can see these settings but not change them.</p>`}
21
+ ${this.settings.length?this.settings.map(e=>this.renderSetting(e)):t`<p class="tc-muted mb">${"custom"===this.owner?"There are no custom settings yet.":"There are no settings here."}</p>`}
22
+ ${this.canEdit&&this.settings.length?t`<button class="primary" ?disabled=${!this.dirty||this.blocked||this.busy} @click=${this.save}>${this.busy?"Saving…":"Save settings"}</button>`:s}
23
+ `}firstUpdated(){this.renderRoot.addEventListener("setting-change",this.record)}}customElements.define("k-settings-panel",p);
24
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingsPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\SettingsPanel.js","names":["ShadowComponent","html","nothing","Toast","Dialog","listSettings","setSetting","deleteSetting","getAllCurrentUserPermissions","humanize","PERMISSIONS","system","custom","permissionsPromise","DEFAULTS","string","number","boolean","json","secret","typed","record","value","undefined","type","Number","JSON","parse","SettingsPanel","static","owner","String","heading","settings","state","changes","canEdit","loading","busy","error","constructor","super","this","connectedCallback","load","shortName","name","slice","length","async","data","permissions","Promise","all","limit","then","Set","has","msg","map","event","valid","detail","find","entry","unchanged","stringify","removed","rest","dirty","Object","keys","blocked","values","some","change","save","failed","entries","isPublic","description","success","create","dialog","title","confirmText","cancelText","confirmAction","read","id","querySelector","trim","includes","text","isFinite","Error","toLowerCase","coerce","checked","addEventListener","target","remove","confirm","confirmed","confirmClasses","cancelClasses","renderSetting","hasOwn","editor","render","firstUpdated","renderRoot","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Icon.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';\nimport './Setting.js';\nimport humanize from './humanize.js';\n\n/*\n The settings of one owner, each with the editor for its type, and a Save button for what was changed. Served at\n /admin/components/settings/SettingsPanel.js:\n\n <k-settings-panel owner=\"system\"></k-settings-panel>\n <k-settings-panel owner=\"my-extension\" heading=\"My Extension\"></k-settings-panel>\n\n `owner` is `system`, `custom` or the name of an extension. The Custom panel also creates and deletes settings. An extension drops\n one of these in its own settings tab and needs no editor of its own. Whether the person can change them depends on who owns\n them (`system:settings:update`, `system:custom-settings:manage`, `system:extensions:manage`); without it the fields are read-only.\n Saving sends only what was changed, each as its own setting.\n*/\nconst PERMISSIONS = { system: 'system:settings:update', custom: 'system:custom-settings:manage' };\nconst EXTENSION_PERMISSION = 'system:extensions:manage';\n\nlet permissionsPromise = null;\nconst myPermissions = () => (permissionsPromise ??= getAllCurrentUserPermissions().then(([, data]) => new Set(data?.permissions ?? [])));\n\nconst DEFAULTS = { string: '', number: 0, boolean: false, json: {}, secret: '' };\n\nconst coerce = (text, type) => {\n if(type === 'number'){\n const number = Number(text);\n if(String(text).trim() === '' || !Number.isFinite(number)) throw new Error('number');\n return number;\n }\n if(type === 'boolean') return String(text).toLowerCase() === 'true';\n if(type === 'json') return JSON.parse(String(text || '{}'));\n return String(text);\n};\n\n/*\n The settings API gives each value as it is stored, as text (`'true'`, `'7'`, a JSON string). The editors take real values, so the value\n is turned into the type the setting has. A secret is left as the mask the API sends: it only says that one is saved.\n*/\nconst typed = record => {\n if(record.value === null || record.value === undefined || record.type === 'secret') return record;\n try {\n if(record.type === 'number') return { ...record, value: Number(record.value) };\n if(record.type === 'boolean') return { ...record, value: record.value === true || record.value === 'true' };\n if(record.type === 'json') return { ...record, value: typeof record.value === 'string' ? JSON.parse(record.value) : record.value };\n } catch {\n return record;\n }\n return record;\n};\n\nexport default class SettingsPanel extends ShadowComponent {\n static properties = {\n owner: { type: String },\n heading: { type: String },\n settings: { state: true },\n changes: { state: true },\n canEdit: { state: true },\n loading: { state: true },\n busy: { state: true },\n error: { state: true },\n };\n\n constructor(){\n super();\n this.owner = '';\n this.heading = '';\n this.settings = [];\n this.changes = {};\n this.canEdit = false;\n this.loading = true;\n this.busy = false;\n this.error = '';\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Loading and saving\n */\n shortName = record => record.name.slice(this.owner.length + 1);\n\n load = async () => {\n const [[error, data], permissions] = await Promise.all([listSettings({ owner: this.owner, limit: 1000 }), myPermissions()]);\n this.canEdit = permissions.has(PERMISSIONS[this.owner] ?? EXTENSION_PERMISSION);\n this.loading = false;\n if(error){\n this.error = error.msg || 'The settings could not be loaded';\n return;\n }\n this.error = '';\n this.settings = data.settings.map(typed);\n this.changes = {};\n };\n\n record = event => {\n const { name, value, valid } = event.detail;\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const unchanged = record && (record.type === 'secret' ? value === '' : JSON.stringify(value) === JSON.stringify(record.value));\n const { [name]: removed, ...rest } = this.changes;\n this.changes = unchanged && valid ? rest : { ...rest, [name]: { value, valid } };\n };\n\n get dirty(){\n return Object.keys(this.changes).length > 0;\n }\n\n get blocked(){\n return Object.values(this.changes).some(change => !change.valid);\n }\n\n save = async () => {\n this.busy = true;\n let failed = 0;\n for(const [name, change] of Object.entries(this.changes)){\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const [error] = await setSetting(this.owner, name, { value: change.value, type: record.type, isPublic: record.isPublic, description: record.description });\n if(error){\n failed++;\n Toast.error(`${humanize(name)}: ${error.msg || 'could not be saved'}`);\n }\n }\n this.busy = false;\n if(!failed) Toast.success('Settings saved');\n await this.load();\n };\n\n /*\n Custom settings: create and delete\n */\n create = () => {\n const dialog = Dialog.create(html`\n <div class=\"p\">\n <label class=\"mb d-b\">Name<input type=\"text\" id=\"newName\" placeholder=\"my_setting\" required></label>\n <label class=\"mb d-b\">Type\n <select id=\"newType\">\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Boolean (true or false)</option>\n <option value=\"json\">JSON</option>\n <option value=\"secret\">Secret</option>\n </select>\n </label>\n <label class=\"mb d-b\">Value<input type=\"text\" id=\"newValue\"></label>\n <label class=\"mb d-b\">Description<input type=\"text\" id=\"newDescription\"></label>\n <label class=\"mb d-b\"><input type=\"checkbox\" id=\"newPublic\"> Public (anyone can read it, without signing in)</label>\n </div>\n `, {\n title: 'Create a setting',\n confirmText: 'Create',\n cancelText: 'Cancel',\n confirmAction: async () => {\n const read = id => dialog.querySelector(id);\n const name = read('#newName').value.trim();\n const type = read('#newType').value;\n if(!name){ Toast.error('Give the setting a name'); return; }\n if(name.includes(':')){ Toast.error('The name cannot contain \":\"'); return; }\n let value;\n try {\n value = read('#newValue').value === '' ? DEFAULTS[type] : coerce(read('#newValue').value, type);\n } catch {\n Toast.error(`That is not a valid ${type} value`);\n return;\n }\n const [error] = await setSetting('custom', name, { value, type, isPublic: type !== 'secret' && read('#newPublic').checked, description: read('#newDescription').value.trim() });\n if(error){ Toast.error(error.msg || 'The setting could not be created'); return; }\n Toast.success('Setting created');\n await this.load();\n },\n });\n dialog.querySelector('#newType').addEventListener('change', event => {\n dialog.querySelector('#newValue').type = event.target.value === 'secret' ? 'password' : 'text';\n });\n };\n\n remove = record => {\n Dialog.confirm(`Delete ${humanize(this.shortName(record))}? This cannot be undone.`, async confirmed => {\n if(!confirmed) return;\n const [error] = await deleteSetting(this.owner, this.shortName(record));\n if(error){ Toast.error(error.msg || 'The setting could not be deleted'); return; }\n Toast.success('Setting deleted');\n await this.load();\n }, { title: 'Delete setting', confirmText: 'Delete', confirmClasses: 'danger ml', cancelText: 'Cancel', cancelClasses: '' });\n };\n\n /*\n Rendering\n */\n renderSetting(record){\n const name = this.shortName(record);\n const value = Object.hasOwn(this.changes, name) && this.changes[name].valid ? this.changes[name].value : record.value;\n const editor = html`<k-setting .setting=${{ name, value, type: record.type, description: record.description }} ?disabled=${!this.canEdit}></k-setting>`;\n if(this.owner !== 'custom' || !this.canEdit) return editor;\n return html`<div class=\"d-f\"><div class=\"flex\">${editor}</div><div><button class=\"danger\" aria-label=${`Delete ${humanize(name)}`} title=\"Delete\" @click=${() => this.remove(record)}><k-icon name=\"delete\"></k-icon></button></div></div>`;\n }\n\n render(){\n if(this.loading) return html`<k-spinner></k-spinner>`;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n return html`\n ${this.heading ? html`<h2 class=\"h4\">${this.heading}</h2>` : nothing}\n ${this.owner === 'custom' && this.canEdit ? html`<p class=\"mb\"><button @click=${this.create}><k-icon name=\"add\"></k-icon> Create a setting</button></p>` : nothing}\n ${this.canEdit ? nothing : html`<p class=\"tc-muted mb\">You can see these settings but not change them.</p>`}\n ${this.settings.length ? this.settings.map(record => this.renderSetting(record)) : html`<p class=\"tc-muted mb\">${this.owner === 'custom' ? 'There are no custom settings yet.' : 'There are no settings here.'}</p>`}\n ${this.canEdit && this.settings.length ? html`<button class=\"primary\" ?disabled=${!this.dirty || this.blocked || this.busy} @click=${this.save}>${this.busy ? 'Saving…' : 'Save settings'}</button>` : nothing}\n `;\n }\n\n firstUpdated(){\n this.renderRoot.addEventListener('setting-change', this.record);\n }\n}\n\ncustomElements.define('k-settings-panel', SettingsPanel);\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import{html as t}from"/kempo-ui/lit-all.min.js";import e from"./SettingEditor.js";export default class i extends e{renderControl(){const e=String(this.value??"");return e.length>120||e.includes("\n")?t`<textarea rows="4" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}></textarea>`:t`<input type="text" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}>`}}customElements.define("k-setting-string",i);
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\StringSetting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\StringSetting.js","names":["html","SettingEditor","StringSetting","renderControl","text","String","this","value","length","includes","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A string setting: a text field, or a larger box for one that is long or has several lines. */\nexport default class StringSetting extends SettingEditor {\n renderControl(){\n const text = String(this.value ?? '');\n if(text.length > 120 || text.includes('\\n')) return html`<textarea rows=\"4\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}></textarea>`;\n return html`<input type=\"text\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-string', StringSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzC,aAAAE,GACE,MAAMC,EAAOC,OAAOC,KAAKC,OAAS,IAClC,OAAGH,EAAKI,OAAS,KAAOJ,EAAKK,SAAS,MAAcT,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,qBAC5JP,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,SACxH,EAGFO,eAAeC,OAAO,mBAAoBb","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ const s=new Set(["api","css","html","id","ip","js","json","seo","smtp","sms","sql","ssl","tls","ui","uri","url","utc","uuid","xml"]);export default e=>String(e??"").replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[\s_\-.:]+/).filter(Boolean).map(e=>s.has(e.toLowerCase())?e.toUpperCase():e[0].toUpperCase()+e.slice(1)).join(" ");
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\humanize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\settings\\humanize.js","names":["ACRONYMS","Set","name","String","replace","split","filter","Boolean","map","word","has","toLowerCase","toUpperCase","slice","join"],"sources":["0"],"sourcesContent":["/*\n A readable name for a setting, made from its name, so nobody has to write one:\n\n session_duration_days -> Session Duration Days\n verification_url -> Verification URL\n shippoCacheMinutes -> Shippo Cache Minutes\n\n Words are split on `_`, `-`, `.`, `:` and spaces and where a lowercase letter meets a capital. Each gets a capital, and the\n well-known abbreviations are written in capitals.\n*/\nconst ACRONYMS = new Set(['api', 'css', 'html', 'id', 'ip', 'js', 'json', 'seo', 'smtp', 'sms', 'sql', 'ssl', 'tls', 'ui', 'uri', 'url', 'utc', 'uuid', 'xml']);\n\nexport default name => String(name ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .split(/[\\s_\\-.:]+/)\n .filter(Boolean)\n .map(word => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))\n .join(' ');\n"],"mappings":"AAUA,MAAMA,EAAW,IAAIC,IAAI,CAAC,MAAO,MAAO,OAAQ,KAAM,KAAM,KAAM,OAAQ,MAAO,OAAQ,MAAO,MAAO,MAAO,MAAO,KAAM,MAAO,MAAO,MAAO,OAAQ,uBAEzIC,GAAQC,OAAOD,GAAQ,IACnCE,QAAQ,qBAAsB,SAC9BC,MAAM,cACNC,OAAOC,SACPC,IAAIC,GAAST,EAASU,IAAID,EAAKE,eAAiBF,EAAKG,cAAgBH,EAAK,GAAGG,cAAgBH,EAAKI,MAAM,IACxGC,KAAK","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ <!--
2
+ locked: true
3
+ -->
4
+ <page title="Emails - Admin">
5
+ <content>
6
+ <script type="module" src="/admin/components/Emails.js"></script>
7
+
8
+ <h1>Emails</h1>
9
+ <p class="tc-muted mb">Every email the site was asked to send, and what happened to it. Kempo does not send email itself: an email extension (such as kempo-resend or kempo-smtp) does. How long this log is kept, and how many times a failed email is tried again, are on the System tab of <a href="/admin/settings">Settings</a>.</p>
10
+
11
+ <k-admin-emails></k-admin-emails>
12
+
13
+ </content>
14
+ </page>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
@@ -84,6 +84,18 @@
84
84
  group="Settings"
85
85
  no-expand
86
86
  >Extensions</k-aside-item>
87
+ <k-aside-item
88
+ icon="mail"
89
+ href="/admin/emails"
90
+ group="Settings"
91
+ no-expand
92
+ >Emails</k-aside-item>
93
+ <k-aside-item
94
+ icon="deployed_code_update"
95
+ href="/admin/updates"
96
+ group="Settings"
97
+ no-expand
98
+ >Updates</k-aside-item>
87
99
  <location name="admin-nav-extensions"></location>
88
100
  <k-aside-item
89
101
  icon="sensors"
@@ -88,7 +88,7 @@
88
88
  ? myPerms.has('system:settings:update')
89
89
  : owner === 'custom'
90
90
  ? myPerms.has('system:custom-settings:manage')
91
- : myPerms.has('system:extension:configure');
91
+ : myPerms.has('system:extensions:manage');
92
92
  const canEditMetadata = owner === 'custom' && myPerms.has('system:custom-settings:manage');
93
93
  const canDelete = owner === 'custom' && myPerms.has('system:custom-settings:manage');
94
94
 
@@ -3,297 +3,14 @@
3
3
  -->
4
4
  <page title="Settings - Admin">
5
5
  <content>
6
- <script type="module" src="/kempo-ui/components/Table.js"></script>
7
- <script type="module" src="/kempo-ui/components/controls/TcFirstPage.js"></script>
8
- <script type="module" src="/kempo-ui/components/controls/TcPrevPage.js"></script>
9
- <script type="module" src="/kempo-ui/components/controls/TcNextPage.js"></script>
10
- <script type="module" src="/kempo-ui/components/controls/TcLastPage.js"></script>
11
- <script type="module" src="/kempo-ui/components/controls/TcPageSelect.js"></script>
12
- <script type="module" src="/kempo-ui/components/controls/TcPageSize.js"></script>
13
- <script type="module" src="/kempo-ui/components/controls/TcEdit.js"></script>
14
- <script type="module" src="/kempo-ui/components/controls/TcDeleteRecord.js"></script>
15
- <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
16
- <script type="module" src="/admin/components/TableRowLink.js"></script>
17
- <script type="module" src="/kempo/components/Permission.js"></script>
6
+ <script type="module" src="/admin/components/settings/AdminSettings.js"></script>
18
7
 
19
- <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
20
- <h1 class="flex mb0">Settings</h1>
21
- <k-permission has="system:custom-settings:manage">
22
- <button id="createSettingBtn" class="primary">Create Setting</button>
23
- </k-permission>
24
- </div>
8
+ <h1>Settings</h1>
25
9
 
26
- <h2>Custom Settings</h2>
27
- <k-table id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom settings have been created yet.">
28
- <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
29
- <div slot="top" class="d-f full" style="align-items: center;">
30
- <div>
31
- <k-permission has="system:custom-settings:manage">
32
- <kc-tc-delete-selected></kc-tc-delete-selected>
33
- </k-permission>
34
- </div>
35
- </div>
36
- <div class="d-f full" style="align-items: center;">
37
- <div>
38
- <kc-tc-first-page></kc-tc-first-page>
39
- <kc-tc-prev-page></kc-tc-prev-page>
40
- <kc-tc-next-page></kc-tc-next-page>
41
- <kc-tc-last-page></kc-tc-last-page>
42
- </div>
43
- <div class="flex"></div>
44
- <div><kc-tc-page-select></kc-tc-page-select></div>
45
- <div class="flex"></div>
46
- <div><kc-tc-page-size></kc-tc-page-size></div>
47
- </div>
48
- </k-table>
49
-
50
- <h2>Extension Settings</h2>
51
- <k-table id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension settings installed.">
52
- <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
53
- <div class="d-f full" style="align-items: center;">
54
- <div>
55
- <kc-tc-first-page></kc-tc-first-page>
56
- <kc-tc-prev-page></kc-tc-prev-page>
57
- <kc-tc-next-page></kc-tc-next-page>
58
- <kc-tc-last-page></kc-tc-last-page>
59
- </div>
60
- <div class="flex"></div>
61
- <div><kc-tc-page-select></kc-tc-page-select></div>
62
- <div class="flex"></div>
63
- <div><kc-tc-page-size></kc-tc-page-size></div>
64
- </div>
65
- </k-table>
66
-
67
- <h2>System Settings</h2>
68
- <k-table id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system settings found.">
69
- <admin-table-row-link slot="before" url-pattern="{_link}" field-name="_link"></admin-table-row-link>
70
- <div class="d-f full" style="align-items: center;">
71
- <div>
72
- <kc-tc-first-page></kc-tc-first-page>
73
- <kc-tc-prev-page></kc-tc-prev-page>
74
- <kc-tc-next-page></kc-tc-next-page>
75
- <kc-tc-last-page></kc-tc-last-page>
76
- </div>
77
- <div class="flex"></div>
78
- <div><kc-tc-page-select></kc-tc-page-select></div>
79
- <div class="flex"></div>
80
- <div><kc-tc-page-size></kc-tc-page-size></div>
81
- </div>
82
- </k-table>
83
-
84
- <script type="module">
85
- import { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';
86
- import { html } from '/kempo-ui/lit-all.min.js';
87
- import Toast from '/kempo-ui/components/Toast.js';
88
- import Dialog from '/kempo-ui/components/Dialog.js';
89
-
90
- await window.customElements.whenDefined('k-table');
91
-
92
- const [, permData] = await getAllCurrentUserPermissions();
93
- const myPerms = new Set(permData?.permissions ?? []);
94
- const canManageSystem = myPerms.has('system:settings:update');
95
- const canManageCustom = myPerms.has('system:custom-settings:manage');
96
-
97
- const buildFields = (editable, showOwner = false) => [
98
- ...(showOwner ? [{ name: 'owner', label: 'Owner', size: 120, editable: false, calculator: r => r.name.split(':')[0] }] : []),
99
- { name: 'name', label: 'Name', size: 220, editable: false, calculator: r => r.name.split(':').slice(1).join(':') },
100
- { name: 'value', label: 'Value', size: 280, editable, formatter: v => {
101
- if(v === null || v === undefined) return '';
102
- const s = String(v);
103
- return s.length > 120 ? s.slice(0, 117) + '...' : s;
104
- }},
105
- { name: 'type', label: 'Type', size: 90, editable },
106
- { name: 'isPublic', label: 'Public', size: 80, editable, formatter: v => String(v) },
107
- { name: 'description', label: 'Description', size: 320, editable },
108
- { name: 'updatedAt', label: 'Updated', size: 190, editable: false, formatter: v => new Date(v).toLocaleString() }
109
- ];
110
-
111
- const coerceValue = (value, type) => {
112
- if(type === 'number') return Number(value);
113
- if(type === 'boolean') return String(value).toLowerCase() === 'true';
114
- if(type === 'json') return JSON.parse(String(value));
115
- return String(value);
116
- };
117
-
118
- const setupTable = ($table, owner, canEdit, loadFn) => {
119
- if(canEdit){
120
- $table.setAttribute('request-edit', '');
121
- const edit = document.createElement('kc-tc-edit');
122
- edit.setAttribute('slot', 'after');
123
- $table.appendChild(edit);
124
- }
125
-
126
- if(owner === 'custom' && canEdit){
127
- $table.setAttribute('request-delete', '');
128
- $table.setAttribute('enable-selection', '');
129
- const del = document.createElement('kc-tc-delete-record');
130
- del.setAttribute('slot', 'after');
131
- $table.appendChild(del);
132
- }
133
-
134
- $table.addEventListener('requestSave', async ({ detail: { record, newData, approve, reject } }) => {
135
- const type = newData.type ?? record.type;
136
- let value = newData.value !== undefined ? newData.value : record.value;
137
-
138
- try {
139
- value = coerceValue(value, type);
140
- } catch(e){
141
- reject();
142
- Toast.error(`Invalid ${type} value`);
143
- return;
144
- }
145
-
146
- const isPublic = newData.isPublic !== undefined
147
- ? String(newData.isPublic).toLowerCase() === 'true'
148
- : record.isPublic;
149
- const [recordOwner, ...nameParts] = record.name.split(':');
150
- const name = nameParts.join(':');
151
- const [error] = await setSetting(recordOwner, name, {
152
- value,
153
- type,
154
- isPublic,
155
- description: newData.description ?? record.description
156
- });
157
-
158
- if(error){ reject(); Toast.error(error.msg || 'Failed to update setting'); return; }
159
- approve();
160
- Toast.success('Setting updated');
161
- });
162
-
163
- if(owner === 'custom' && canEdit){
164
- $table.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
165
- const displayName = r => r.name.split(':').slice(1).join(':');
166
- const count = records.length;
167
- const msg = count === 1
168
- ? `<div class="p"><p>Delete "${displayName(records[0])}"? This action cannot be undone.</p></div>`
169
- : `<div class="p"><p>Delete ${count} settings? This action cannot be undone.</p></div>`;
170
- Dialog.confirm(msg, async (confirmed) => {
171
- if(!confirmed){ reject(); return; }
172
- let failed = 0;
173
- for(const record of records){
174
- const [recOwner, ...nameParts] = record.name.split(':');
175
- const [error] = await deleteSetting(recOwner, nameParts.join(':'));
176
- if(error) failed++;
177
- }
178
- if(failed){
179
- reject();
180
- Toast.error(`Failed to delete ${failed} setting${failed !== 1 ? 's' : ''}`);
181
- return;
182
- }
183
- approve();
184
- Toast.success(count === 1 ? 'Setting deleted' : `${count} settings deleted`);
185
- loadFn();
186
- });
187
- });
188
- }
189
- };
190
-
191
- const addLinks = settings => {
192
- settings.forEach(s => {
193
- const [o, ...parts] = s.name.split(':');
194
- s._link = `/admin/settings/${encodeURIComponent(o)}/${encodeURIComponent(parts.join(':'))}/`;
195
- });
196
- };
197
-
198
- const loadTable = async ($table, owner, canEdit) => {
199
- const [error, data] = await listSettings({ owner });
200
- if(error){ Toast.error(error.msg || 'Failed to load settings'); return; }
201
- addLinks(data.settings);
202
- $table.setData({ records: data.settings, fields: buildFields(canEdit, owner === 'extension') });
203
- $table.setupFetchRecords(data.total, async (start, count) => {
204
- const [err, d] = await listSettings({ owner, limit: count, offset: start });
205
- if(err) return [];
206
- addLinks(d.settings);
207
- return d.settings;
208
- });
209
- };
210
-
211
- const $customTable = document.getElementById('customTable');
212
- const $extensionTable = document.getElementById('extensionTable');
213
- const $systemTable = document.getElementById('systemTable');
214
-
215
- setupTable($customTable, 'custom', canManageCustom, () => loadTable($customTable, 'custom', canManageCustom));
216
- setupTable($extensionTable, 'extension', false, () => loadTable($extensionTable, 'extension', false));
217
- setupTable($systemTable, 'system', canManageSystem, () => loadTable($systemTable, 'system', canManageSystem));
218
-
219
- await Promise.all([
220
- loadTable($customTable, 'custom', canManageCustom),
221
- loadTable($extensionTable, 'extension', false),
222
- loadTable($systemTable, 'system', canManageSystem)
223
- ]);
224
-
225
- if(canManageCustom){
226
- document.getElementById('createSettingBtn').addEventListener('click', () => {
227
- const $dialog = Dialog.create(html`
228
- <div class="p">
229
- <div class="mb">
230
- <label class="d-b mb-sm">Name</label>
231
- <input type="text" id="dlg-name" class="full" placeholder="e.g. my-setting" required>
232
- </div>
233
- <div class="mb">
234
- <label class="d-b mb-sm">Value</label>
235
- <input type="text" id="dlg-value" class="full">
236
- </div>
237
- <div class="mb">
238
- <label class="d-b mb-sm">Type</label>
239
- <select id="dlg-type" class="full">
240
- <option value="string">String</option>
241
- <option value="number">Number</option>
242
- <option value="boolean">Boolean</option>
243
- <option value="json">JSON</option>
244
- <option value="secret">Secret</option>
245
- </select>
246
- </div>
247
- <div class="mb d-f" style="align-items: center; gap: var(--spacer-sm);" id="dlg-isPublic-row">
248
- <input type="checkbox" id="dlg-isPublic">
249
- <label for="dlg-isPublic">Public (accessible without authentication)</label>
250
- </div>
251
- <div>
252
- <label class="d-b mb-sm">Description</label>
253
- <input type="text" id="dlg-description" class="full">
254
- </div>
255
- </div>
256
- `, {
257
- title: 'Create Setting',
258
- confirmText: 'Create',
259
- confirmAction: async () => {
260
- const name = $dialog.querySelector('#dlg-name').value.trim();
261
- if(!name){ Toast.error('Name is required'); return; }
262
- if(name.includes(':')){ Toast.error('Name cannot contain ":"'); return; }
263
-
264
- const rawValue = $dialog.querySelector('#dlg-value').value;
265
- const type = $dialog.querySelector('#dlg-type').value;
266
- const isPublic = $dialog.querySelector('#dlg-isPublic').checked;
267
- const description = $dialog.querySelector('#dlg-description').value.trim();
268
-
269
- let value;
270
- try {
271
- value = coerceValue(rawValue, type);
272
- } catch(e){
273
- Toast.error(`Invalid ${type} value`);
274
- return;
275
- }
276
-
277
- const [error] = await setSetting('custom', name, { value, type, isPublic, description });
278
- if(error){ Toast.error(error.msg || 'Failed to create setting'); return; }
279
- Toast.success('Setting created');
280
- loadTable($customTable, 'custom', canManageCustom);
281
- },
282
- cancelText: 'Cancel'
283
- });
284
-
285
- const $dlgValue = $dialog.querySelector('#dlg-value');
286
- const $dlgIsPublic = $dialog.querySelector('#dlg-isPublic');
287
- const $dlgIsPublicRow = $dialog.querySelector('#dlg-isPublic-row');
288
- $dialog.querySelector('#dlg-type').addEventListener('change', e => {
289
- const isSecret = e.target.value === 'secret';
290
- $dlgValue.type = isSecret ? 'password' : 'text';
291
- $dlgIsPublicRow.style.display = isSecret ? 'none' : '';
292
- if(isSecret) $dlgIsPublic.checked = false;
293
- });
294
- });
295
- }
296
- </script>
10
+ <k-admin-settings>
11
+ <location name="admin-settings-tabs" />
12
+ </k-admin-settings>
13
+ <location name="admin-settings-scripts" />
297
14
 
298
15
  </content>
299
16
  </page>
@@ -0,0 +1,14 @@
1
+ <!--
2
+ locked: true
3
+ -->
4
+ <page title="Updates - Admin">
5
+ <content>
6
+ <script type="module" src="/admin/components/Updates.js"></script>
7
+
8
+ <h1>Updates</h1>
9
+ <p class="tc-muted mb">Kempo checks npm for a newer version and tells you here and in your notifications. Installing one downloads it and restarts the server.</p>
10
+
11
+ <k-admin-updates></k-admin-updates>
12
+
13
+ </content>
14
+ </page>
@@ -0,0 +1,2 @@
1
+ import s from"../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../server/utils/email/listEmailTemplates.js";export default async(r,t)=>{const[o,i]=await s(r.cookies.session_token,"system:pages:read");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const[n,a]=await e({owner:r.query.owner||null});if(n)return t.status(n.code).json({error:n.msg});t.json({templates:a})};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\email-templates\GET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\email-templates\\GET.js","names":["currentUserHasPermission","listEmailTemplates","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","templates","owner","query"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\nimport listEmailTemplates from '../../../../server/utils/email/listEmailTemplates.js';\n\n/* The email templates of the extension named in `?owner=`, or kempo's own and the site's when there is none. */\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:read');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst [error, templates] = await listEmailTemplates({ owner: request.query.owner || null });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.json({ templates });\n};\n"],"mappings":"OAAOA,MAA8B,0EAC9BC,MAAwB,sEAGhBC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,qBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAOA,EAAOE,SAAmBb,EAAmB,CAAEc,MAAOZ,EAAQa,MAAMD,OAAS,OACpF,GAAGH,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASO,KAAK,CAAEG","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../server/utils/email/resetEmailTemplate.js";export default async(r,t)=>{const[o,i]=await s(r.cookies.session_token,"system:pages:update");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const[n,a]=await e({template:r.body?.template});if(n)return t.status(n.code).json({error:n.msg});t.json(a)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\email-templates\file\DELETE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\email-templates\\file\\DELETE.js","names":["currentUserHasPermission","resetEmailTemplate","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","reset","template","body"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport resetEmailTemplate from '../../../../../server/utils/email/resetEmailTemplate.js';\n\n/* Goes back to the packaged version of an email template, by removing the site's own. Body: `{ template }`. */\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:update');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst [error, reset] = await resetEmailTemplate({ template: request.body?.template });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.json(reset);\n};\n"],"mappings":"OAAOA,MAA8B,6EAC9BC,MAAwB,yEAGhBC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,uBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAOA,EAAOE,SAAeb,EAAmB,CAAEc,SAAUZ,EAAQa,MAAMD,WAC1E,GAAGH,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASO,KAAKG","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../server/utils/email/readEmailTemplate.js";export default async(r,t)=>{const[o,i]=await s(r.cookies.session_token,"system:pages:read");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const[a,n]=await e({template:r.query.template});if(a)return t.status(a.code).json({error:a.msg});t.json(n)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\email-templates\file\GET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\email-templates\\file\\GET.js","names":["currentUserHasPermission","readEmailTemplate","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","template","query"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport readEmailTemplate from '../../../../../server/utils/email/readEmailTemplate.js';\n\n/* One email template: what the site sends now, and the packaged version it can go back to. `?template=owner/name`. */\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:read');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst [error, template] = await readEmailTemplate({ template: request.query.template });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.json(template);\n};\n"],"mappings":"OAAOA,MAA8B,6EAC9BC,MAAuB,wEAGfC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,qBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAOA,EAAOE,SAAkBb,EAAkB,CAAEa,SAAUX,EAAQY,MAAMD,WAC5E,GAAGF,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASO,KAAKG","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../server/utils/email/saveEmailTemplate.js";export default async(r,t)=>{const[o,i]=await s(r.cookies.session_token,"system:pages:update");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const[a,n]=await e({template:r.body?.template,source:r.body?.source});if(a)return t.status(a.code).json({error:a.msg});t.json(n)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\email-templates\file\PUT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\email-templates\\file\\PUT.js","names":["currentUserHasPermission","saveEmailTemplate","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","saved","template","body","source"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport saveEmailTemplate from '../../../../../server/utils/email/saveEmailTemplate.js';\n\n/* Saves the site's version of an email template. Body: `{ template, source }`. */\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:update');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst [error, saved] = await saveEmailTemplate({ template: request.body?.template, source: request.body?.source });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.json(saved);\n};\n"],"mappings":"OAAOA,MAA8B,6EAC9BC,MAAuB,wEAGfC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,uBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAOA,EAAOE,SAAeb,EAAkB,CAAEc,SAAUZ,EAAQa,MAAMD,SAAUE,OAAQd,EAAQa,MAAMC,SACzG,GAAGL,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASO,KAAKG","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../server/utils/email/renderEmailTemplate.js";export default async(r,t)=>{const[o,i]=await s(r.cookies.session_token,"system:pages:read");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const[a,n]=await e({template:r.query.template,data:{}});if(a)return t.status(a.code).json({error:a.msg});t.type("html").send(n)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\email-templates\preview\GET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\email-templates\\preview\\GET.js","names":["currentUserHasPermission","renderEmailTemplate","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","page","template","query","data","type","send"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport renderEmailTemplate from '../../../../../server/utils/email/renderEmailTemplate.js';\n\n/*\n An email template drawn with no data, to open in a tab: what the site would send, with the parts that need data left blank. An\n extension that can do better (real or sample data) serves its own preview and gives it to <k-email-templates> as `preview-url`.\n*/\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:pages:read');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst [error, page] = await renderEmailTemplate({ template: request.query.template, data: {} });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.type('html').send(page);\n};\n"],"mappings":"OAAOA,MAA8B,6EAC9BC,MAAyB,0EAMjBC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,qBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAOA,EAAOE,SAAcb,EAAoB,CAAEc,SAAUZ,EAAQa,MAAMD,SAAUE,KAAM,CAAC,IAC3F,GAAGL,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASc,KAAK,QAAQC,KAAKL","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../server/utils/email/listEmails.js";export default async(r,t)=>{const[i,o]=await s(r.cookies.session_token,"system:emails:read");if(i)return t.status(i.code).json({error:i.msg});if(!o)return t.status(403).json({error:"Insufficient permissions"});const{status:a,type:n,search:m,limit:u,offset:f}=r.query,[c,l]=await e({status:a,type:n,search:m,limit:u,offset:f});if(c)return t.status(c.code).json({error:c.msg});t.json(l)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\emails\GET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\emails\\GET.js","names":["currentUserHasPermission","listEmails","async","request","response","permError","hasPermission","cookies","session_token","status","code","json","error","msg","type","search","limit","offset","query","data"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\nimport listEmails from '../../../../server/utils/email/listEmails.js';\n\n/* The log of emails, newest first, without their bodies. `?status=`, `?type=`, `?search=`, `?limit=`, `?offset=`. */\nexport default async (request, response) => {\n\tconst [permError, hasPermission] = await currentUserHasPermission(request.cookies.session_token, 'system:emails:read');\n\tif(permError) return response.status(permError.code).json({ error: permError.msg });\n\tif(!hasPermission) return response.status(403).json({ error: 'Insufficient permissions' });\n\n\tconst { status, type, search, limit, offset } = request.query;\n\tconst [error, data] = await listEmails({ status, type, search, limit, offset });\n\tif(error) return response.status(error.code).json({ error: error.msg });\n\n\tresponse.json(data);\n};\n"],"mappings":"OAAOA,MAA8B,0EAC9BC,MAAgB,8DAGRC,MAAOC,EAASC,KAC9B,MAAOC,EAAWC,SAAuBN,EAAyBG,EAAQI,QAAQC,cAAe,sBACjG,GAAGH,EAAW,OAAOD,EAASK,OAAOJ,EAAUK,MAAMC,KAAK,CAAEC,MAAOP,EAAUQ,MAC7E,IAAIP,EAAe,OAAOF,EAASK,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAE7D,MAAMH,OAAEA,EAAMK,KAAEA,EAAIC,OAAEA,EAAMC,MAAEA,EAAKC,OAAEA,GAAWd,EAAQe,OACjDN,EAAOO,SAAclB,EAAW,CAAEQ,SAAQK,OAAMC,SAAQC,QAAOC,WACtE,GAAGL,EAAO,OAAOR,EAASK,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAEjET,EAASO,KAAKQ","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import s from"../../../../../server/utils/permissions/currentUserHasPermission.js";import r from"../../../../../server/utils/email/getEmail.js";export default async(e,t)=>{const[i,o]=await s(e.cookies.session_token,"system:emails:read");if(i)return t.status(i.code).json({error:i.msg});if(!o)return t.status(403).json({error:"Insufficient permissions"});const[n,a]=await r(e.params.id);if(n)return t.status(n.code).json({error:n.msg});t.json(a)};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\emails\[id]\GET.js.map