domma-cms 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +123 -21
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/plugins.json +19 -29
  69. package/config/server.json +6 -6
  70. package/config/site.json +12 -2
  71. package/package.json +24 -10
  72. package/plugins/example-analytics/stats.json +17 -12
  73. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  74. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  75. package/plugins/theme-roller/config.js +1 -0
  76. package/plugins/theme-roller/plugin.js +233 -0
  77. package/plugins/theme-roller/plugin.json +31 -0
  78. package/plugins/theme-roller/public/active-theme.css +0 -0
  79. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  80. package/public/css/forms.css +1 -0
  81. package/public/css/site.css +1 -1
  82. package/public/js/forms.js +1 -0
  83. package/public/js/site.js +1 -1
  84. package/scripts/build.js +194 -129
  85. package/scripts/pro.js +254 -0
  86. package/scripts/reset.js +33 -8
  87. package/scripts/seed.js +343 -78
  88. package/scripts/setup.js +1 -0
  89. package/server/middleware/auth.js +136 -120
  90. package/server/routes/api/actions.js +200 -0
  91. package/server/routes/api/auth.js +292 -146
  92. package/server/routes/api/blocks.js +84 -0
  93. package/server/routes/api/collections.js +79 -27
  94. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  95. package/server/routes/api/layouts.js +49 -39
  96. package/server/routes/api/media.js +118 -92
  97. package/server/routes/api/navigation.js +40 -36
  98. package/server/routes/api/pages.js +132 -118
  99. package/server/routes/api/plugins.js +6 -3
  100. package/server/routes/api/settings.js +104 -88
  101. package/server/routes/api/users.js +27 -19
  102. package/server/routes/api/views.js +148 -0
  103. package/server/routes/public.js +124 -108
  104. package/server/server.js +269 -181
  105. package/server/services/actions.js +387 -0
  106. package/server/services/adapterRegistry.js +98 -0
  107. package/server/services/adapters/FileAdapter.js +192 -0
  108. package/server/services/adapters/MongoAdapter.js +220 -0
  109. package/server/services/blocks.js +162 -0
  110. package/server/services/collections.js +74 -86
  111. package/server/services/connectionManager.js +102 -0
  112. package/server/services/content.js +312 -307
  113. package/server/services/email.js +126 -0
  114. package/server/services/forms.js +173 -0
  115. package/server/services/markdown.js +1378 -747
  116. package/server/services/permissionRegistry.js +173 -0
  117. package/server/services/presetCollections.js +251 -0
  118. package/server/services/renderer.js +75 -1
  119. package/server/services/roles.js +227 -0
  120. package/server/services/rowAccess.js +104 -0
  121. package/server/services/userProfiles.js +199 -0
  122. package/server/services/users.js +281 -212
  123. package/server/services/views.js +280 -0
  124. package/server/templates/page.html +119 -113
  125. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  126. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  127. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  128. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  129. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  130. package/plugins/form-builder/config.js +0 -9
  131. package/plugins/form-builder/data/forms/consent.json +0 -104
  132. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  133. package/plugins/form-builder/data/forms/contacts.json +0 -66
  134. package/plugins/form-builder/data/forms/feedback.json +0 -130
  135. package/plugins/form-builder/data/submissions/consent.json +0 -13
  136. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  137. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  138. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  139. package/plugins/form-builder/plugin.json +0 -52
  140. package/plugins/form-builder/public/inject-body.html +0 -352
  141. package/plugins/form-builder/public/inject-head.html +0 -58
  142. package/plugins/form-builder/public/package.json +0 -1
  143. package/scripts/copy-domma.js +0 -48
  144. package/server/services/userTypes.js +0 -167
  145. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -24,12 +24,27 @@
24
24
  * DELETE /collections/:slug/public/:id - Delete entry (if api.delete enabled)
25
25
  */
26
26
  import {
27
- listCollections, getCollection, createCollection, updateCollection, deleteCollection,
28
- listEntries, getEntry, createEntry, updateEntry, deleteEntry, clearEntries,
29
- exportEntries, importEntries
27
+ clearEntries,
28
+ createCollection,
29
+ createEntry,
30
+ deleteCollection,
31
+ deleteEntry,
32
+ exportEntries,
33
+ getCollection,
34
+ getEntry,
35
+ importEntries,
36
+ listCollections,
37
+ listEntries,
38
+ updateCollection,
39
+ updateEntry
30
40
  } from '../../services/collections.js';
