kempo 4.9.5 → 4.9.7

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 (37) hide show
  1. package/dist/admin/accounts/groups/[name]/index.page.html +5 -5
  2. package/dist/admin/accounts/groups/index.page.html +12 -10
  3. package/dist/admin/accounts/permissions/index.page.html +1 -1
  4. package/dist/admin/accounts/users/[id]/index.page.html +81 -63
  5. package/dist/admin/accounts/users/index.page.html +7 -9
  6. package/dist/admin/components/EmailTemplates.js +11 -19
  7. package/dist/admin/components/EmailTemplates.js.map +1 -1
  8. package/dist/admin/components/Emails.js +32 -29
  9. package/dist/admin/components/Emails.js.map +1 -1
  10. package/dist/admin/components/Updates.js +17 -22
  11. package/dist/admin/components/Updates.js.map +1 -1
  12. package/dist/admin/components/settings/SettingsPanel.js +1 -1
  13. package/dist/admin/components/settings/SettingsPanel.js.map +1 -1
  14. package/dist/admin/components/tableControls/EditEmailTemplate.js +2 -0
  15. package/dist/admin/components/tableControls/EditEmailTemplate.js.map +1 -0
  16. package/dist/admin/components/tableControls/UpdateItem.js +2 -0
  17. package/dist/admin/components/tableControls/UpdateItem.js.map +1 -0
  18. package/dist/admin/components/tableControls/UpdateSelected.js +2 -0
  19. package/dist/admin/components/tableControls/UpdateSelected.js.map +1 -0
  20. package/dist/admin/components/tableControls/ViewEmail.js +2 -0
  21. package/dist/admin/components/tableControls/ViewEmail.js.map +1 -0
  22. package/dist/admin/content/fragments/index.page.html +12 -12
  23. package/dist/admin/content/globals/index.page.html +12 -11
  24. package/dist/admin/content/pages/index.page.html +11 -23
  25. package/dist/admin/content/templates/index.page.html +13 -25
  26. package/dist/admin/emails/index.page.html +1 -1
  27. package/dist/admin/extensions/index.page.html +10 -9
  28. package/dist/admin/settings/index.page.html +13 -3
  29. package/middleware/kempo.js +1 -0
  30. package/package.json +2 -2
  31. package/server/utils/sessions/pruneExpiredSessions.js +32 -0
  32. package/server/utils/settings/settingRules.js +1 -0
  33. package/server/utils/settings/systemSettings.js +1 -0
  34. package/dist/admin/components/settings/AdminSettings.js +0 -7
  35. package/dist/admin/components/settings/AdminSettings.js.map +0 -1
  36. package/dist/admin/components/tableControls/ExtensionDetails.js +0 -2
  37. package/dist/admin/components/tableControls/ExtensionDetails.js.map +0 -1
@@ -13,7 +13,7 @@
13
13
  <script type="module" src="/kempo-ui/components/controls/TcDeleteRecord.js"></script>
14
14
  <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
15
15
  <script type="module" src="/kempo-ui/components/Icon.js"></script>
16
- <script type="module" src="/admin/components/TableRowLink.js"></script>
16
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
17
17
  <script type="module" src="/kempo/components/Permission.js"></script>
18
18
 
19
19
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
@@ -24,8 +24,8 @@
24
24
  </div>
25
25
 
26
26
  <h2>Custom Fragments</h2>
27
- <k-table id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom fragments have been created yet.">
28
- <admin-table-row-link slot="before" url-pattern="/admin/content/fragments/edit?fragment={editPath}" field-name="editPath"></admin-table-row-link>
27
+ <k-table open-on-click id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom fragments have been created yet.">
28
+ <kc-tc-open slot="before"></kc-tc-open>
29
29
  <div slot="top" class="d-f full" style="align-items: center;">
30
30
  <div>
31
31
  <k-permission has="system:fragments:delete">
@@ -48,8 +48,8 @@
48
48
  </k-table>
49
49
 
50
50
  <h2>Extension Fragments</h2>
51
- <k-table id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension fragments installed.">
52
- <admin-table-row-link slot="before" url-pattern="/admin/content/fragments/edit?fragment={editPath}" field-name="editPath"></admin-table-row-link>
51
+ <k-table open-on-click id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension fragments installed.">
52
+ <kc-tc-open slot="before"></kc-tc-open>
53
53
  <div class="d-f full" style="align-items: center;">
54
54
  <div>
55
55
  <kc-tc-first-page></kc-tc-first-page>
@@ -65,8 +65,8 @@
65
65
  </k-table>
66
66
 
67
67
  <h2>System Fragments</h2>
68
- <k-table id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system fragments found.">
69
- <admin-table-row-link slot="before" url-pattern="/admin/content/fragments/edit?fragment={editPath}" field-name="editPath"></admin-table-row-link>
68
+ <k-table open-on-click id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system fragments found.">
69
+ <kc-tc-open slot="before"></kc-tc-open>
70
70
  <div class="d-f full" style="align-items: center;">
