domma-cms 0.55.1 → 0.69.3
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/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -578,6 +578,24 @@ export async function formsRoutes(fastify) {
|
|
|
578
578
|
formValues[field.name] = val !== undefined ? (typeof val === 'string' ? val.trim() : val) : '';
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
// Triggers, from the same engine the browser ran. A block-submit that
|
|
582
|
+
// only existed in the browser would be a suggestion: anyone can re-enable
|
|
583
|
+
// a button. Resolved BEFORE the field pass because a trigger decides
|
|
584
|
+
// which fields are in play at all.
|
|
585
|
+
const triggerState = FormLogicEngine.resolveTriggerState(form.fields || [], formValues);
|
|
586
|
+
|
|
587
|
+
if (triggerState.blocks.length) {
|
|
588
|
+
return reply.status(400).send({
|
|
589
|
+
error: triggerState.blocks.map(b => b.message).join(' ')
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Hidden and disabled are the same thing to a submission: the visitor
|
|
594
|
+
// was given no way to answer, so the answer is not theirs and is not
|
|
595
|
+
// required. A value posted for one anyway is dropped below.
|
|
596
|
+
const triggerOff = new Set([...triggerState.hidden, ...triggerState.disabled]);
|
|
597
|
+
const triggerRequired = new Set(triggerState.required);
|
|
598
|
+
|
|
581
599
|
// Evaluate conditional logic
|
|
582
600
|
const missingFields = [];
|
|
583
601
|
const validationErrors = [];
|
|
@@ -587,11 +605,13 @@ export async function formsRoutes(fastify) {
|
|
|
587
605
|
if (field.type === 'page-break' || field.type === 'spacer') continue;
|
|
588
606
|
const vis = FormLogicEngine.evaluateFieldVisibility(field, formValues);
|
|
589
607
|
if (vis === 'hidden') continue;
|
|
608
|
+
if (triggerOff.has(field.name)) continue;
|
|
590
609
|
visibleFieldNames.add(field.name);
|
|
591
610
|
|
|
592
611
|
const value = formValues[field.name];
|
|
593
612
|
const isEmpty = !value?.toString().trim();
|
|
594
|
-
const required = FormLogicEngine.evaluateFieldRequirement(field, formValues)
|
|
613
|
+
const required = FormLogicEngine.evaluateFieldRequirement(field, formValues)
|
|
614
|
+
|| triggerRequired.has(field.name);
|
|
595
615
|
if (required && isEmpty) {
|
|
596
616
|
missingFields.push(field.label || field.name);
|
|
597
617
|
}
|
|
@@ -719,10 +739,29 @@ export async function formsRoutes(fastify) {
|
|
|
719
739
|
}
|
|
720
740
|
}
|
|
721
741
|
|
|
742
|
+
// Trigger events the server owns. Fired after the entry is stored, for
|
|
743
|
+
// the same reason the other actions are: a failed side effect must not
|
|
744
|
+
// cost the visitor their submission.
|
|
745
|
+
let triggerRedirect = null;
|
|
746
|
+
for (const event of triggerState.events) {
|
|
747
|
+
if (event.type === 'run-action' && event.action.slug && entry) {
|
|
748
|
+
try {
|
|
749
|
+
await executeAction(event.action.slug, entry.id, {user: submittingUser});
|
|
750
|
+
} catch (err) {
|
|
751
|
+
fastify.log.warn(`[forms] Trigger action "${event.action.slug}" failed for form "${slug}": ${err.message}`);
|
|
752
|
+
}
|
|
753
|
+
} else if (event.type === 'redirect' && event.action.url && !triggerRedirect) {
|
|
754
|
+
// First one wins - two triggers redirecting at once is an
|
|
755
|
+
// authoring mistake, not a case to resolve cleverly.
|
|
756
|
+
triggerRedirect = String(event.action.url);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
722
760
|
hooks.emit('form:submitted', {slug, entryId: entry?.id || null, data, formTitle: form.title});
|
|
723
761
|
|
|
724
|
-
// Template interpolation for successRedirect
|
|
725
|
-
|
|
762
|
+
// Template interpolation for successRedirect. A trigger's redirect is
|
|
763
|
+
// the more specific instruction, so it wins over the form's default.
|
|
764
|
+
let redirect = triggerRedirect || settings.successRedirect || null;
|
|
726
765
|
if (redirect && entry?.id) {
|
|
727
766
|
redirect = redirect.replace(/\{\{entryId\}\}/g, entry.id);
|
|
728
767
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Notes API - /api/notes
|
|
3
|
+
*
|
|
4
|
+
* Was the `notes` plugin until 0.67.0. The logic is unchanged; what moved is
|
|
5
|
+
* where it lives and how it is gated.
|
|
6
|
+
*
|
|
7
|
+
* **Per-user scope.** Entries carry a `userId` and every read is filtered to the
|
|
8
|
+
* caller's own. The plugin made this configurable (`scope: 'user' | 'site'`) and
|
|
9
|
+
* no install ever set it to anything else, so it is a constant here. Note this
|
|
10
|
+
* is hand-filtering rather than `rowAccess.js`, which is the mechanism core has
|
|
11
|
+
* for exactly this - moving onto it is a behaviour change and belongs in its own
|
|
12
|
+
* task, not smuggled into a relocation.
|
|
13
|
+
*
|
|
14
|
+
* **The collection slug is `notes-notes`, not the directory it used to sit in.**
|
|
15
|
+
* `setupPlugin()` read `schema.slug`, so the live collection has always been
|
|
16
|
+
* called this, and existing entries are found only if it stays exactly so.
|
|
17
|
+
*/
|
|
18
|
+
import {ago, clip, plural} from '../../services/sidebarBadges.js';
|
|
19
|
+
import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
|
|
20
|
+
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
|
|
3
21
|
|
|
4
22
|
const SLUG = 'notes-notes';
|
|
5
23
|
|
|
@@ -14,13 +32,20 @@ function toNote(entry) {
|
|
|
14
32
|
};
|
|
15
33
|
}
|
|
16
34
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Auth is injectable so a test can drive these routes without registering
|
|
37
|
+
* @fastify/jwt or a role store - the same shape dashboard.js and plugins.js use.
|
|
38
|
+
*
|
|
39
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
40
|
+
* @param {object} [opts]
|
|
41
|
+
*/
|
|
42
|
+
export async function notesRoutes(fastify, opts = {}) {
|
|
43
|
+
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
44
|
+
const requirePermission = opts.requirePermission || defaultRequirePermission;
|
|
45
|
+
|
|
21
46
|
|
|
22
47
|
function userId(request) {
|
|
23
|
-
return
|
|
48
|
+
return request.user?.id ?? request.user?.sub ?? null;
|
|
24
49
|
}
|
|
25
50
|
|
|
26
51
|
async function loadAll(uid) {
|
|
@@ -30,8 +55,27 @@ export default async function notesPlugin(fastify, options) {
|
|
|
30
55
|
return items;
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
/** GET /api/
|
|
34
|
-
fastify.get('/
|
|
58
|
+
/** GET /api/notes/count - the sidebar badge: how many notes this user has. */
|
|
59
|
+
fastify.get('/count', {preHandler: [authenticate, requirePermission('notes', 'read')]}, async (request) => {
|
|
60
|
+
const notes = await loadAll(userId(request)); // newest edit first
|
|
61
|
+
const categories = new Set(notes.flatMap(n => (Array.isArray(n.categories) ? n.categories : []).map(c => String(c).toLowerCase())));
|
|
62
|
+
return {
|
|
63
|
+
count: notes.length, label: plural(notes.length, 'note'), tone: 'info',
|
|
64
|
+
details: {
|
|
65
|
+
title: 'Notes',
|
|
66
|
+
rows: [
|
|
67
|
+
{label: 'Notes', value: String(notes.length)},
|
|
68
|
+
{label: 'Categories', value: String(categories.size)},
|
|
69
|
+
{label: 'Edited this week', value: String(notes.filter(n => Date.now() - new Date(n.updatedAt).getTime() < 7 * 86_400_000).length)}
|
|
70
|
+
],
|
|
71
|
+
items: notes.slice(0, 4).map(n => ({text: clip(n.title || n.content || 'Untitled'), meta: `edited ${ago(n.updatedAt)}`})),
|
|
72
|
+
empty: 'No notes yet.'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** GET /api/notes/items - supports ?q= and ?category= */
|
|
78
|
+
fastify.get('/items', {preHandler: [authenticate, requirePermission('notes', 'read')]}, async (request, reply) => {
|
|
35
79
|
const uid = userId(request);
|
|
36
80
|
const { q, category } = request.query;
|
|
37
81
|
|
|
@@ -55,8 +99,8 @@ export default async function notesPlugin(fastify, options) {
|
|
|
55
99
|
return reply.send(items);
|
|
56
100
|
});
|
|
57
101
|
|
|
58
|
-
/** POST /api/
|
|
59
|
-
fastify.post('/items', {
|
|
102
|
+
/** POST /api/notes/items */
|
|
103
|
+
fastify.post('/items', {preHandler: [authenticate, requirePermission('notes', 'create')]}, async (request, reply) => {
|
|
60
104
|
const uid = userId(request);
|
|
61
105
|
const { title, content, categories } = request.body ?? {};
|
|
62
106
|
|
|
@@ -72,8 +116,8 @@ export default async function notesPlugin(fastify, options) {
|
|
|
72
116
|
return reply.code(201).send(toNote(entry));
|
|
73
117
|
});
|
|
74
118
|
|
|
75
|
-
/** PUT /api/
|
|
76
|
-
fastify.put('/items/:id', {
|
|
119
|
+
/** PUT /api/notes/items/:id */
|
|
120
|
+
fastify.put('/items/:id', {preHandler: [authenticate, requirePermission('notes', 'update')]}, async (request, reply) => {
|
|
77
121
|
const uid = userId(request);
|
|
78
122
|
const { id } = request.params;
|
|
79
123
|
const { title, content, categories } = request.body ?? {};
|
|
@@ -95,8 +139,8 @@ export default async function notesPlugin(fastify, options) {
|
|
|
95
139
|
return reply.send(toNote(updated));
|
|
96
140
|
});
|
|
97
141
|
|
|
98
|
-
/** DELETE /api/
|
|
99
|
-
fastify.delete('/items/:id', {
|
|
142
|
+
/** DELETE /api/notes/items/:id */
|
|
143
|
+
fastify.delete('/items/:id', {preHandler: [authenticate, requirePermission('notes', 'delete')]}, async (request, reply) => {
|
|
100
144
|
const uid = userId(request);
|
|
101
145
|
const { id } = request.params;
|
|
102
146
|
|
|
@@ -108,8 +152,8 @@ export default async function notesPlugin(fastify, options) {
|
|
|
108
152
|
return reply.send({ success: true });
|
|
109
153
|
});
|
|
110
154
|
|
|
111
|
-
/** GET /api/
|
|
112
|
-
fastify.get('/categories', {
|
|
155
|
+
/** GET /api/notes/categories */
|
|
156
|
+
fastify.get('/categories', {preHandler: [authenticate, requirePermission('notes', 'read')]}, async (request, reply) => {
|
|
113
157
|
const uid = userId(request);
|
|
114
158
|
const items = await loadAll(uid);
|
|
115
159
|
const cats = new Set();
|
|
@@ -7,13 +7,63 @@
|
|
|
7
7
|
* POST /api/system/notifications/:id/dismiss - Mark dismissed
|
|
8
8
|
* DELETE /api/system/notifications/:id - Hard delete (admin only)
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
authenticate as defaultAuthenticate,
|
|
12
|
+
requirePermission as defaultRequirePermission
|
|
13
|
+
} from '../../middleware/auth.js';
|
|
11
14
|
import { requireManager } from '../../middleware/managerAuth.js';
|
|
12
15
|
import { getAdapter } from '../../services/adapterRegistry.js';
|
|
13
16
|
|
|
14
17
|
const SLUG = 'notifications';
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
/** Ids already warned about, so one bad record does not fill the log. */
|
|
20
|
+
const warnedMalformed = new Set();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A notification's payload, or null when the record is not a notification.
|
|
24
|
+
*
|
|
25
|
+
* Entries are `{id, data, meta}`, but this collection has been written to
|
|
26
|
+
* directly before now - a form once pointed its submissions at `notifications`
|
|
27
|
+
* and left flat records behind with no `data` at all. Reading `entry.data.x`
|
|
28
|
+
* off one of those threw, and because the bell polls on every admin page load,
|
|
29
|
+
* a single stray record turned into a 500 on every screen in the admin. One
|
|
30
|
+
* unreadable row is skipped; it is not the rest of the inbox's problem.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} entry
|
|
33
|
+
* @param {object} [log] - Fastify logger, when there is one to warn through.
|
|
34
|
+
* @returns {object|null}
|
|
35
|
+
*/
|
|
36
|
+
function notificationData(entry, log) {
|
|
37
|
+
const data = entry && entry.data;
|
|
38
|
+
if (data && typeof data === 'object') return data;
|
|
39
|
+
const id = entry?.id || '(no id)';
|
|
40
|
+
if (log && !warnedMalformed.has(id)) {
|
|
41
|
+
warnedMalformed.add(id);
|
|
42
|
+
log.warn(`[notifications] Skipping malformed entry "${id}" - no data object. `
|
|
43
|
+
+ 'Something wrote to the notifications collection directly.');
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Is this notification live for this user right now: not expired, not dismissed.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} data - A notification payload.
|
|
52
|
+
* @param {string} userId
|
|
53
|
+
* @param {string} now - ISO timestamp.
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isActiveFor(data, userId, now) {
|
|
57
|
+
if (data.expiresAt && data.expiresAt < now) return false;
|
|
58
|
+
return !(Array.isArray(data.dismissedBy) && data.dismissedBy.includes(userId));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function notificationsRoutes(fastify, opts = {}) {
|
|
62
|
+
// Injectable so tests can supply no-ops rather than a seeded roles cache -
|
|
63
|
+
// the same DI pattern as menus.js, projects.js and context-menus.js.
|
|
64
|
+
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
65
|
+
const requirePermission = opts.requirePermission || defaultRequirePermission;
|
|
66
|
+
|
|
17
67
|
const canRead = { preHandler: [authenticate, requirePermission('notifications', 'read')] };
|
|
18
68
|
const canDelete = { preHandler: [authenticate, requirePermission('notifications', 'delete')] };
|
|
19
69
|
|
|
@@ -69,14 +119,12 @@ export async function notificationsRoutes(fastify) {
|
|
|
69
119
|
|
|
70
120
|
return all
|
|
71
121
|
.filter(entry => {
|
|
72
|
-
const d = entry.
|
|
73
|
-
|
|
74
|
-
if (d.expiresAt && d.expiresAt < now) return false;
|
|
75
|
-
// Exclude dismissed by this user
|
|
76
|
-
if (Array.isArray(d.dismissedBy) && d.dismissedBy.includes(userId)) return false;
|
|
77
|
-
return true;
|
|
122
|
+
const d = notificationData(entry, fastify.log);
|
|
123
|
+
return !!d && isActiveFor(d, userId, now);
|
|
78
124
|
})
|
|
79
|
-
|
|
125
|
+
// A record with no createdAt sorts last rather than taking the
|
|
126
|
+
// whole list down with it.
|
|
127
|
+
.sort((a, b) => String(b.data.createdAt || '').localeCompare(String(a.data.createdAt || '')))
|
|
80
128
|
.map(entry => ({
|
|
81
129
|
...entry,
|
|
82
130
|
unread: !Array.isArray(entry.data.readBy) || !entry.data.readBy.includes(userId)
|
|
@@ -91,9 +139,8 @@ export async function notificationsRoutes(fastify) {
|
|
|
91
139
|
const all = await adapter.all(SLUG);
|
|
92
140
|
|
|
93
141
|
const count = all.filter(entry => {
|
|
94
|
-
const d = entry.
|
|
95
|
-
if (d
|
|
96
|
-
if (Array.isArray(d.dismissedBy) && d.dismissedBy.includes(userId)) return false;
|
|
142
|
+
const d = notificationData(entry, fastify.log);
|
|
143
|
+
if (!d || !isActiveFor(d, userId, now)) return false;
|
|
97
144
|
return !Array.isArray(d.readBy) || !d.readBy.includes(userId);
|
|
98
145
|
}).length;
|
|
99
146
|
|
|
@@ -107,13 +154,15 @@ export async function notificationsRoutes(fastify) {
|
|
|
107
154
|
const adapter = await getAdapter(SLUG);
|
|
108
155
|
const entry = await adapter.get(SLUG, id);
|
|
109
156
|
|
|
110
|
-
|
|
157
|
+
const data = notificationData(entry, fastify.log);
|
|
158
|
+
// A record the list never showed is not one this route can mark read.
|
|
159
|
+
if (!data) return reply.code(404).send({ error: 'Notification not found.' });
|
|
111
160
|
|
|
112
|
-
const readBy = Array.isArray(
|
|
161
|
+
const readBy = Array.isArray(data.readBy) ? data.readBy : [];
|
|
113
162
|
if (readBy.includes(userId)) return { ok: true }; // already read - idempotent
|
|
114
163
|
|
|
115
164
|
const updated = await adapter.update(SLUG, id, {
|
|
116
|
-
...
|
|
165
|
+
...data,
|
|
117
166
|
readBy: [...readBy, userId]
|
|
118
167
|
});
|
|
119
168
|
|
|
@@ -127,13 +176,14 @@ export async function notificationsRoutes(fastify) {
|
|
|
127
176
|
const adapter = await getAdapter(SLUG);
|
|
128
177
|
const entry = await adapter.get(SLUG, id);
|
|
129
178
|
|
|
130
|
-
|
|
179
|
+
const data = notificationData(entry, fastify.log);
|
|
180
|
+
if (!data) return reply.code(404).send({ error: 'Notification not found.' });
|
|
131
181
|
|
|
132
|
-
const dismissedBy = Array.isArray(
|
|
133
|
-
const readBy = Array.isArray(
|
|
182
|
+
const dismissedBy = Array.isArray(data.dismissedBy) ? data.dismissedBy : [];
|
|
183
|
+
const readBy = Array.isArray(data.readBy) ? data.readBy : [];
|
|
134
184
|
|
|
135
185
|
const updated = await adapter.update(SLUG, id, {
|
|
136
|
-
...
|
|
186
|
+
...data,
|
|
137
187
|
dismissedBy: dismissedBy.includes(userId) ? dismissedBy : [...dismissedBy, userId],
|
|
138
188
|
readBy: readBy.includes(userId) ? readBy : [...readBy, userId] // dismissing also marks read
|
|
139
189
|
});
|
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin Marketplace API
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Plugin Marketplace API - the managed (fleet) path.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/plugins/marketplace - the catalogue as this site sees it
|
|
5
|
+
* POST /api/plugins/marketplace/install - fetch a bundle and install it
|
|
6
|
+
* DELETE /api/plugins/marketplace/:slug - uninstall
|
|
7
|
+
*
|
|
8
|
+
* This is delivery path A: a site the manager forked, buying from the fleet's
|
|
9
|
+
* own catalogue over loopback. The standalone path - buy, download one file,
|
|
10
|
+
* Install from File - goes through `POST /plugins/install-upload` instead and
|
|
11
|
+
* shares nothing with this but the installer.
|
|
12
|
+
*
|
|
13
|
+
* An unmanaged site has no MANAGER_URL, every route here answers "not
|
|
14
|
+
* available", and the screen says so in terms that fit that customer.
|
|
6
15
|
*
|
|
7
16
|
* All routes require authentication + admin role.
|
|
8
17
|
*/
|
|
9
18
|
import {authenticate, requireAdmin} from '../../middleware/auth.js';
|
|
10
|
-
import {fetchCatalogue, fetchPluginBundle} from '../../services/managerClient.js';
|
|
11
|
-
import {installPlugin, uninstallPlugin} from '../../services/pluginInstaller.js';
|
|
19
|
+
import {fetchCatalogue, fetchPluginBundle, isManaged, siteSlug} from '../../services/managerClient.js';
|
|
20
|
+
import {compareVersions, installPlugin, uninstallPlugin} from '../../services/pluginInstaller.js';
|
|
21
|
+
import {discoverPlugins} from '../../services/plugins.js';
|
|
12
22
|
|
|
13
23
|
export async function pluginMarketplaceRoutes(fastify) {
|
|
14
24
|
const adminOnly = { preHandler: [authenticate, requireAdmin] };
|
|
15
25
|
|
|
16
26
|
// GET /plugins/marketplace
|
|
17
27
|
fastify.get('/plugins/marketplace', adminOnly, async (_request, reply) => {
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
if (!isManaged()) return reply.send({ available: false, catalogue: [] });
|
|
29
|
+
|
|
30
|
+
const catalogue = await fetchCatalogue();
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
// What is on THIS disk is a local fact - the manager's ledger knows
|
|
33
|
+
// what a site is licensed for, never what it actually has. Joined here
|
|
34
|
+
// so the card can offer Update rather than an Install that would come
|
|
35
|
+
// back 409.
|
|
36
|
+
const installed = new Map((await discoverPlugins()).map(m => [m.name, m.version]));
|
|
37
|
+
return reply.send({
|
|
38
|
+
available: true,
|
|
39
|
+
site: siteSlug(),
|
|
40
|
+
catalogue: catalogue.map(entry => {
|
|
41
|
+
const have = installed.get(entry.slug);
|
|
42
|
+
return {
|
|
43
|
+
...entry,
|
|
44
|
+
installed: have !== undefined,
|
|
45
|
+
installedVersion: have ?? null,
|
|
46
|
+
updatable: have !== undefined && compareVersions(entry.version, have) > 0
|
|
47
|
+
};
|
|
48
|
+
})
|
|
49
|
+
});
|
|
25
50
|
});
|
|
26
51
|
|
|
27
52
|
// POST /plugins/marketplace/install
|
|
28
53
|
fastify.post('/plugins/marketplace/install', adminOnly, async (request, reply) => {
|
|
29
|
-
const { slug, version } = request.body || {};
|
|
54
|
+
const { slug, version, update } = request.body || {};
|
|
30
55
|
|
|
31
56
|
if (!slug || !/^[a-z0-9][a-z0-9-_]{0,63}$/.test(slug)) {
|
|
32
57
|
return reply.code(400).send({ error: 'Invalid plugin slug.' });
|
|
@@ -34,25 +59,55 @@ export async function pluginMarketplaceRoutes(fastify) {
|
|
|
34
59
|
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
35
60
|
return reply.code(400).send({ error: 'Invalid version format. Expected semver (e.g. 1.0.0).' });
|
|
36
61
|
}
|
|
62
|
+
if (!isManaged()) {
|
|
63
|
+
return reply.code(503).send({ error: 'This site is not managed - install from a file instead.' });
|
|
64
|
+
}
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
let bundle;
|
|
67
|
+
try {
|
|
68
|
+
bundle = await fetchPluginBundle(slug, version);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// A refusal from the manager is the answer, not a failure: "this
|
|
71
|
+
// site has no licence for invoice" is what the admin needs to read.
|
|
72
|
+
return reply.code(403).send({ error: err.message });
|
|
73
|
+
}
|
|
41
74
|
if (!bundle) {
|
|
42
|
-
return reply.code(503).send({ error: '
|
|
75
|
+
return reply.code(503).send({ error: 'The manager is not reachable right now.' });
|
|
43
76
|
}
|
|
44
77
|
|
|
45
78
|
try {
|
|
46
|
-
await installPlugin(slug,
|
|
79
|
+
const result = await installPlugin(slug, {
|
|
80
|
+
tarball: bundle.tarball,
|
|
81
|
+
signature: bundle.signature,
|
|
82
|
+
publicKeyId: bundle.publicKeyId,
|
|
83
|
+
// Minted for this site by the manager. Without it a paid plugin
|
|
84
|
+
// installs and then refuses to load, which reads as a broken
|
|
85
|
+
// install rather than a licensing answer.
|
|
86
|
+
entitlement: bundle.entitlement,
|
|
87
|
+
// Same plugin already here means update, which the admin asked
|
|
88
|
+
// for by pressing Update rather than Install.
|
|
89
|
+
allowUpgrade: update === true
|
|
90
|
+
});
|
|
91
|
+
return reply.send({
|
|
92
|
+
success: true,
|
|
93
|
+
name: slug,
|
|
94
|
+
action: result?.action ?? 'install',
|
|
95
|
+
from: result?.from ?? null,
|
|
96
|
+
version: result?.version ?? bundle.version,
|
|
97
|
+
restartRequired: true
|
|
98
|
+
});
|
|
47
99
|
} catch (err) {
|
|
48
100
|
fastify.log.error({ err }, '[marketplace] install failed');
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
// Messages the installer raises deliberately, all of them things an
|
|
102
|
+
// admin can act on. Anything else is kept vague on purpose - an
|
|
103
|
+
// unexpected error can carry a path from this server.
|
|
104
|
+
const safe = [
|
|
105
|
+
'Plugin already installed', 'Plugin signature invalid', 'Unknown public key',
|
|
106
|
+
'Invalid plugin slug', 'Refusing to downgrade', 'Refusing to upgrade',
|
|
107
|
+
'This site accepts signed plugins only'
|
|
108
|
+
].some(m => err.message.startsWith(m));
|
|
52
109
|
return reply.code(400).send({ error: safe ? err.message : 'Installation failed.' });
|
|
53
110
|
}
|
|
54
|
-
|
|
55
|
-
return reply.send({ success: true });
|
|
56
111
|
});
|
|
57
112
|
|
|
58
113
|
// DELETE /plugins/marketplace/:slug
|