31
- import { authenticate, requirePermission } from '../../middleware/auth.js';
32
- import { getRoleLevel, invalidate as invalidateUserTypes } from '../../services/userTypes.js';
41
+ import {authenticate, requireAdmin, requirePermission} from '../../middleware/auth.js';
42
+ import {getRoleLevel, invalidate as invalidateRoles} from '../../services/roles.js';
43
+ import {getConfig, saveConfig} from '../../config.js';
44
+ import {PRESET_COLLECTION_SLUGS} from '../../services/presetCollections.js';
45
+ import {ensureFormForCollection} from '../../services/forms.js';
46
+
47
+ const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION_SLUGS]);
33
48
 
34
49
  /**
35
50
  * Resolve the role level number for a named role.
@@ -76,34 +91,71 @@ async function checkPublicAccess(schema, operation, request, reply) {
76
91
  }
77
92
 
78
93
  export async function collectionsRoutes(fastify) {
79
- const guard = { preHandler: [authenticate, requirePermission('collections')] };
94
+ const canRead = {preHandler: [authenticate, requirePermission('collections', 'read')]};
95
+ const canCreate = {preHandler: [authenticate, requirePermission('collections', 'create')]};
96
+ const canUpdate = {preHandler: [authenticate, requirePermission('collections', 'update')]};
97
+ const canDelete = {preHandler: [authenticate, requirePermission('collections', 'delete')]};
80
98
 
81
99
  // -------------------------------------------------------------------------
82
100
  // Collection CRUD (schema management)
83
101
  // -------------------------------------------------------------------------
84
102
 
85
- fastify.get('/collections', guard, async () => {
103
+ fastify.get('/collections', canRead, async () => {
86
104
  return listCollections();
87
105
  });
88
106
 
89
- fastify.post('/collections', guard, async (request, reply) => {
90
- const { title, slug, description, fields, api } = request.body || {};
107
+ fastify.get('/collections/pro-status', canRead, async () => {
108
+ try {
109
+ const connections = getConfig('connections');
110
+ const names = Object.keys(connections).filter(k => k !== '_comment');
111
+ return {pro: names.length > 0, connections: names};
112
+ } catch {
113
+ return {pro: false, connections: []};
114
+ }
115
+ });
116
+
117
+ fastify.get('/collections/connections', {preHandler: [authenticate, requireAdmin]}, async () => {
118
+ try {
119
+ const {_comment, ...rest} = getConfig('connections');
120
+ return rest;
121
+ } catch {
122
+ return {};
123
+ }
124
+ });
125
+
126
+ fastify.put('/collections/connections', {preHandler: [authenticate, requireAdmin]}, async (request, reply) => {
127
+ const data = request.body;
128
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
129
+ return reply.status(400).send({error: 'Invalid connections data'});
130
+ }
131
+ for (const [name, conn] of Object.entries(data)) {
132
+ if (!conn.type || !conn.uri || !conn.database) {
133
+ return reply.status(400).send({error: `Connection "${name}" requires type, uri, and database`});
134
+ }
135
+ }
136
+ saveConfig('connections', data);
137
+ return {success: true};
138
+ });
139
+
140
+ fastify.post('/collections', canCreate, async (request, reply) => {
141
+ const {title, slug, description, fields, api, storage} = request.body || {};
91
142
  if (!title) return reply.status(400).send({ error: 'title is required' });
92
143
  try {
93
- const schema = await createCollection({ title, slug, description, fields, api });
144
+ const schema = await createCollection({title, slug, description, fields, api, storage});
145
+ await ensureFormForCollection(schema);
94
146
  return reply.status(201).send(schema);
95
147
  } catch (err) {
96
148
  return reply.status(409).send({ error: err.message });
97
149
  }
98
150
  });
99
151
 
100
- fastify.get('/collections/:slug', guard, async (request, reply) => {
152
+ fastify.get('/collections/:slug', canRead, async (request, reply) => {
101
153
  const schema = await getCollection(request.params.slug);
102
154
  if (!schema) return reply.status(404).send({ error: 'Collection not found' });
103
155
  return schema;
104
156
  });
105
157
 
106
- fastify.put('/collections/:slug', guard, async (request, reply) => {
158
+ fastify.put('/collections/:slug', canUpdate, async (request, reply) => {
107
159
  try {
108
160
  return await updateCollection(request.params.slug, request.body || {});
109
161
  } catch (err) {
@@ -111,8 +163,8 @@ export async function collectionsRoutes(fastify) {
111
163
  }
112
164
  });
113
165
 
114
- fastify.delete('/collections/:slug', guard, async (request, reply) => {
115
- if (request.params.slug === 'user-types') {
166
+ fastify.delete('/collections/:slug', canDelete, async (request, reply) => {
167
+ if (ALL_PRESET_SLUGS.has(request.params.slug)) {
116
168
  return reply.status(403).send({ error: 'Cannot delete a preset collection' });
117
169
  }
118
170
  try {
@@ -127,7 +179,7 @@ export async function collectionsRoutes(fastify) {
127
179
  // Entry CRUD
128
180
  // -------------------------------------------------------------------------
129
181
 
130
- fastify.get('/collections/:slug/entries', guard, async (request, reply) => {
182
+ fastify.get('/collections/:slug/entries', canRead, async (request, reply) => {
131
183
  const schema = await getCollection(request.params.slug);
132
184
  if (!schema) return reply.status(404).send({ error: 'Collection not found' });
133
185
  const { page, limit, sort, order, search } = request.query;
@@ -140,30 +192,30 @@ export async function collectionsRoutes(fastify) {
140
192
  });
141
193
  });
142
194
 
143
- fastify.get('/collections/:slug/entries/:id', guard, async (request, reply) => {
195
+ fastify.get('/collections/:slug/entries/:id', canRead, async (request, reply) => {
144
196
  const entry = await getEntry(request.params.slug, request.params.id);
145
197
  if (!entry) return reply.status(404).send({ error: 'Entry not found' });
146
198
  return entry;
147
199
  });
148
200
 
149
- fastify.post('/collections/:slug/entries', guard, async (request, reply) => {
201
+ fastify.post('/collections/:slug/entries', canCreate, async (request, reply) => {
150
202
  const user = request.user;
151
203
  try {
152
204
  const entry = await createEntry(request.params.slug, request.body?.data || {}, {
153
205
  createdBy: user?.id || null,
154
206
  source: 'admin'
155
207
  });
156
- if (request.params.slug === 'user-types') await invalidateUserTypes();
208
+ if (request.params.slug === 'roles') await invalidateRoles();
157
209
  return reply.status(201).send(entry);
158
210
  } catch (err) {
159
211
  return reply.status(400).send({ error: err.message });
160
212
  }
161
213
  });
162
214
 
163
- fastify.put('/collections/:slug/entries/:id', guard, async (request, reply) => {
215
+ fastify.put('/collections/:slug/entries/:id', canUpdate, async (request, reply) => {
164
216
  try {
165
217
  const entry = await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
166
- if (request.params.slug === 'user-types') await invalidateUserTypes();
218
+ if (request.params.slug === 'roles') await invalidateRoles();
167
219
  return entry;
168
220
  } catch (err) {
169
221
  const status = err.message === 'Entry not found' ? 404 : 400;
@@ -171,16 +223,16 @@ export async function collectionsRoutes(fastify) {
171
223
  }
172
224
  });
173
225
 
174
- fastify.delete('/collections/:slug/entries/:id', guard, async (request, reply) => {
175
- if (request.params.slug === 'user-types') {
176
- const entry = await getEntry('user-types', request.params.id);
226
+ fastify.delete('/collections/:slug/entries/:id', canDelete, async (request, reply) => {
227
+ if (request.params.slug === 'roles') {
228
+ const entry = await getEntry('roles', request.params.id);
177
229
  if (entry?.data?.level === 0) {
178
230
  return reply.status(403).send({ error: 'Cannot delete the root admin role' });
179
231
  }
180
232
  }
181
233
  try {
182
234
  await deleteEntry(request.params.slug, request.params.id);
183
- if (request.params.slug === 'user-types') await invalidateUserTypes();
235
+ if (request.params.slug === 'roles') await invalidateRoles();
184
236
  return { success: true };
185
237
  } catch (err) {
186
238
  return reply.status(404).send({ error: err.message });
@@ -188,7 +240,7 @@ export async function collectionsRoutes(fastify) {
188
240
  });
189
241
 
190
242
  // Clear all entries — DELETE /collections/:slug/entries (no :id)
191
- fastify.delete('/collections/:slug/entries', guard, async (request, reply) => {
243
+ fastify.delete('/collections/:slug/entries', canDelete, async (request, reply) => {
192
244
  try {
193
245
  await clearEntries(request.params.slug);
194
246
  return { success: true };
@@ -201,7 +253,7 @@ export async function collectionsRoutes(fastify) {
201
253
  // Export / Import
202
254
  // -------------------------------------------------------------------------
203
255
 
204
- fastify.get('/collections/:slug/export', guard, async (request, reply) => {
256
+ fastify.get('/collections/:slug/export', canRead, async (request, reply) => {
205
257
  const format = request.query.format === 'csv' ? 'csv' : 'json';
206
258
  try {
207
259
  const output = await exportEntries(request.params.slug, format);
@@ -218,7 +270,7 @@ export async function collectionsRoutes(fastify) {
218
270
  }
219
271
  });
220
272
 
221
- fastify.post('/collections/:slug/import', guard, async (request, reply) => {
273
+ fastify.post('/collections/:slug/import', canCreate, async (request, reply) => {
222
274
  const user = request.user;
223
275
  const entries = request.body?.entries;
224
276
  if (!Array.isArray(entries)) return reply.status(400).send({ error: 'entries must be an array' });