71
71
  <div>
72
72
  <kc-tc-first-page></kc-tc-first-page>
@@ -94,7 +94,7 @@
94
94
 
95
95
  const fields = [
96
96
  { name: 'locked', label: '', size: 40, calculator: r => r.locked ? html`<k-icon name="lock"></k-icon>` : html`` },
97
- { name: 'name', label: 'Name', size: 200 },
97
+ { name: 'name', label: 'Name' },
98
98
  { name: 'file', label: 'File', size: 300 },
99
99
  { name: 'author', label: 'Author', size: 150 },
100
100
  { name: 'updatedAt', label: 'Updated', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' },
@@ -116,10 +116,10 @@
116
116
  Toast.error(error.msg || 'Failed to load fragments');
117
117
  return;
118
118
  }
119
- const toEditPath = f => ({ ...f, editPath: f.file.replace(/\.fragment\.html$/, '') });
120
- const custom = data.fragments.filter(f => f.owner === 'custom').map(toEditPath);
121
- const extension = data.fragments.filter(f => f.owner !== 'custom' && f.owner !== 'system').map(toEditPath);
122
- const system = data.fragments.filter(f => f.owner === 'system').map(toEditPath);
119
+ const withEditUrl = f => ({ ...f, _url: `/admin/content/fragments/edit?fragment=${encodeURIComponent(f.file.replace(/\.fragment\.html$/, ''))}` });
120
+ const custom = data.fragments.filter(f => f.owner === 'custom').map(withEditUrl);
121
+ const extension = data.fragments.filter(f => f.owner !== 'custom' && f.owner !== 'system').map(withEditUrl);
122
+ const system = data.fragments.filter(f => f.owner === 'system').map(withEditUrl);
123
123
  $customTable.setData({ records: custom, fields });
124
124
  $extensionTable.setData({ records: extension, fields: extensionFields });
125
125
  $systemTable.setData({ records: system, fields });
@@ -14,7 +14,7 @@
14
14
  <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
15
15
  <script type="module" src="/kempo-ui/components/Icon.js"></script>
16
16
  <script type="module" src="/kempo-ui/components/Combobox.js"></script>
17
- <script type="module" src="/admin/components/TableRowLink.js"></script>
17
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
18
18
  <script type="module" src="/kempo/components/Permission.js"></script>
19
19
 
20
20
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
@@ -25,8 +25,8 @@
25
25
  </div>
26
26
 
27
27
  <h2>Custom</h2>
28
- <k-table id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom global content has been created yet.">
29
- <admin-table-row-link slot="before" url-pattern="/admin/content/globals/edit?id={id}" field-name="id"></admin-table-row-link>
28
+ <k-table open-on-click id="customTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No custom global content has been created yet.">
29
+ <kc-tc-open slot="before"></kc-tc-open>
30
30
  <div slot="top" class="d-f full" style="align-items: center;">
31
31
  <div>
32
32
  <k-permission has="system:globals:delete">
@@ -49,8 +49,8 @@
49
49
  </k-table>
50
50
 
51
51
  <h2>Extensions</h2>
52
- <k-table id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension global content installed.">
53
- <admin-table-row-link slot="before" url-pattern="/admin/content/globals/edit?id={id}" field-name="id"></admin-table-row-link>
52
+ <k-table open-on-click id="extensionTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No extension global content installed.">
53
+ <kc-tc-open slot="before"></kc-tc-open>
54
54
  <div class="d-f full" style="align-items: center;">
55
55
  <div>
56
56
  <kc-tc-first-page></kc-tc-first-page>
@@ -66,8 +66,8 @@
66
66
  </k-table>
67
67
 
68
68
  <h2>System</h2>
69
- <k-table id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system global content found.">
70
- <admin-table-row-link slot="before" url-pattern="/admin/content/globals/edit?id={id}" field-name="id"></admin-table-row-link>
69
+ <k-table open-on-click id="systemTable" enable-pages page-size="25" page-size-options="[10,25,50,100]" placeholder="No system global content found.">
70
+ <kc-tc-open slot="before"></kc-tc-open>
71
71
  <div class="d-f full" style="align-items: center;">
72
72
  <div>
73
73
  <kc-tc-first-page></kc-tc-first-page>
@@ -95,7 +95,7 @@
95
95
 
96
96
  const fields = [
97
97
  { name: 'locked', label: '', size: 40, calculator: r => r.locked ? html`<k-icon name="lock"></k-icon>` : html`` },
98
- { name: 'name', label: 'Name', size: 200 },
98
+ { name: 'name', label: 'Name' },
99
99
  { name: 'location', label: 'Location', size: 150 },
100
100
  { name: 'priority', label: 'Priority', size: 80 },
101
101
  { name: 'author', label: 'Author', size: 150 },
@@ -118,9 +118,10 @@
118
118
  Toast.error(error.msg || 'Failed to load global content');
119
119
  return;
120
120
  }
121
- const custom = data.entries.filter(e => e.owner === 'custom');
122
- const extension = data.entries.filter(e => e.owner !== 'custom' && e.owner !== 'system');
123
- const system = data.entries.filter(e => e.owner === 'system');
121
+ const withEditUrl = e => ({ ...e, _url: `/admin/content/globals/edit?id=${encodeURIComponent(e.id)}` });
122
+ const custom = data.entries.filter(e => e.owner === 'custom').map(withEditUrl);
123
+ const extension = data.entries.filter(e => e.owner !== 'custom' && e.owner !== 'system').map(withEditUrl);
124
+ const system = data.entries.filter(e => e.owner === 'system').map(withEditUrl);
124
125
  $customTable.setData({ records: custom, fields });
125
126
  $extensionTable.setData({ records: extension, fields: extensionFields });
126
127
  $systemTable.setData({ records: system, fields });
@@ -45,7 +45,7 @@
45
45
  ></script>
46
46
  <script
47
47
  type="module"
48
- src="/admin/components/TableRowLink.js"
48
+ src="/kempo-ui/components/controls/TcOpen.js"
49
49
  ></script>
50
50
  <script
51
51
  type="module"
@@ -67,18 +67,14 @@
67
67
  </div>
68
68
 
69
69
  <h2>Custom Pages</h2>
70
- <k-table
70
+ <k-table open-on-click
71
71
  id="customTable"
72
72
  enable-pages
73
73
  page-size="25"
74
74
  page-size-options="[10,25,50,100]"
75
75
  placeholder="No custom pages have been created yet."
76
76
  >
77
- <admin-table-row-link
78
- slot="before"
79
- url-pattern="/admin/content/pages/edit?path={editPath}"
80
- field-name="editPath"
81
- ></admin-table-row-link>
77
+ <kc-tc-open slot="before"></kc-tc-open>
82
78
  <div
83
79
  slot="top"
84
80
  class="d-f full"
@@ -108,18 +104,14 @@
108
104
  </k-table>
109
105
 
110
106
  <h2>Extension Pages</h2>
111
- <k-table
107
+ <k-table open-on-click
112
108
  id="extensionTable"
113
109
  enable-pages
114
110
  page-size="25"
115
111
  page-size-options="[10,25,50,100]"
116
112
  placeholder="No extension pages installed."
117
113
  >
118
- <admin-table-row-link
119
- slot="before"
120
- url-pattern="/admin/content/pages/edit?path={editPath}"
121
- field-name="editPath"
122
- ></admin-table-row-link>
114
+ <kc-tc-open slot="before"></kc-tc-open>
123
115
  <div
124
116
  class="d-f full"
125
117
  style="align-items: center;"
@@ -138,18 +130,14 @@
138
130
  </k-table>
139
131
 
140
132
  <h2>System Pages</h2>
141
- <k-table
133
+ <k-table open-on-click
142
134
  id="systemTable"
143
135
  enable-pages
144
136
  page-size="25"
145
137
  page-size-options="[10,25,50,100]"
146
138
  placeholder="No system pages found."
147
139
  >
148
- <admin-table-row-link
149
- slot="before"
150
- url-pattern="/admin/content/pages/edit?path={editPath}"
151
- field-name="editPath"
152
- ></admin-table-row-link>
140
+ <kc-tc-open slot="before"></kc-tc-open>
153
141
  <div
154
142
  class="d-f full"
155
143
  style="align-items: center;"
@@ -183,7 +171,7 @@
183
171
  { name: 'name', label: 'Name', size: 200 },
184
172
  { name: 'url', label: 'URL', size: 250 },
185
173
  { name: 'author', label: 'Author', size: 150 },
186
- { name: 'description', label: 'Description', size: 300 },
174
+ { name: 'description', label: 'Description' },
187
175
  { name: 'updatedAt', label: 'Updated', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' },
188
176
  { name: 'createdAt', label: 'Created', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' }
189
177
  ];
@@ -214,7 +202,7 @@
214
202
 
215
203
  setInterval(updateScanInfo, 1000);
216
204
 
217
- const toEditPath = p => ({ ...p, editPath: p.file.replace(/\.page\.html$/, '') });
205
+ const withEditUrl = p => ({ ...p, _url: `/admin/content/pages/edit?path=${encodeURIComponent(p.file.replace(/\.page\.html$/, ''))}` });
218
206
 
219
207
  const initTable = async ($table, tableFields, owner) => {
220
208
  const [error, data] = await listPages({ owner, showHidden: false, offset: 0, limit: 25 });
@@ -222,11 +210,11 @@
222
210
  Toast.error(error.msg || 'Failed to load pages');
223
211
  return null;
224
212
  }
225
- $table.setData({ records: data.pages.map(toEditPath), fields: tableFields });
213
+ $table.setData({ records: data.pages.map(withEditUrl), fields: tableFields });
226
214
  if(data.total > data.pages.length){
227
215
  $table.setupFetchRecords(data.total, async (start, count) => {
228
216
  const [, d] = await listPages({ owner, showHidden: false, offset: start, limit: count });
229
- return (d?.pages || []).map(toEditPath);
217
+ return (d?.pages || []).map(withEditUrl);
230
218
  });
231
219
  }
232
220
  return data.scannedAt;
@@ -13,7 +13,7 @@
13
13
  <script type="module" src="/kempo-ui/components/controls/TcDeleteRecord.js"></script>
14
14
  <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
15
15
  <script type="module" src="/kempo-ui/components/Icon.js"></script>
16
- <script type="module" src="/admin/components/TableRowLink.js"></script>
16
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
17
17
  <script type="module" src="/kempo/components/Permission.js"></script>
18
18
 
19
19
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
@@ -24,18 +24,14 @@
24
24
  </div>
25
25
 
26
26
  <h2>Custom Templates</h2>
27
- <k-table
27
+ <k-table open-on-click
28
28
  id="customTable"
29
29
  enable-pages
30
30
  page-size="25"
31
31
  page-size-options="[10,25,50,100]"
32
32
  placeholder="No custom templates have been created yet."
33
33
  >
34
- <admin-table-row-link
35
- slot="before"
36
- url-pattern="/admin/content/templates/edit?template={editPath}"
37
- field-name="editPath"
38
- ></admin-table-row-link>
34
+ <kc-tc-open slot="before"></kc-tc-open>
39
35
  <div slot="top" class="d-f full" style="align-items: center;">
40
36
  <div>
41
37
  <k-permission has="system:pages:delete">
@@ -58,18 +54,14 @@
58
54
  </k-table>
59
55
 
60
56
  <h2>Extension Templates</h2>
61
- <k-table
57
+ <k-table open-on-click
62
58
  id="extensionTable"
63
59
  enable-pages
64
60
  page-size="25"
65
61
  page-size-options="[10,25,50,100]"
66
62
  placeholder="No extension templates installed."
67
63
  >
68
- <admin-table-row-link
69
- slot="before"
70
- url-pattern="/admin/content/templates/edit?template={editPath}"
71
- field-name="editPath"
72
- ></admin-table-row-link>
64
+ <kc-tc-open slot="before"></kc-tc-open>
73
65
  <div class="d-f full" style="align-items: center;">
74
66
  <div>
75
67
  <kc-tc-first-page></kc-tc-first-page>
@@ -85,18 +77,14 @@
85
77
  </k-table>
86
78
 
87
79
  <h2>System Templates</h2>
88
- <k-table
80
+ <k-table open-on-click
89
81
  id="systemTable"
90
82
  enable-pages
91
83
  page-size="25"
92
84
  page-size-options="[10,25,50,100]"
93
85
  placeholder="No system templates found."
94
86
  >
95
- <admin-table-row-link
96
- slot="before"
97
- url-pattern="/admin/content/templates/edit?template={editPath}"
98
- field-name="editPath"
99
- ></admin-table-row-link>
87
+ <kc-tc-open slot="before"></kc-tc-open>
100
88
  <div class="d-f full" style="align-items: center;">
101
89
  <div>
102
90
  <kc-tc-first-page></kc-tc-first-page>
@@ -124,7 +112,7 @@
124
112
 
125
113
  const fields = [
126
114
  { name: 'locked', label: '', size: 40, calculator: r => r.locked ? html`<k-icon name="lock"></k-icon>` : html`` },
127
- { name: 'name', label: 'Name', size: 150 },
115
+ { name: 'name', label: 'Name' },
128
116
  { name: 'directory', label: 'Directory', size: 200 },
129
117
  { name: 'author', label: 'Author', size: 150 },
130
118
  { name: 'updatedAt', label: 'Updated', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' },
@@ -146,14 +134,14 @@
146
134
  Toast.error(error.msg || 'Failed to load templates');
147
135
  return;
148
136
  }
149
- const toEditPath = t => ({
137
+ const withEditUrl = t => ({
150
138
  ...t,
151
139
  directory: t.directory === '.' ? '/' : t.directory,
152
- editPath: (t.directory === '.' ? t.name : `${t.directory}/${t.name}`)
140
+ _url: `/admin/content/templates/edit?template=${encodeURIComponent(t.directory === '.' ? t.name : `${t.directory}/${t.name}`)}`
153
141
  });
154
- const custom = data.templates.filter(t => t.owner === 'custom').map(toEditPath);
155
- const extension = data.templates.filter(t => t.owner !== 'custom' && t.owner !== 'system').map(toEditPath);
156
- const system = data.templates.filter(t => t.owner === 'system').map(toEditPath);
142
+ const custom = data.templates.filter(t => t.owner === 'custom').map(withEditUrl);
143
+ const extension = data.templates.filter(t => t.owner !== 'custom' && t.owner !== 'system').map(withEditUrl);
144
+ const system = data.templates.filter(t => t.owner === 'system').map(withEditUrl);
157
145
  $customTable.setData({ records: custom, fields });
158
146
  $extensionTable.setData({ records: extension, fields: extensionFields });
159
147
  $systemTable.setData({ records: system, fields });
@@ -10,7 +10,7 @@
10
10
  <h1>Emails</h1>
11
11
  <p class="tc-muted mb">Kempo does not send email itself: an email extension (such as kempo-resend or kempo-smtp) does. How long the 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>
12
12
 
13
- <k-tabs persistent-id="admin-emails">
13
+ <k-tabs persistent-id="admin-emails" hash-routing>
14
14
  <k-tab for="log">Log</k-tab>
15
15
  <k-tab for="templates">Templates</k-tab>
16
16
  <k-tab-content name="log">
@@ -11,7 +11,7 @@
11
11
  <script type="module" src="/kempo-ui/components/controls/TcPageSelect.js"></script>
12
12
  <script type="module" src="/kempo-ui/components/controls/TcPageSize.js"></script>
13
13
  <script type="module" src="/admin/components/tableControls/InstallExtension.js"></script>
14
- <script type="module" src="/admin/components/tableControls/ExtensionDetails.js"></script>
14
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
15
15
  <script type="module" src="/admin/components/tableControls/ToggleExtension.js"></script>
16
16
  <script type="module" src="/admin/components/tableControls/UninstallExtension.js"></script>
17
17
  <script type="module" src="/kempo/components/Permission.js"></script>
@@ -22,8 +22,8 @@
22
22
  </div>
23
23
 
24
24
  <h2>Installed Extensions</h2>
25
- <k-table id="installedTable" enable-pages page-size="25" placeholder="No extensions are currently installed.">
26
- <admin-extension-details slot="after"></admin-extension-details>
25
+ <k-table open-on-click id="installedTable" enable-pages page-size="25" placeholder="No extensions are currently installed.">
26
+ <kc-tc-open slot="after" title="View in Store"><k-icon name="storefront"></k-icon></kc-tc-open>
27
27
  <admin-toggle-extension slot="after"></admin-toggle-extension>
28
28
  <admin-uninstall-extension slot="after"></admin-uninstall-extension>
29
29
  <div class="d-f full" style="align-items: center;">
@@ -42,9 +42,9 @@
42
42
 
43
43
  <h2>Downloaded, Not Installed</h2>
44
44
  <p class="tc-muted">Extension packages already in this site. To find and download more, <a href="/admin/extensions/browse/">browse the marketplace</a>.</p>
45
- <k-table id="availableTable" enable-pages page-size="25" placeholder="Every downloaded extension is installed.">
45
+ <k-table open-on-click id="availableTable" enable-pages page-size="25" placeholder="Every downloaded extension is installed.">
46
46
  <admin-install-extension slot="after"></admin-install-extension>
47
- <admin-extension-details slot="after"></admin-extension-details>
47
+ <kc-tc-open slot="after" title="View in Store"><k-icon name="storefront"></k-icon></kc-tc-open>
48
48
  <div class="d-f full" style="align-items: center;">
49
49
  <div>
50
50
  <kc-tc-first-page></kc-tc-first-page>
@@ -66,11 +66,12 @@
66
66
 
67
67
  const installedTable = document.getElementById('installedTable');
68
68
  const availableTable = document.getElementById('availableTable');
69
+ const withDetailsUrl = e => ({ ...e, _url: `/admin/extensions/details/?name=${encodeURIComponent(e.name)}` });
69
70
 
70
71
  const [installedErr, installedData] = await listExtensions();
71
72
  if(!installedErr){
72
73
  installedTable.setData({
73
- records: installedData.items,
74
+ records: installedData.items.map(withDetailsUrl),
74
75
  fields: [
75
76
  { name: 'name', label: 'Name' },
76
77
  { name: 'version', label: 'Version' },
@@ -85,7 +86,7 @@
85
86
  const installedNames = new Set((installedData?.items || []).map(e => e.name));
86
87
  const notInstalled = availableData.items.filter(e => !installedNames.has(e.name));
87
88
  availableTable.setData({
88
- records: notInstalled,
89
+ records: notInstalled.map(withDetailsUrl),
89
90
  fields: [
90
91
  { name: 'name', label: 'Name' },
91
92
  { name: 'version', label: 'Version' },
@@ -97,7 +98,7 @@
97
98
  const refreshInstalledTable = async () => {
98
99
  const [err, data] = await listExtensions();
99
100
  if(!err) installedTable.setData({
100
- records: data.items,
101
+ records: data.items.map(withDetailsUrl),
101
102
  fields: [
102
103
  { name: 'name', label: 'Name' },
103
104
  { name: 'version', label: 'Version' },
@@ -113,7 +114,7 @@
113
114
  if(!instErr && !availErr){
114
115
  const installedNames = new Set((instData?.items || []).map(e => e.name));
115
116
  availableTable.setData({
116
- records: availData.items.filter(e => !installedNames.has(e.name)),
117
+ records: availData.items.filter(e => !installedNames.has(e.name)).map(withDetailsUrl),
117
118
  fields: [
118
119
  { name: 'name', label: 'Name' },
119
120
  { name: 'version', label: 'Version' },
@@ -3,13 +3,23 @@
3
3
  -->
4
4
  <page title="Settings - Admin">
5
5
  <content>
6
- <script type="module" src="/admin/components/settings/AdminSettings.js"></script>
6
+ <script type="module" src="/kempo-ui/components/Tabs.js"></script>
7
+ <script type="module" src="/admin/components/settings/SettingsPanel.js"></script>
7
8
 
8
9
  <h1>Settings</h1>
9
10
 
10
- <k-admin-settings>
11
+ <k-tabs active="custom" hash-routing>
12
+ <k-tab for="custom">Custom</k-tab>
13
+ <k-tab for="system">System</k-tab>
11
14
  <location name="admin-settings-tabs" />
12
- </k-admin-settings>
15
+
16
+ <k-tab-content name="custom" class="pt">
17
+ <k-settings-panel owner="custom"></k-settings-panel>
18
+ </k-tab-content>
19
+ <k-tab-content name="system" class="pt">
20
+ <k-settings-panel owner="system"></k-settings-panel>
21
+ </k-tab-content>
22
+ </k-tabs>
13
23
  <location name="admin-settings-scripts" />
14
24
 
15
25
  </content>
@@ -257,6 +257,7 @@ export default config => {
257
257
  ['schema notice', notifySchemaAttention],
258
258
  ['jobs', () => registerJobs({ owner: 'kempo', jobs: [
259
259
  { name: 'email-maintenance', every: '1m', callback: './server/utils/email/maintainEmails.js' },
260
+ { name: 'session-cleanup', every: '1h', callback: './server/utils/sessions/pruneExpiredSessions.js' },
260
261
  { name: 'update-check', every: '12h', callback: './server/utils/updates/runUpdateCheck.js' },
261
262
  ] })],
262
263
  ]){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.9.5",
3
+ "version": "4.9.7",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {
@@ -61,7 +61,7 @@
61
61
  "peerDependencies": {
62
62
  "kempo-css": ">=2.2.5",
63
63
  "kempo-server": ">=3.4.0",
64
- "kempo-ui": ">=0.6.0"
64
+ "kempo-ui": ">=0.7.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@electric-sql/pglite": "^0.5.9",
@@ -0,0 +1,32 @@
1
+ import { lt } from 'drizzle-orm';
2
+ import db from '../../db/index.js';
3
+ import { session } from '../../db/schema.js';
4
+ import getSetting from '../settings/getSetting.js';
5
+
6
+ /*
7
+ Deletes every session that expired more than `system:session_cleanup_hours` ago (default 24), for every user.
8
+ Run hourly by the scheduler (middleware/kempo.js registers it). 0 keeps expired sessions forever, and the
9
+ per-user "clear expired" button still works either way.
10
+ */
11
+ export default async ({ now = new Date(), hours } = {}) => {
12
+ try {
13
+ let cutoffHours = hours;
14
+
15
+ if(cutoffHours === undefined){
16
+ const [, stored] = await getSetting('system', 'session_cleanup_hours', 24);
17
+ cutoffHours = Number(stored);
18
+ if(!Number.isFinite(cutoffHours) || cutoffHours < 0) cutoffHours = 24;
19
+ }
20
+
21
+ if(cutoffHours === 0) return [null, { deleted: 0 }];
22
+
23
+ const removed = await db
24
+ .delete(session)
25
+ .where(lt(session.expiresAt, new Date(now.getTime() - cutoffHours * 3600000)))
26
+ .returning({ token: session.token });
27
+
28
+ return [null, { deleted: removed.length }];
29
+ } catch(error) {
30
+ return [{ code: 500, msg: 'Failed to delete expired sessions' }, null];
31
+ }
32
+ };
@@ -12,6 +12,7 @@ const RULES = {
12
12
  'system:email_retention_by_type': value => (value && typeof value === 'object' && !Array.isArray(value) && Object.values(value).every(days => whole(days, 0, 3650))
13
13
  ? null
14
14
  : { code: 400, msg: 'Give each kind of email a whole number of days from 0 to 3650, like { "verify-email": 7 }' }),
15
+ 'system:session_cleanup_hours': value => (whole(value, 0, 8760) ? null : { code: 400, msg: 'Hours must be a whole number from 0 to 8760 (one year)' }),
15
16
  'system:email_from': value => (value === '' || isValidSender(value) ? null : { code: 400, msg: 'The sender must look like name@example.com or Shop Name <name@example.com>' }),
16
17
  };
17
18
 
@@ -14,6 +14,7 @@ export const SYSTEM_SETTINGS = [
14
14
  { owner: 'system', name: 'allow_registration', value: true, type: 'boolean', isPublic: true, description: 'Allow new users to register' },
15
15
  { owner: 'system', name: 'require_email_verification', value: false, type: 'boolean', isPublic: false, description: 'Require email verification before account access' },
16
16
  { owner: 'system', name: 'verification_url', value: 'http://localhost:3000/verify-email/{{token}}', type: 'string', isPublic: false, description: 'Email verification URL template' },
17
+ { owner: 'system', name: 'session_cleanup_hours', value: 24, type: 'number', isPublic: false, description: 'Hours after a session expires before it is deleted automatically (0 keeps expired sessions)' },
17
18
  { owner: 'system', name: 'notification_retention_days', value: 90, type: 'number', isPublic: false, description: 'Days to keep notifications before they are deleted (0 keeps them forever)' },
18
19
  { owner: 'system', name: 'password_reset_url', value: 'http://localhost:3000/reset-password/{{token}}', type: 'string', isPublic: false, description: 'Password reset URL template' },
19
20
  { owner: 'system', name: 'email_max_retries', value: 1, type: 'number', isPublic: false, description: 'How many times kempo tries again to send an email that could not be sent, after the first try (0 never retries)' },
@@ -1,7 +0,0 @@
1
- import t from"/kempo-ui/components/ShadowComponent.js";import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Tabs.js";import"./SettingsPanel.js";import s from"./humanize.js";export default class n extends t{static properties={extensionTabs:{state:!0}};constructor(){super(),this.extensionTabs=[]}connectedCallback(){super.connectedCallback(),this.extensionTabs=[...this.querySelectorAll(":scope > k-settings-tab")].map((t,e)=>{const n=t.getAttribute("name")||t.getAttribute("label")||`tab-${e}`;return t.slot=`tab-${n}`,t.style.display="block",{id:n,slotName:`tab-${n}`,label:t.getAttribute("label")||s(n),position:Number(t.getAttribute("position"))||500,extension:!0}})}render(){const t=[{id:"custom",label:"Custom",position:100,content:()=>e`<k-settings-panel owner="custom"></k-settings-panel>`},{id:"system",label:"System",position:200,content:()=>e`<k-settings-panel owner="system"></k-settings-panel>`},...this.extensionTabs].sort((t,e)=>t.position-e.position);return e`
2
- <k-tabs>
3
- ${t.map(t=>e`<k-tab for=${t.id}>${t.label}</k-tab>`)}
4
- ${t.map(t=>e`<k-tab-content name=${t.id} class="pt">${t.extension?e`<slot name=${t.slotName}></slot>`:t.content()}</k-tab-content>`)}
5
- </k-tabs>
6
- `}}customElements.define("k-admin-settings",n);
7
- //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\AdminSettings.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"src\\admin\\components\\settings\\AdminSettings.js","names":["ShadowComponent","html","humanize","AdminSettings","static","extensionTabs","state","constructor","super","this","connectedCallback","querySelectorAll","map","$tab","index","name","getAttribute","slot","style","display","id","slotName","label","position","Number","extension","render","tabs","content","sort","a","b","tab","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Tabs.js';\nimport './SettingsPanel.js';\nimport humanize from './humanize.js';\n\n/*\n The admin Settings page, served at /admin/components/settings/AdminSettings.js. Tabs: **Custom** (the site's own settings) and **System**\n (kempo's), then a tab for each extension that chooses to add one. An extension that does not add a tab has nothing here: it manages\n its own settings wherever it likes.\n\n **An extension adds a tab** by filling the `admin-settings-tabs` location of the page with a `k-settings-tab` (and loads what the tab\n uses from the `admin-settings-scripts` location). `position` orders the tabs (Custom is 100, System 200, a tab with none 500):\n\n <k-settings-tab name=\"shipping\" label=\"Shipping\" position=\"500\">\n <k-settings-panel owner=\"kempo-shipping\"></k-settings-panel>\n </k-settings-tab>\n\n The tab holds anything: the generic panel above, a few of the extension's settings with their editors, or a screen of its own.\n*/\nconst DEFAULT_TAB_POSITION = 500;\n\nexport default class AdminSettings extends ShadowComponent {\n static properties = {\n extensionTabs: { state: true },\n };\n\n constructor(){\n super();\n this.extensionTabs = [];\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.extensionTabs = [...this.querySelectorAll(':scope > k-settings-tab')].map(($tab, index) => {\n const name = $tab.getAttribute('name') || $tab.getAttribute('label') || `tab-${index}`;\n $tab.slot = `tab-${name}`;\n $tab.style.display = 'block';\n return {\n id: name,\n slotName: `tab-${name}`,\n label: $tab.getAttribute('label') || humanize(name),\n position: Number($tab.getAttribute('position')) || DEFAULT_TAB_POSITION,\n extension: true,\n };\n });\n }\n\n /*\n Rendering\n */\n render(){\n const tabs = [\n { id: 'custom', label: 'Custom', position: 100, content: () => html`<k-settings-panel owner=\"custom\"></k-settings-panel>` },\n { id: 'system', label: 'System', position: 200, content: () => html`<k-settings-panel owner=\"system\"></k-settings-panel>` },\n ...this.extensionTabs,\n ].sort((a, b) => a.position - b.position);\n\n return html`\n <k-tabs>\n ${tabs.map(tab => html`<k-tab for=${tab.id}>${tab.label}</k-tab>`)}\n ${tabs.map(tab => html`<k-tab-content name=${tab.id} class=\"pt\">${tab.extension ? html`<slot name=${tab.slotName}></slot>` : tab.content()}</k-tab-content>`)}\n </k-tabs>\n `;\n }\n}\n\ncustomElements.define('k-admin-settings', AdminSettings);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,qCACA,4BACAC,MAAc,+BAkBN,MAAMC,UAAsBH,EACzCI,kBAAoB,CAClBC,cAAe,CAAEC,OAAO,IAG1B,WAAAC,GACEC,QACAC,KAAKJ,cAAgB,EACvB,CAKA,iBAAAK,GACEF,MAAME,oBACND,KAAKJ,cAAgB,IAAII,KAAKE,iBAAiB,4BAA4BC,IAAI,CAACC,EAAMC,KACpF,MAAMC,EAAOF,EAAKG,aAAa,SAAWH,EAAKG,aAAa,UAAY,OAAOF,IAG/E,OAFAD,EAAKI,KAAO,OAAOF,IACnBF,EAAKK,MAAMC,QAAU,QACd,CACLC,GAAIL,EACJM,SAAU,OAAON,IACjBO,MAAOT,EAAKG,aAAa,UAAYd,EAASa,GAC9CQ,SAAUC,OAAOX,EAAKG,aAAa,cAzBd,IA0BrBS,WAAW,IAGjB,CAKA,MAAAC,GACE,MAAMC,EAAO,CACX,CAAEP,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,wDACnE,CAAEmB,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,2DAChEQ,KAAKJ,eACRwB,KAAK,CAACC,EAAGC,IAAMD,EAAEP,SAAWQ,EAAER,UAEhC,OAAOtB,CAAI;;UAEL0B,EAAKf,IAAIoB,GAAO/B,CAAI,cAAc+B,EAAIZ,MAAMY,EAAIV;UAChDK,EAAKf,IAAIoB,GAAO/B,CAAI,uBAAuB+B,EAAIZ,iBAAiBY,EAAIP,UAAYxB,CAAI,cAAc+B,EAAIX,mBAAqBW,EAAIJ;;KAGvI,EAGFK,eAAeC,OAAO,mBAAoB/B","ignoreList":[]}
@@ -1,2 +0,0 @@
1
- import e from"../AdminTableControl.js";import{html as n}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";export default class t extends e{connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="View in Store")}handleAction(){this.record&&(window.location.href=`/admin/extensions/details/?name=${encodeURIComponent(this.record.name)}`)}render(){return n`<k-icon name="storefront"></k-icon>`}}customElements.define("admin-extension-details",t);
2
- //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\ExtensionDetails.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"src\\admin\\components\\tableControls\\ExtensionDetails.js","names":["AdminTableControl","html","ExtensionDetails","connectedCallback","super","this","hasAttribute","title","handleAction","record","window","location","href","encodeURIComponent","name","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Icon.js';\n\n/*\n Opens the marketplace page for a row's extension: its README, what it requires and adds, its rating (which\n can be changed from there once it is installed). The page shows whatever the marketplace knows; for an\n extension it does not list it says so.\n*/\nexport default class ExtensionDetails extends AdminTableControl {\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'View in Store';\n }\n\n handleAction(){\n if(!this.record) return;\n window.location.href = `/admin/extensions/details/?name=${encodeURIComponent(this.record.name)}`;\n }\n\n render(){ return html`<k-icon name=\"storefront\"></k-icon>`; }\n}\n\ncustomElements.define('admin-extension-details', ExtensionDetails);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,iCACd,8CAOQ,MAAMC,UAAyBF,EAC5C,iBAAAG,GACEC,MAAMD,oBACFE,KAAKC,aAAa,WAAUD,KAAKE,MAAQ,gBAC/C,CAEA,YAAAC,GACMH,KAAKI,SACTC,OAAOC,SAASC,KAAO,mCAAmCC,mBAAmBR,KAAKI,OAAOK,QAC3F,CAEA,MAAAC,GAAU,OAAOd,CAAI,qCAAuC,EAG9De,eAAeC,OAAO,0BAA2Bf","ignoreList":[]}