kempo 4.6.1 → 4.7.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.
- package/README.md +5 -0
- package/app-public/account/index.page.html +2 -0
- package/app-public/nav.fragment.html +2 -1
- package/app-templates/emails/email.template.html +14 -0
- package/app-templates/emails/reset-password.page.html +19 -0
- package/app-templates/emails/test.page.html +21 -0
- package/app-templates/emails/verify-email.page.html +18 -0
- package/bin/cli.js +3 -0
- package/dist/admin/components/Dashboard.js +2 -2
- package/dist/admin/components/Dashboard.js.map +1 -1
- package/dist/admin/components/Menus.js +103 -0
- package/dist/admin/components/Menus.js.map +1 -0
- package/dist/admin/components/tableControls/ExtensionDetails.js +1 -1
- package/dist/admin/components/tableControls/ExtensionDetails.js.map +1 -1
- package/dist/admin/content/menus/index.page.html +11 -0
- package/dist/admin/extensions/browse/index.page.html +4 -1
- package/dist/admin/extensions/index.page.html +1 -0
- package/dist/admin/nav.fragment.html +6 -0
- package/dist/kempo/api/extensions/POST.js +1 -1
- package/dist/kempo/api/extensions/POST.js.map +1 -1
- package/dist/kempo/api/extensions/known/GET.js +1 -1
- package/dist/kempo/api/extensions/known/GET.js.map +1 -1
- package/dist/kempo/api/extensions/rating/PUT.js +1 -1
- package/dist/kempo/api/extensions/rating/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/DELETE.js +1 -1
- package/dist/kempo/api/fragments/DELETE.js.map +1 -1
- package/dist/kempo/api/fragments/GET.js +1 -1
- package/dist/kempo/api/fragments/GET.js.map +1 -1
- package/dist/kempo/api/fragments/POST.js +1 -1
- package/dist/kempo/api/fragments/POST.js.map +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/file/GET.js +1 -1
- package/dist/kempo/api/fragments/file/GET.js.map +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js.map +1 -1
- package/dist/kempo/api/fragments/file/PUT.js +1 -1
- package/dist/kempo/api/fragments/file/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js.map +1 -1
- package/dist/kempo/api/globals/DELETE.js +1 -1
- package/dist/kempo/api/globals/DELETE.js.map +1 -1
- package/dist/kempo/api/globals/GET.js +1 -1
- package/dist/kempo/api/globals/GET.js.map +1 -1
- package/dist/kempo/api/globals/POST.js +1 -1
- package/dist/kempo/api/globals/POST.js.map +1 -1
- package/dist/kempo/api/globals/disable/PUT.js +1 -1
- package/dist/kempo/api/globals/disable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/enable/PUT.js +1 -1
- package/dist/kempo/api/globals/enable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/entry/GET.js +1 -1
- package/dist/kempo/api/globals/entry/GET.js.map +1 -1
- package/dist/kempo/api/globals/entry/PUT.js +1 -1
- package/dist/kempo/api/globals/entry/PUT.js.map +1 -1
- package/dist/kempo/api/globals/lock/PUT.js +1 -1
- package/dist/kempo/api/globals/lock/PUT.js.map +1 -1
- package/dist/kempo/api/menus/DELETE.js +2 -0
- package/dist/kempo/api/menus/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/GET.js +2 -0
- package/dist/kempo/api/menus/GET.js.map +1 -0
- package/dist/kempo/api/menus/POST.js +2 -0
- package/dist/kempo/api/menus/POST.js.map +1 -0
- package/dist/kempo/api/menus/adopt/PUT.js +2 -0
- package/dist/kempo/api/menus/adopt/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/DELETE.js +2 -0
- package/dist/kempo/api/menus/items/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/items/POST.js +2 -0
- package/dist/kempo/api/menus/items/POST.js.map +1 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js +2 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/order/PUT.js +2 -0
- package/dist/kempo/api/menus/items/order/PUT.js.map +1 -0
- package/dist/kempo/api/menus/location/PUT.js +2 -0
- package/dist/kempo/api/menus/location/PUT.js.map +1 -0
- package/dist/kempo/api/pages/DELETE.js +1 -1
- package/dist/kempo/api/pages/DELETE.js.map +1 -1
- package/dist/kempo/api/pages/GET.js +1 -1
- package/dist/kempo/api/pages/GET.js.map +1 -1
- package/dist/kempo/api/pages/POST.js +1 -1
- package/dist/kempo/api/pages/POST.js.map +1 -1
- package/dist/kempo/api/pages/directories/GET.js +1 -1
- package/dist/kempo/api/pages/directories/GET.js.map +1 -1
- package/dist/kempo/api/pages/disable/PUT.js +1 -1
- package/dist/kempo/api/pages/disable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/enable/PUT.js +1 -1
- package/dist/kempo/api/pages/enable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/file/GET.js +1 -1
- package/dist/kempo/api/pages/file/GET.js.map +1 -1
- package/dist/kempo/api/pages/file/PATCH.js +1 -1
- package/dist/kempo/api/pages/file/PATCH.js.map +1 -1
- package/dist/kempo/api/pages/file/PUT.js +1 -1
- package/dist/kempo/api/pages/file/PUT.js.map +1 -1
- package/dist/kempo/api/pages/lock/PUT.js +1 -1
- package/dist/kempo/api/pages/lock/PUT.js.map +1 -1
- package/dist/kempo/api/pages/rescan/POST.js +1 -1
- package/dist/kempo/api/pages/rescan/POST.js.map +1 -1
- package/dist/kempo/api/pages/search/GET.js +1 -1
- package/dist/kempo/api/pages/search/GET.js.map +1 -1
- package/dist/kempo/api/pages/templates/GET.js +1 -1
- package/dist/kempo/api/pages/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/DELETE.js +1 -1
- package/dist/kempo/api/templates/DELETE.js.map +1 -1
- package/dist/kempo/api/templates/GET.js +1 -1
- package/dist/kempo/api/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/POST.js +1 -1
- package/dist/kempo/api/templates/POST.js.map +1 -1
- package/dist/kempo/api/templates/disable/PUT.js +1 -1
- package/dist/kempo/api/templates/disable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/enable/PUT.js +1 -1
- package/dist/kempo/api/templates/enable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/file/GET.js +1 -1
- package/dist/kempo/api/templates/file/GET.js.map +1 -1
- package/dist/kempo/api/templates/file/PATCH.js +1 -1
- package/dist/kempo/api/templates/file/PATCH.js.map +1 -1
- package/dist/kempo/api/templates/file/PUT.js +1 -1
- package/dist/kempo/api/templates/file/PUT.js.map +1 -1
- package/dist/kempo/api/templates/lock/PUT.js +1 -1
- package/dist/kempo/api/templates/lock/PUT.js.map +1 -1
- package/dist/kempo/components/DashboardWidget.js +4 -4
- package/dist/kempo/components/DashboardWidget.js.map +1 -1
- package/dist/kempo/icons/campaign.svg +1 -0
- package/dist/kempo/icons/fields.svg +1 -0
- package/dist/kempo/icons/open_in_new.svg +1 -0
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/middleware/kempo.js +48 -12
- package/package.json +2 -2
- package/server/db/schema.js +24 -0
- package/server/sdk.js +21 -0
- package/server/utils/auth/requestPasswordReset.js +9 -3
- package/server/utils/auth/verifyEmail.js +10 -1
- package/server/utils/dashboard/coreWidgets.js +1 -0
- package/server/utils/dashboard/helpers.js +6 -0
- package/server/utils/email/renderEmailTemplate.js +63 -0
- package/server/utils/email/sendEmailFromTemplate.js +9 -27
- package/server/utils/extensions/aliases.js +71 -0
- package/server/utils/extensions/checkClaims.js +32 -0
- package/server/utils/extensions/claims.js +54 -0
- package/server/utils/extensions/createTablesFromSchema.js +13 -20
- package/server/utils/extensions/enableExtensionTree.js +8 -0
- package/server/utils/extensions/{getMarketplaceClient.js → getSiteMarketplaceClient.js} +1 -1
- package/server/utils/extensions/installExtension.js +32 -0
- package/server/utils/extensions/installFromCatalog.js +1 -1
- package/server/utils/extensions/installPackage.js +1 -1
- package/server/utils/extensions/{catalogClient.js → marketplaceClient.js} +4 -1
- package/server/utils/extensions/readKempoConfig.js +2 -2
- package/server/utils/extensions/scopeCache.js +14 -1
- package/server/utils/extensions/scopes.js +72 -0
- package/server/utils/extensions/uninstallExtensionTree.js +2 -1
- package/server/utils/extensions/updateExtension.js +35 -0
- package/server/utils/global-content/createGlobalContent.js +8 -18
- package/server/utils/global-content/deleteGlobalContent.js +1 -16
- package/server/utils/global-content/disableGlobalContent.js +1 -16
- package/server/utils/global-content/enableGlobalContent.js +1 -16
- package/server/utils/global-content/helpers.js +40 -6
- package/server/utils/global-content/listGlobalContent.js +2 -2
- package/server/utils/global-content/setGlobalContentLocked.js +1 -16
- package/server/utils/global-content/updateGlobalContent.js +1 -16
- package/server/utils/menus/adoptMenuItem.js +58 -0
- package/server/utils/menus/assignMenuLocation.js +47 -0
- package/server/utils/menus/deleteMenu.js +27 -0
- package/server/utils/menus/deleteMenuItems.js +27 -0
- package/server/utils/menus/findMenuLocations.js +41 -0
- package/server/utils/menus/findNavItems.js +74 -0
- package/server/utils/menus/findPages.js +73 -0
- package/server/utils/menus/listMenus.js +103 -0
- package/server/utils/menus/markup.js +55 -0
- package/server/utils/menus/reorderMenuItems.js +36 -0
- package/server/utils/menus/saveMenu.js +54 -0
- package/server/utils/menus/saveMenuItem.js +122 -0
- package/server/utils/menus/setMenuItemEnabled.js +31 -0
- package/server/utils/menus/store.js +90 -0
- package/server/utils/scheduler/helpers.js +36 -0
- package/server/utils/scheduler/listJobs.js +11 -0
- package/server/utils/scheduler/registerJobs.js +37 -0
- package/server/utils/scheduler/runJobs.js +90 -0
- package/server/utils/scheduler/scheduler.js +37 -0
- package/server/utils/scheduler/unregisterJobs.js +16 -0
- package/server/utils/settings/ensureEncryptionKey.js +132 -0
- package/server/utils/settings/secretCrypto.js +5 -1
- package/dist/kempo/icons/create_new_folder.svg +0 -1
- package/dist/kempo/icons/credit_card.svg +0 -1
- package/dist/kempo/icons/login.svg +0 -1
- package/dist/kempo/icons/payments.svg +0 -1
- package/dist/kempo/icons/toll.svg +0 -1
- package/dist/kempo/icons/wallet.svg +0 -1
package/middleware/kempo.js
CHANGED
|
@@ -5,11 +5,16 @@ import { renderExternalPage } from 'kempo-server/templating';
|
|
|
5
5
|
import getSession from '../server/utils/auth/getSession.js';
|
|
6
6
|
import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
|
|
7
7
|
import { LEXICAL_BASE, bundleFileName as lexicalBundleFileName } from '../server/utils/lexical/packages.js';
|
|
8
|
-
import { getEnabledExtensions } from '../server/utils/extensions/scopeCache.js';
|
|
8
|
+
import { getEnabledExtensions, onExtensionsChanged } from '../server/utils/extensions/scopeCache.js';
|
|
9
|
+
import { resyncMenus } from '../server/utils/menus/store.js';
|
|
9
10
|
import readKempoConfig from '../server/utils/extensions/readKempoConfig.js';
|
|
10
11
|
import { extensionAdminDirs, extensionPublicDirs } from '../server/utils/extensions/contentDirs.js';
|
|
11
12
|
import triggerHook from '../server/utils/hooks/triggerHook.js';
|
|
12
13
|
import { ADMIN_GLOBALS_DIR } from '../server/utils/admin-global-content/helpers.js';
|
|
14
|
+
import { startScheduler } from '../server/utils/scheduler/scheduler.js';
|
|
15
|
+
import { ensureEncryptionKey } from '../server/utils/settings/ensureEncryptionKey.js';
|
|
16
|
+
import { resolveAlias } from '../server/utils/extensions/aliases.js';
|
|
17
|
+
import { normalizeScopes } from '../server/utils/extensions/scopes.js';
|
|
13
18
|
|
|
14
19
|
const MIME_TYPES = {
|
|
15
20
|
'.js': 'application/javascript',
|
|
@@ -177,10 +182,30 @@ const serveDir = async (dirPath, method, request, response, resolveDir, rootDir,
|
|
|
177
182
|
};
|
|
178
183
|
|
|
179
184
|
export default config => {
|
|
185
|
+
startScheduler();
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
The key that encrypts stored credentials exists before any request needs it, generated into .env the
|
|
189
|
+
first time when the site has none. Opt out with `autoEncryptionKey: false` in the server config or
|
|
190
|
+
KEMPO_AUTO_ENCRYPTION_KEY=off, and never in the in-memory test database.
|
|
191
|
+
*/
|
|
192
|
+
const encryptionKeyReady = config.autoEncryptionKey === false || process.env.KEMPO_AUTO_ENCRYPTION_KEY === 'off' || process.env.KEMPO_DB === 'pglite'
|
|
193
|
+
? Promise.resolve()
|
|
194
|
+
: ensureEncryptionKey().then(([error]) => {
|
|
195
|
+
if(error) console.error(`[kempo] ${error.msg}`);
|
|
196
|
+
});
|
|
180
197
|
const PROJECT_PUBLIC = config.rootPath || join(process.cwd(), 'public');
|
|
181
198
|
const ADMIN_ROOT = resolveAdminRoot(config.adminRoot);
|
|
182
199
|
|
|
200
|
+
/*
|
|
201
|
+
A menu holds a copy of what an extension supplied, so it is brought up to date whenever extensions are
|
|
202
|
+
installed, enabled, disabled or removed: what an extension supplied leaves the site with it. Not at
|
|
203
|
+
start, when the database may not be answering yet and every extension would look absent.
|
|
204
|
+
*/
|
|
205
|
+
onExtensionsChanged(() => resyncMenus(PROJECT_PUBLIC));
|
|
206
|
+
|
|
183
207
|
return async (request, response, next) => {
|
|
208
|
+
await encryptionKeyReady;
|
|
184
209
|
const { path } = request;
|
|
185
210
|
const url = request.url.split('?')[0];
|
|
186
211
|
|
|
@@ -394,23 +419,34 @@ export default config => {
|
|
|
394
419
|
*/
|
|
395
420
|
const publicContentDirs = await extensionPublicDirs();
|
|
396
421
|
|
|
422
|
+
const loaded = [];
|
|
397
423
|
for(const ext of extensions){
|
|
398
424
|
const extRoot = join(NODE_MODULES, ext.name);
|
|
399
|
-
let kempo;
|
|
400
425
|
try {
|
|
401
|
-
({ kempo
|
|
402
|
-
} catch {
|
|
403
|
-
|
|
404
|
-
}
|
|
426
|
+
loaded.push({ extRoot, kempo: (await readKempoConfig(extRoot)).kempo });
|
|
427
|
+
} catch { /* not readable: nothing to route */ }
|
|
428
|
+
}
|
|
405
429
|
|
|
406
|
-
|
|
407
|
-
|
|
430
|
+
/*
|
|
431
|
+
An extension may answer on a short address it declares in `aliases` (`/cart` for `/commerce/cart`);
|
|
432
|
+
the routing below then treats the request as if it had arrived at the real address. A page the
|
|
433
|
+
site has of its own at that address always wins (see server/utils/extensions/aliases.js).
|
|
434
|
+
*/
|
|
435
|
+
const routedUrl = await resolveAlias(url, loaded, PROJECT_PUBLIC);
|
|
408
436
|
|
|
409
|
-
|
|
410
|
-
|
|
437
|
+
for(const { extRoot, kempo } of loaded){
|
|
438
|
+
/*
|
|
439
|
+
An extension may claim several scopes, each serving a folder of its package ("public-scope" serves
|
|
440
|
+
public/, "public-scopes" names others). Installing or enabling one that claims an address another
|
|
441
|
+
enabled extension has is refused, so at most one extension matches any request.
|
|
442
|
+
*/
|
|
443
|
+
const [, scopes] = normalizeScopes(kempo);
|
|
444
|
+
const claimed = scopes.find(({ scope }) => routedUrl === `/${scope}` || routedUrl.startsWith(`/${scope}/`));
|
|
445
|
+
if(!claimed) continue;
|
|
411
446
|
|
|
412
|
-
const
|
|
413
|
-
const
|
|
447
|
+
const scopePrefix = `/${claimed.scope}`;
|
|
448
|
+
const publicDir = join(extRoot, claimed.dir);
|
|
449
|
+
const subPath = routedUrl.slice(scopePrefix.length) || '/';
|
|
414
450
|
const method = request.method?.toUpperCase() || 'GET';
|
|
415
451
|
const resolveDir = buildResolveDir(PROJECT_PUBLIC, url);
|
|
416
452
|
const segments = subPath.replace(/^\//, '').split('/').filter(Boolean);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fullstack CMS framework for creating websites with user authentication",
|
|
6
6
|
"scripts": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"kempo-css": ">=2.2.5",
|
|
62
62
|
"kempo-server": ">=3.4.0",
|
|
63
|
-
"kempo-ui": ">=0.
|
|
63
|
+
"kempo-ui": ">=0.6.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@electric-sql/pglite": "^0.5.9",
|
package/server/db/schema.js
CHANGED
|
@@ -115,6 +115,29 @@ export const extension = pgTable('extension', {
|
|
|
115
115
|
updatedAt: timestamp('updatedAt').notNull(),
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
/*
|
|
119
|
+
Scheduled jobs: recurring work an extension declares in its kempo-config.json `jobs`. A row per job
|
|
120
|
+
is the schedule and the lease: `nextRunAt` is when it is next due and `lockedUntil` is held while a
|
|
121
|
+
process runs it, so several server processes sharing one database run each job once per interval.
|
|
122
|
+
`lastStatus` is 'ok' or 'error'.
|
|
123
|
+
*/
|
|
124
|
+
export const scheduledJob = pgTable('scheduledJob', {
|
|
125
|
+
id: text('id').primaryKey(),
|
|
126
|
+
owner: text('owner').notNull(),
|
|
127
|
+
name: text('name').notNull(),
|
|
128
|
+
everyMs: integer('everyMs').notNull(),
|
|
129
|
+
callback: text('callback').notNull(),
|
|
130
|
+
nextRunAt: timestamp('nextRunAt').notNull(),
|
|
131
|
+
lockedUntil: timestamp('lockedUntil'),
|
|
132
|
+
lastRunAt: timestamp('lastRunAt'),
|
|
133
|
+
lastStatus: text('lastStatus'),
|
|
134
|
+
lastError: text('lastError'),
|
|
135
|
+
createdAt: timestamp('createdAt').notNull(),
|
|
136
|
+
}, table => [
|
|
137
|
+
index('scheduledJob_owner_idx').on(table.owner),
|
|
138
|
+
index('scheduledJob_next_idx').on(table.nextRunAt),
|
|
139
|
+
]);
|
|
140
|
+
|
|
118
141
|
/*
|
|
119
142
|
Realtime System
|
|
120
143
|
*/
|
|
@@ -203,6 +226,7 @@ export const dashboardWidget = pgTable('dashboardWidget', {
|
|
|
203
226
|
title: text('title').notNull(),
|
|
204
227
|
description: text('description'),
|
|
205
228
|
icon: text('icon'),
|
|
229
|
+
iconSrc: text('iconSrc'),
|
|
206
230
|
element: text('element').notNull(),
|
|
207
231
|
module: text('module').notNull(),
|
|
208
232
|
defaultWidth: integer('defaultWidth').notNull().default(2),
|
package/server/sdk.js
CHANGED
|
@@ -144,6 +144,16 @@ export { default as enableGlobalContent } from './utils/global-content/enableGlo
|
|
|
144
144
|
export { default as getGlobalContent } from './utils/global-content/getGlobalContent.js';
|
|
145
145
|
export { default as listGlobalContent } from './utils/global-content/listGlobalContent.js';
|
|
146
146
|
export { default as setGlobalContentLocked } from './utils/global-content/setGlobalContentLocked.js';
|
|
147
|
+
export { default as listMenus } from './utils/menus/listMenus.js';
|
|
148
|
+
export { default as findNavItems } from './utils/menus/findNavItems.js';
|
|
149
|
+
export { default as saveMenu } from './utils/menus/saveMenu.js';
|
|
150
|
+
export { default as deleteMenu } from './utils/menus/deleteMenu.js';
|
|
151
|
+
export { default as assignMenuLocation } from './utils/menus/assignMenuLocation.js';
|
|
152
|
+
export { default as saveMenuItem } from './utils/menus/saveMenuItem.js';
|
|
153
|
+
export { default as deleteMenuItems } from './utils/menus/deleteMenuItems.js';
|
|
154
|
+
export { default as setMenuItemEnabled } from './utils/menus/setMenuItemEnabled.js';
|
|
155
|
+
export { default as reorderMenuItems } from './utils/menus/reorderMenuItems.js';
|
|
156
|
+
export { default as adoptMenuItem } from './utils/menus/adoptMenuItem.js';
|
|
147
157
|
export { default as updateGlobalContent } from './utils/global-content/updateGlobalContent.js';
|
|
148
158
|
|
|
149
159
|
/*
|
|
@@ -159,12 +169,23 @@ export { default as getAdminGlobalContent } from './utils/admin-global-content/g
|
|
|
159
169
|
export { default as listAdminGlobalContent } from './utils/admin-global-content/listAdminGlobalContent.js';
|
|
160
170
|
export { default as updateAdminGlobalContent } from './utils/admin-global-content/updateAdminGlobalContent.js';
|
|
161
171
|
|
|
172
|
+
/*
|
|
173
|
+
Scheduled Jobs
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
export { default as listJobs } from './utils/scheduler/listJobs.js';
|
|
177
|
+
export { default as registerJobs } from './utils/scheduler/registerJobs.js';
|
|
178
|
+
export { default as unregisterJobs } from './utils/scheduler/unregisterJobs.js';
|
|
179
|
+
export { runDueJobs, runJob } from './utils/scheduler/runJobs.js';
|
|
180
|
+
export { startScheduler, stopScheduler } from './utils/scheduler/scheduler.js';
|
|
181
|
+
|
|
162
182
|
/*
|
|
163
183
|
Email
|
|
164
184
|
*/
|
|
165
185
|
|
|
166
186
|
export { default as sendEmail } from './utils/email/sendEmail.js';
|
|
167
187
|
export { default as sendEmailFromTemplate } from './utils/email/sendEmailFromTemplate.js';
|
|
188
|
+
export { default as renderEmailTemplate } from './utils/email/renderEmailTemplate.js';
|
|
168
189
|
|
|
169
190
|
/*
|
|
170
191
|
Filesystem Utilities
|
|
@@ -7,7 +7,13 @@ import getSetting from '../settings/getSetting.js';
|
|
|
7
7
|
|
|
8
8
|
const generateToken = () => crypto.randomBytes(32).toString('hex');
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/*
|
|
11
|
+
`subject` and `template` let a caller send its own wording with the same link, for example an
|
|
12
|
+
extension that creates an account for someone at checkout and wants the email to say "set your
|
|
13
|
+
password". `template` is a name for sendEmailFromTemplate, including an extension's "owner/name".
|
|
14
|
+
The template receives `name` and `resetLink`.
|
|
15
|
+
*/
|
|
16
|
+
export default async ({ email, subject = 'Reset Your Password', template = 'reset-password' }) => {
|
|
11
17
|
if(!email){
|
|
12
18
|
return [{ code: 400, msg: 'Email is required' }, null];
|
|
13
19
|
}
|
|
@@ -42,8 +48,8 @@ export default async ({ email }) => {
|
|
|
42
48
|
|
|
43
49
|
const [emailError] = await sendEmailFromTemplate({
|
|
44
50
|
to: email,
|
|
45
|
-
subject
|
|
46
|
-
template
|
|
51
|
+
subject,
|
|
52
|
+
template,
|
|
47
53
|
data: {
|
|
48
54
|
name,
|
|
49
55
|
resetLink,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
2
|
import { verificationToken, user } from '../../db/schema.js';
|
|
3
3
|
import { eq, and, gt } from 'drizzle-orm';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ token }) => {
|
|
6
7
|
if(!token){
|
|
@@ -36,7 +37,15 @@ export default async ({ token }) => {
|
|
|
36
37
|
.delete(verificationToken)
|
|
37
38
|
.where(eq(verificationToken.token, token));
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
/*
|
|
41
|
+
Someone has just proved they own this address, which is what an extension needs before it hands
|
|
42
|
+
them something that was only ever tied to the address (guest orders, say). Fires
|
|
43
|
+
`user:email_verified` with `{ userId, email }`.
|
|
44
|
+
*/
|
|
45
|
+
const [verified] = await db.select({ email: user.email }).from(user).where(eq(user.id, tokenRecord.userId)).limit(1);
|
|
46
|
+
await triggerHook('user:email_verified', { userId: tokenRecord.userId, email: verified?.email ?? null });
|
|
47
|
+
|
|
48
|
+
return [null, {
|
|
40
49
|
success: true,
|
|
41
50
|
userId: tokenRecord.userId
|
|
42
51
|
}];
|
|
@@ -24,6 +24,7 @@ export const normalizeWidget = ({
|
|
|
24
24
|
title,
|
|
25
25
|
description = null,
|
|
26
26
|
icon = null,
|
|
27
|
+
iconSrc = null,
|
|
27
28
|
element,
|
|
28
29
|
module,
|
|
29
30
|
defaultWidth = 2,
|
|
@@ -57,6 +58,10 @@ export const normalizeWidget = ({
|
|
|
57
58
|
return [{ code: 400, msg: 'Icon must be an icon name' }, null];
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
if(iconSrc !== null && !(isLocalPath(iconSrc) && iconSrc.length <= 200)){
|
|
62
|
+
return [{ code: 400, msg: 'iconSrc must be the path to an icon file on this site, beginning with a single "/"' }, null];
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
if(typeof element !== 'string' || !ELEMENT_PATTERN.test(element)){
|
|
61
66
|
return [{ code: 400, msg: 'Element must be a custom element tag name such as "my-ext-widget"' }, null];
|
|
62
67
|
}
|
|
@@ -128,6 +133,7 @@ export const normalizeWidget = ({
|
|
|
128
133
|
title: title.trim(),
|
|
129
134
|
description,
|
|
130
135
|
icon,
|
|
136
|
+
iconSrc,
|
|
131
137
|
element,
|
|
132
138
|
module,
|
|
133
139
|
defaultWidth,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { renderPage, renderExternalPage } from 'kempo-server/templating';
|
|
5
|
+
import shake from 'kempo-css/shake';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Renders an email template to HTML, without sending it.
|
|
11
|
+
|
|
12
|
+
A plain name ("verify-email") is looked up in the site's `templates/emails`, then in kempo's own.
|
|
13
|
+
|
|
14
|
+
A name with an owner ("my-extension/order-paid") is an extension's template. The site may override
|
|
15
|
+
it at `templates/emails/my-extension/order-paid.page.html`; otherwise it is the extension's own
|
|
16
|
+
`emails/order-paid.page.html`. Either way the layout (`<page template="email">`) is the site's
|
|
17
|
+
`templates/emails/email.template.html` when it has one, and kempo's otherwise, so every email a site
|
|
18
|
+
sends looks the same.
|
|
19
|
+
|
|
20
|
+
Returns [error, html].
|
|
21
|
+
*/
|
|
22
|
+
export default async ({ template, data = {} } = {}) => {
|
|
23
|
+
if(!template){
|
|
24
|
+
return [{ code: 400, msg: 'Template name is required' }, null];
|
|
25
|
+
}
|
|
26
|
+
if(!/^[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)?$/.test(template)){
|
|
27
|
+
return [{ code: 400, msg: `Invalid email template name: ${template}` }, null];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const consumerDir = join(process.cwd(), 'templates/emails');
|
|
31
|
+
const moduleDir = join(__dirname, '../../../app-templates/emails');
|
|
32
|
+
const [owner, name] = template.includes('/') ? template.split('/') : [null, template];
|
|
33
|
+
|
|
34
|
+
let pagePath;
|
|
35
|
+
let rootDir;
|
|
36
|
+
if(owner){
|
|
37
|
+
const override = join(consumerDir, owner, `${name}.page.html`);
|
|
38
|
+
pagePath = existsSync(override) ? override : join(process.cwd(), 'node_modules', owner, 'emails', `${name}.page.html`);
|
|
39
|
+
rootDir = existsSync(join(consumerDir, 'email.template.html')) ? consumerDir : moduleDir;
|
|
40
|
+
} else {
|
|
41
|
+
rootDir = existsSync(join(consumerDir, `${name}.page.html`)) ? consumerDir : moduleDir;
|
|
42
|
+
pagePath = join(rootDir, `${name}.page.html`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if(!existsSync(pagePath)){
|
|
46
|
+
return [{ code: 404, msg: `Email template not found: ${template}` }, null];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const themePath = join(process.cwd(), 'public/styles.css');
|
|
50
|
+
const themeOpts = existsSync(themePath) ? { theme: themePath } : {};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
let html = owner
|
|
54
|
+
? await renderExternalPage(pagePath, rootDir, rootDir, {}, data)
|
|
55
|
+
: await renderPage(pagePath, rootDir, {}, data);
|
|
56
|
+
const css = shake(html, { ...themeOpts, colorMode: 'light' });
|
|
57
|
+
html = html.replace('</head>', `<style>${css}</style></head>`);
|
|
58
|
+
return [null, html];
|
|
59
|
+
} catch(error){
|
|
60
|
+
console.error('Email render error:', error);
|
|
61
|
+
return [{ code: 500, msg: 'Failed to render the email template' }, null];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
import { dirname, join } from 'path';
|
|
4
|
-
import { renderPage } from 'kempo-server/templating';
|
|
5
|
-
import shake from 'kempo-css/shake';
|
|
1
|
+
import renderEmailTemplate from './renderEmailTemplate.js';
|
|
6
2
|
import sendEmail from './sendEmail.js';
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/*
|
|
5
|
+
`template` is a plain name for kempo's and the site's templates, or "owner/name" for an extension's;
|
|
6
|
+
see renderEmailTemplate.js.
|
|
7
|
+
*/
|
|
11
8
|
export default async ({ to, subject, template, data = {} }) => {
|
|
12
9
|
if(!to){
|
|
13
10
|
return [{ code: 400, msg: 'Recipient email is required' }, null];
|
|
@@ -21,25 +18,10 @@ export default async ({ to, subject, template, data = {} }) => {
|
|
|
21
18
|
return [{ code: 400, msg: 'Template name is required' }, null];
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const pagePath = join(rootDir, `${template}.page.html`);
|
|
28
|
-
|
|
29
|
-
if(!existsSync(pagePath)){
|
|
30
|
-
return [{ code: 404, msg: `Email template not found: ${template}` }, null];
|
|
21
|
+
const [error, html] = await renderEmailTemplate({ template, data });
|
|
22
|
+
if(error){
|
|
23
|
+
return [error, null];
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
const themeOpts = existsSync(themePath) ? { theme: themePath } : {};
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
let html = await renderPage(pagePath, rootDir, {}, data);
|
|
38
|
-
const css = shake(html, { ...themeOpts, colorMode: 'light' });
|
|
39
|
-
html = html.replace('</head>', `<style>${css}</style></head>`);
|
|
40
|
-
return await sendEmail({ to, subject, html });
|
|
41
|
-
} catch(error){
|
|
42
|
-
console.error('Email render error:', error);
|
|
43
|
-
return [{ code: 500, msg: 'Failed to send email from template' }, null];
|
|
44
|
-
}
|
|
26
|
+
return await sendEmail({ to, subject, html });
|
|
45
27
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { stat } from 'fs/promises';
|
|
3
|
+
import { scopeNames } from './scopes.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Aliases let an extension answer on a short, familiar address instead of only under its public scope:
|
|
7
|
+
an extension with the scope `commerce` can declare `"aliases": { "/cart": "/commerce/cart" }` and its
|
|
8
|
+
cart page is then reachable at /cart/ too. The address stays as typed (it is a rewrite, not a redirect).
|
|
9
|
+
|
|
10
|
+
An alias must point inside one of the extension's own scopes, so an extension can only ever give a
|
|
11
|
+
second address to its own pages. It never shadows a page the site has of its own at that address, and
|
|
12
|
+
it cannot take over anything under /admin or /api. The alias address is a claim, like a scope, and no
|
|
13
|
+
other enabled extension may claim it (see claims.js).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const RESERVED = /^\/(admin|api|kempo|kempo-css|kempo-ui)(\/|$)/;
|
|
17
|
+
|
|
18
|
+
const trim = value => String(value).replace(/\/+$/, '');
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Checks the `aliases` an extension declares: { "/from": "/scope/to" }. `scope` is the extension's
|
|
22
|
+
scope, or the list of its scopes. Returns [error, aliases].
|
|
23
|
+
*/
|
|
24
|
+
export const normalizeAliases = (aliases, scope) => {
|
|
25
|
+
const scopes = [scope].flat().filter(Boolean);
|
|
26
|
+
if(aliases === undefined || aliases === null) return [null, {}];
|
|
27
|
+
if(typeof aliases !== 'object' || Array.isArray(aliases)) return [{ code: 400, msg: 'aliases must be an object of address to address' }, null];
|
|
28
|
+
|
|
29
|
+
const result = {};
|
|
30
|
+
for(const [from, to] of Object.entries(aliases)){
|
|
31
|
+
const source = trim(from);
|
|
32
|
+
const target = trim(to);
|
|
33
|
+
if(!source.startsWith('/') || source === '' || RESERVED.test(source)) return [{ code: 400, msg: `Alias "${from}" must be an address such as /cart, outside /admin and /api` }, null];
|
|
34
|
+
if(!scopes.some(name => target === `/${name}` || target.startsWith(`/${name}/`))) return [{ code: 400, msg: `Alias "${from}" must point inside the extension's own scope (/${scopes.join(', /') || '…'}/…)` }, null];
|
|
35
|
+
result[source] = target;
|
|
36
|
+
}
|
|
37
|
+
return [null, result];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/* True when the site itself has a page, directory or file at this address, which always wins over an alias. */
|
|
41
|
+
const siteHasPath = async (publicDir, address) => {
|
|
42
|
+
const base = join(publicDir, ...address.split('/').filter(Boolean));
|
|
43
|
+
const candidates = [base, `${base}.page.html`, `${base}.html`, join(base, 'index.page.html'), join(base, 'index.html'), join(base, 'index.js'), join(base, 'GET.js')];
|
|
44
|
+
for(const candidate of candidates){
|
|
45
|
+
try {
|
|
46
|
+
const info = await stat(candidate);
|
|
47
|
+
if(candidate === base ? info.isFile() : true) return true;
|
|
48
|
+
} catch { /* not there */ }
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
The address to serve for `url`: the alias target when an enabled extension declares one for it and
|
|
55
|
+
the site has no page there, otherwise `url` unchanged. `loaded` is [{ kempo }] for the enabled extensions.
|
|
56
|
+
*/
|
|
57
|
+
export const resolveAlias = async (url, loaded, publicDir) => {
|
|
58
|
+
const clean = trim(url) || '/';
|
|
59
|
+
for(const { kempo } of loaded){
|
|
60
|
+
const scopes = scopeNames(kempo);
|
|
61
|
+
if(!scopes.length || !kempo.aliases) continue;
|
|
62
|
+
const [error, aliases] = normalizeAliases(kempo.aliases, scopes);
|
|
63
|
+
if(error) continue;
|
|
64
|
+
for(const [from, to] of Object.entries(aliases)){
|
|
65
|
+
if(clean !== from && !clean.startsWith(`${from}/`)) continue;
|
|
66
|
+
if(await siteHasPath(publicDir, from)) return url;
|
|
67
|
+
return to + url.slice(from.length);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return url;
|
|
71
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import readKempoConfig from './readKempoConfig.js';
|
|
3
|
+
import { getEnabledExtensions } from './scopeCache.js';
|
|
4
|
+
import { findClaimConflicts, describeConflict } from './claims.js';
|
|
5
|
+
|
|
6
|
+
const configOf = async name => {
|
|
7
|
+
try {
|
|
8
|
+
return { name, kempo: (await readKempoConfig(join(process.cwd(), 'node_modules', name))).kempo };
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
Refuses to put an extension on the site when another one that is on already uses an address it
|
|
16
|
+
claims. "names" are the extensions about to be switched on (installed, or enabled together with what
|
|
17
|
+
they require) and "verb" is what is being done ("install", "enable" or "update"). Returns [error] with
|
|
18
|
+
a 409 and the whole list as "conflicts", or [null].
|
|
19
|
+
|
|
20
|
+
An extension whose package cannot be read is skipped, as it cannot be routed to anyway. "kempo" gives
|
|
21
|
+
the config of a name to check instead of reading it, for one that has just changed.
|
|
22
|
+
*/
|
|
23
|
+
export default async ({ names, verb, kempo = {} }) => {
|
|
24
|
+
const candidates = (await Promise.all(names.map(async name => kempo[name] ? { name, kempo: kempo[name] } : configOf(name)))).filter(Boolean);
|
|
25
|
+
const enabled = (await getEnabledExtensions()).map(row => row.name).filter(name => !names.includes(name));
|
|
26
|
+
const others = (await Promise.all(enabled.map(configOf))).filter(Boolean);
|
|
27
|
+
|
|
28
|
+
const conflicts = findClaimConflicts(candidates, others);
|
|
29
|
+
if(!conflicts.length) return [null];
|
|
30
|
+
|
|
31
|
+
return [{ code: 409, msg: describeConflict(conflicts[0], verb), conflicts }];
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { scopeNames } from './scopes.js';
|
|
2
|
+
import { normalizeAliases } from './aliases.js';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
An address an extension answers on is a *claim*, and the router can give an address to only one
|
|
6
|
+
extension. Whichever one it reaches first gets every request for it, and the other's pages, scripts
|
|
7
|
+
and API routes under it answer 404 without any error, so two extensions may not claim the same
|
|
8
|
+
address or one inside the other.
|
|
9
|
+
|
|
10
|
+
Every claim is a path: each of the extension's scopes (/products) and each of its alias addresses
|
|
11
|
+
(/cart). An alias that is invalid is left out here, since it is refused on its own.
|
|
12
|
+
*/
|
|
13
|
+
export const claimsOf = kempo => {
|
|
14
|
+
const names = scopeNames(kempo);
|
|
15
|
+
const [, aliases] = normalizeAliases(kempo?.aliases, names);
|
|
16
|
+
return [...names.map(scope => `/${scope}`), ...Object.keys(aliases ?? {})];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/* Two claims collide when they are the same address, or one is inside the other. */
|
|
20
|
+
export const overlaps = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
The collisions between extensions. "candidates" is what is about to be installed or switched on and
|
|
24
|
+
"others" what is on already, both as [{ name, kempo }]. Candidates are checked against each other as
|
|
25
|
+
well. Returns [{ name, claim, other, otherClaim }], one per colliding pair of claims. An extension
|
|
26
|
+
never collides with itself, so one that claims a scope and an alias in another of its own is fine.
|
|
27
|
+
*/
|
|
28
|
+
export const findClaimConflicts = (candidates, others) => {
|
|
29
|
+
const withClaims = list => list.map(({ name, kempo }) => ({ name, claims: claimsOf(kempo) }));
|
|
30
|
+
const mine = withClaims(candidates);
|
|
31
|
+
const theirs = withClaims(others);
|
|
32
|
+
const conflicts = [];
|
|
33
|
+
|
|
34
|
+
const compare = (left, right) => {
|
|
35
|
+
for(const claim of left.claims){
|
|
36
|
+
for(const otherClaim of right.claims){
|
|
37
|
+
if(overlaps(claim, otherClaim)) conflicts.push({ name: left.name, claim, other: right.name, otherClaim });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
mine.forEach((left, index) => {
|
|
43
|
+
theirs.filter(right => right.name !== left.name).forEach(right => compare(left, right));
|
|
44
|
+
mine.slice(index + 1).forEach(right => compare(left, right));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return conflicts;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/* The sentence a person is shown, naming who holds the address and what to do about it. */
|
|
51
|
+
export const describeConflict = ({ name, claim, other, otherClaim }, verb) => {
|
|
52
|
+
const what = claim === otherClaim ? otherClaim : `${otherClaim}, which overlaps ${claim}`;
|
|
53
|
+
return `Cannot ${verb} ${name}: ${other} already uses ${what}. Disable or uninstall ${other} first.`;
|
|
54
|
+
};
|
|
@@ -2,24 +2,18 @@ import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
3
|
import db from '../../db/index.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
PgTimestamp: 'timestamp',
|
|
18
|
-
PgDate: 'date',
|
|
19
|
-
PgTime: 'time',
|
|
20
|
-
PgJsonb: 'jsonb',
|
|
21
|
-
PgJson: 'json',
|
|
22
|
-
PgUUID: 'uuid',
|
|
5
|
+
/*
|
|
6
|
+
A column's SQL type is whatever Drizzle says it is (`bigserial`, `varchar(255)`, `timestamp`, `text[]`), never a
|
|
7
|
+
guess. A hand-kept map of types once sent `bigserial` to `text` with no sequence, which made a table that could
|
|
8
|
+
not take an insert. An enum has a type of its own that does not exist here yet, so it is stored as text.
|
|
9
|
+
*/
|
|
10
|
+
const sqlTypeOf = column => {
|
|
11
|
+
if(column.columnType === 'PgEnumColumn') return 'text';
|
|
12
|
+
try {
|
|
13
|
+
return column.getSQLType();
|
|
14
|
+
} catch {
|
|
15
|
+
return 'text';
|
|
16
|
+
}
|
|
23
17
|
};
|
|
24
18
|
|
|
25
19
|
const serializeDefault = (value) => {
|
|
@@ -33,8 +27,7 @@ const tableToSql = (table) => {
|
|
|
33
27
|
const config = getTableConfig(table);
|
|
34
28
|
const primaryKeys = config.columns.filter(c => c.primary).map(c => c.name);
|
|
35
29
|
const cols = config.columns.map(c => {
|
|
36
|
-
|
|
37
|
-
let def = `"${c.name}" ${type}`;
|
|
30
|
+
let def = `"${c.name}" ${sqlTypeOf(c)}`;
|
|
38
31
|
if(c.notNull) def += ' NOT NULL';
|
|
39
32
|
if(c.hasDefault && c.default !== undefined) def += ` DEFAULT ${serializeDefault(c.default)}`;
|
|
40
33
|
if(c.primary && primaryKeys.length === 1) def += ' PRIMARY KEY';
|
|
@@ -3,6 +3,7 @@ import { extension } from '../../db/schema.js';
|
|
|
3
3
|
import { eq, inArray } from 'drizzle-orm';
|
|
4
4
|
import { invalidateScopeCache } from './scopeCache.js';
|
|
5
5
|
import getRequirementTree from './getRequirementTree.js';
|
|
6
|
+
import checkClaims from './checkClaims.js';
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
Switches an extension on. It cannot run without what it requires, so any of that which is installed
|
|
@@ -40,6 +41,13 @@ export default async ({ name, cascade = false }) => {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const names = [name, ...disabled.map(requirement => requirement.name)];
|
|
44
|
+
|
|
45
|
+
/* Switching on what claims an address another enabled extension already has would silently break one of them. */
|
|
46
|
+
const [claimsError] = await checkClaims({ names, verb: 'enable' });
|
|
47
|
+
if(claimsError){
|
|
48
|
+
return [claimsError, null];
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
await db.update(extension)
|
|
44
52
|
.set({ enabled: true, updatedAt: new Date() })
|
|
45
53
|
.where(inArray(extension.name, names));
|