domma-cms 0.44.0 → 0.45.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/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +16 -16
- package/config/plugins.json +1 -1
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +14 -14
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +167 -167
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +199 -199
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +302 -302
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User Profiles Service
|
|
3
|
-
* Preset Collection
|
|
4
|
-
* keyed by user UUID. Seeded on startup; entries auto-managed on user lifecycle.
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'fs/promises';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import {config} from '../config.js';
|
|
9
|
-
|
|
10
|
-
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
11
|
-
const SLUG = 'user-profiles';
|
|
12
|
-
const DIR = path.join(COLLECTIONS_DIR, SLUG);
|
|
13
|
-
const SCHEMA_PATH = path.join(DIR, 'schema.json');
|
|
14
|
-
const DATA_PATH = path.join(DIR, 'data.json');
|
|
15
|
-
|
|
16
|
-
const USERS_DIR = path.resolve(config.content.usersDir);
|
|
17
|
-
|
|
18
|
-
const PRESET_SCHEMA = {
|
|
19
|
-
slug: SLUG,
|
|
20
|
-
title: 'User Profiles',
|
|
21
|
-
description: 'Extended profile data linked to users by ID.',
|
|
22
|
-
preset: true,
|
|
23
|
-
fields: [],
|
|
24
|
-
api: {
|
|
25
|
-
create: {enabled: false, access: 'admin'},
|
|
26
|
-
read: {enabled: false, access: 'admin'},
|
|
27
|
-
update: {enabled: false, access: 'admin'},
|
|
28
|
-
delete: {enabled: false, access: 'admin'}
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Read data.json, returning an empty array on any error.
|
|
38
|
-
*
|
|
39
|
-
* @returns {Promise<object[]>}
|
|
40
|
-
*/
|
|
41
|
-
async function readData() {
|
|
42
|
-
try {
|
|
43
|
-
const raw = await fs.readFile(DATA_PATH, 'utf8');
|
|
44
|
-
return JSON.parse(raw);
|
|
45
|
-
} catch {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Write entries array to data.json.
|
|
52
|
-
*
|
|
53
|
-
* @param {object[]} entries
|
|
54
|
-
* @returns {Promise<void>}
|
|
55
|
-
*/
|
|
56
|
-
async function writeData(entries) {
|
|
57
|
-
await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Public API
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Seed the preset collection on boot.
|
|
66
|
-
* Schema is always overwritten; data.json is only created if absent.
|
|
67
|
-
*
|
|
68
|
-
* @returns {Promise<void>}
|
|
69
|
-
*/
|
|
70
|
-
export async function seed() {
|
|
71
|
-
await fs.mkdir(DIR, {recursive: true});
|
|
72
|
-
|
|
73
|
-
// Only write schema if absent
|
|
74
|
-
try {
|
|
75
|
-
await fs.access(SCHEMA_PATH);
|
|
76
|
-
} catch {
|
|
77
|
-
await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Only create data file if absent
|
|
81
|
-
try {
|
|
82
|
-
await fs.access(DATA_PATH);
|
|
83
|
-
} catch {
|
|
84
|
-
await writeData([]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Ensure a profile entry exists for the given user ID.
|
|
90
|
-
* Creates an empty profile if one does not already exist.
|
|
91
|
-
*
|
|
92
|
-
* @param {string} userId
|
|
93
|
-
* @returns {Promise<void>}
|
|
94
|
-
*/
|
|
95
|
-
export async function ensureProfile(userId) {
|
|
96
|
-
const entries = await readData();
|
|
97
|
-
const exists = entries.some(e => e.id === userId);
|
|
98
|
-
if (!exists) {
|
|
99
|
-
entries.push({
|
|
100
|
-
id: userId,
|
|
101
|
-
data: {},
|
|
102
|
-
meta: {createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()}
|
|
103
|
-
});
|
|
104
|
-
await writeData(entries);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get the profile entry for a user, or null if not found.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} userId
|
|
112
|
-
* @returns {Promise<object|null>}
|
|
113
|
-
*/
|
|
114
|
-
export async function getProfile(userId) {
|
|
115
|
-
const entries = await readData();
|
|
116
|
-
return entries.find(e => e.id === userId) || null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Merge new data into a user's profile.
|
|
121
|
-
* Creates the profile entry if it does not exist.
|
|
122
|
-
*
|
|
123
|
-
* @param {string} userId
|
|
124
|
-
* @param {object} data - Flat key/value pairs to merge into profile data
|
|
125
|
-
* @returns {Promise<object>} Updated profile entry
|
|
126
|
-
*/
|
|
127
|
-
export async function updateProfile(userId, data) {
|
|
128
|
-
const entries = await readData();
|
|
129
|
-
const index = entries.findIndex(e => e.id === userId);
|
|
130
|
-
|
|
131
|
-
if (index === -1) {
|
|
132
|
-
const entry = {
|
|
133
|
-
id: userId,
|
|
134
|
-
data: {...data},
|
|
135
|
-
meta: {createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()}
|
|
136
|
-
};
|
|
137
|
-
entries.push(entry);
|
|
138
|
-
await writeData(entries);
|
|
139
|
-
return entry;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
entries[index] = {
|
|
143
|
-
...entries[index],
|
|
144
|
-
data: {...entries[index].data, ...data},
|
|
145
|
-
meta: {...entries[index].meta, updatedAt: new Date().toISOString()}
|
|
146
|
-
};
|
|
147
|
-
await writeData(entries);
|
|
148
|
-
return entries[index];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Remove a user's profile entry. Silent if not found.
|
|
153
|
-
*
|
|
154
|
-
* @param {string} userId
|
|
155
|
-
* @returns {Promise<void>}
|
|
156
|
-
*/
|
|
157
|
-
export async function deleteProfile(userId) {
|
|
158
|
-
const entries = await readData();
|
|
159
|
-
const filtered = entries.filter(e => e.id !== userId);
|
|
160
|
-
if (filtered.length !== entries.length) {
|
|
161
|
-
await writeData(filtered);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Return a Map of userId → profile data object.
|
|
167
|
-
* Reads data.json once
|
|
168
|
-
*
|
|
169
|
-
* @returns {Promise<Map<string, object>>}
|
|
170
|
-
*/
|
|
171
|
-
export async function listProfiles() {
|
|
172
|
-
const entries = await readData();
|
|
173
|
-
const map = new Map();
|
|
174
|
-
for (const entry of entries) {
|
|
175
|
-
map.set(entry.id, entry.data || {});
|
|
176
|
-
}
|
|
177
|
-
return map;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Migration helper
|
|
182
|
-
* Called once on startup after seed().
|
|
183
|
-
*
|
|
184
|
-
* @returns {Promise<void>}
|
|
185
|
-
*/
|
|
186
|
-
export async function ensureAllProfiles() {
|
|
187
|
-
let userFiles;
|
|
188
|
-
try {
|
|
189
|
-
userFiles = await fs.readdir(USERS_DIR);
|
|
190
|
-
} catch {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const jsonFiles = userFiles.filter(f => f.endsWith('.json'));
|
|
195
|
-
for (const file of jsonFiles) {
|
|
196
|
-
const userId = file.replace(/\.json$/, '');
|
|
197
|
-
await ensureProfile(userId);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* User Profiles Service
|
|
3
|
+
* Preset Collection - stores extended profile data for each user,
|
|
4
|
+
* keyed by user UUID. Seeded on startup; entries auto-managed on user lifecycle.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {config} from '../config.js';
|
|
9
|
+
|
|
10
|
+
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
11
|
+
const SLUG = 'user-profiles';
|
|
12
|
+
const DIR = path.join(COLLECTIONS_DIR, SLUG);
|
|
13
|
+
const SCHEMA_PATH = path.join(DIR, 'schema.json');
|
|
14
|
+
const DATA_PATH = path.join(DIR, 'data.json');
|
|
15
|
+
|
|
16
|
+
const USERS_DIR = path.resolve(config.content.usersDir);
|
|
17
|
+
|
|
18
|
+
const PRESET_SCHEMA = {
|
|
19
|
+
slug: SLUG,
|
|
20
|
+
title: 'User Profiles',
|
|
21
|
+
description: 'Extended profile data linked to users by ID.',
|
|
22
|
+
preset: true,
|
|
23
|
+
fields: [],
|
|
24
|
+
api: {
|
|
25
|
+
create: {enabled: false, access: 'admin'},
|
|
26
|
+
read: {enabled: false, access: 'admin'},
|
|
27
|
+
update: {enabled: false, access: 'admin'},
|
|
28
|
+
delete: {enabled: false, access: 'admin'}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read data.json, returning an empty array on any error.
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promise<object[]>}
|
|
40
|
+
*/
|
|
41
|
+
async function readData() {
|
|
42
|
+
try {
|
|
43
|
+
const raw = await fs.readFile(DATA_PATH, 'utf8');
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write entries array to data.json.
|
|
52
|
+
*
|
|
53
|
+
* @param {object[]} entries
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
*/
|
|
56
|
+
async function writeData(entries) {
|
|
57
|
+
await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Public API
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Seed the preset collection on boot.
|
|
66
|
+
* Schema is always overwritten; data.json is only created if absent.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
export async function seed() {
|
|
71
|
+
await fs.mkdir(DIR, {recursive: true});
|
|
72
|
+
|
|
73
|
+
// Only write schema if absent - admin edits to fields must be preserved across restarts
|
|
74
|
+
try {
|
|
75
|
+
await fs.access(SCHEMA_PATH);
|
|
76
|
+
} catch {
|
|
77
|
+
await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Only create data file if absent
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(DATA_PATH);
|
|
83
|
+
} catch {
|
|
84
|
+
await writeData([]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Ensure a profile entry exists for the given user ID.
|
|
90
|
+
* Creates an empty profile if one does not already exist.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} userId
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
export async function ensureProfile(userId) {
|
|
96
|
+
const entries = await readData();
|
|
97
|
+
const exists = entries.some(e => e.id === userId);
|
|
98
|
+
if (!exists) {
|
|
99
|
+
entries.push({
|
|
100
|
+
id: userId,
|
|
101
|
+
data: {},
|
|
102
|
+
meta: {createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()}
|
|
103
|
+
});
|
|
104
|
+
await writeData(entries);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the profile entry for a user, or null if not found.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} userId
|
|
112
|
+
* @returns {Promise<object|null>}
|
|
113
|
+
*/
|
|
114
|
+
export async function getProfile(userId) {
|
|
115
|
+
const entries = await readData();
|
|
116
|
+
return entries.find(e => e.id === userId) || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge new data into a user's profile.
|
|
121
|
+
* Creates the profile entry if it does not exist.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} userId
|
|
124
|
+
* @param {object} data - Flat key/value pairs to merge into profile data
|
|
125
|
+
* @returns {Promise<object>} Updated profile entry
|
|
126
|
+
*/
|
|
127
|
+
export async function updateProfile(userId, data) {
|
|
128
|
+
const entries = await readData();
|
|
129
|
+
const index = entries.findIndex(e => e.id === userId);
|
|
130
|
+
|
|
131
|
+
if (index === -1) {
|
|
132
|
+
const entry = {
|
|
133
|
+
id: userId,
|
|
134
|
+
data: {...data},
|
|
135
|
+
meta: {createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()}
|
|
136
|
+
};
|
|
137
|
+
entries.push(entry);
|
|
138
|
+
await writeData(entries);
|
|
139
|
+
return entry;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
entries[index] = {
|
|
143
|
+
...entries[index],
|
|
144
|
+
data: {...entries[index].data, ...data},
|
|
145
|
+
meta: {...entries[index].meta, updatedAt: new Date().toISOString()}
|
|
146
|
+
};
|
|
147
|
+
await writeData(entries);
|
|
148
|
+
return entries[index];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Remove a user's profile entry. Silent if not found.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} userId
|
|
155
|
+
* @returns {Promise<void>}
|
|
156
|
+
*/
|
|
157
|
+
export async function deleteProfile(userId) {
|
|
158
|
+
const entries = await readData();
|
|
159
|
+
const filtered = entries.filter(e => e.id !== userId);
|
|
160
|
+
if (filtered.length !== entries.length) {
|
|
161
|
+
await writeData(filtered);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Return a Map of userId → profile data object.
|
|
167
|
+
* Reads data.json once - use this for list endpoints to avoid N+1 reads.
|
|
168
|
+
*
|
|
169
|
+
* @returns {Promise<Map<string, object>>}
|
|
170
|
+
*/
|
|
171
|
+
export async function listProfiles() {
|
|
172
|
+
const entries = await readData();
|
|
173
|
+
const map = new Map();
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
map.set(entry.id, entry.data || {});
|
|
176
|
+
}
|
|
177
|
+
return map;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Migration helper - ensure every existing user has a profile entry.
|
|
182
|
+
* Called once on startup after seed().
|
|
183
|
+
*
|
|
184
|
+
* @returns {Promise<void>}
|
|
185
|
+
*/
|
|
186
|
+
export async function ensureAllProfiles() {
|
|
187
|
+
let userFiles;
|
|
188
|
+
try {
|
|
189
|
+
userFiles = await fs.readdir(USERS_DIR);
|
|
190
|
+
} catch {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const jsonFiles = userFiles.filter(f => f.endsWith('.json'));
|
|
195
|
+
for (const file of jsonFiles) {
|
|
196
|
+
const userId = file.replace(/\.json$/, '');
|
|
197
|
+
await ensureProfile(userId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* User Roles
|
|
2
|
+
* User Roles - multi-role support helpers.
|
|
3
3
|
*
|
|
4
4
|
* A user has ONE primary role (`user.role`) and an optional array of
|
|
5
5
|
* additional roles (`user.additionalRoles`). Permission, visibility, and
|
|
6
6
|
* row-access checks consult ALL of them and grant access if any one role
|
|
7
|
-
* satisfies
|
|
7
|
+
* satisfies - "any path wins" semantics that matches how most apps actually
|
|
8
8
|
* model multi-membership (you can be both a candidate AND a recruiter).
|
|
9
9
|
*
|
|
10
10
|
* Hierarchical-level comparisons (`canManageUser`, `requireAdmin`, the
|
|
11
11
|
* level-clamp in `checkSingleVisibility`) use `getEffectiveLevel` which
|
|
12
|
-
* returns the LOWEST level number across all the user's roles
|
|
12
|
+
* returns the LOWEST level number across all the user's roles - i.e. the
|
|
13
13
|
* highest privilege the user has access to. This is what makes "additional
|
|
14
14
|
* admin" meaningfully grant admin-tier access without rewriting the level
|
|
15
15
|
* arithmetic everywhere.
|
|
16
16
|
*
|
|
17
17
|
* Backward compatibility: users with no `additionalRoles` field behave
|
|
18
|
-
* exactly as before
|
|
18
|
+
* exactly as before - `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
|
|
19
19
|
* returns `getRoleLevel(user.role)`. Existing callers that read `user.role`
|
|
20
20
|
* directly continue to work; the new helpers are opt-in for callers that
|
|
21
21
|
* want the union semantics.
|
|
@@ -40,7 +40,7 @@ export function getEffectiveRoles(user) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Return the effective role level for a user
|
|
43
|
+
* Return the effective role level for a user - the LOWEST level number
|
|
44
44
|
* across all roles. Lower number = higher privilege, so the user's most
|
|
45
45
|
* privileged role wins all hierarchical checks.
|
|
46
46
|
*
|
|
@@ -57,7 +57,7 @@ export function getEffectiveLevel(user) {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Check whether a user holds a specific role (primary or additional).
|
|
60
|
-
* Exact-name match
|
|
60
|
+
* Exact-name match - does NOT walk the hierarchy. For hierarchical "user
|
|
61
61
|
* is at least this level" checks use `getEffectiveLevel` against the target
|
|
62
62
|
* role's level instead.
|
|
63
63
|
*
|
|
@@ -72,7 +72,7 @@ export function userHasRole(user, role) {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Check whether a user holds ANY of the given roles. Convenience helper
|
|
75
|
-
* for "anyone in this set can do X" patterns
|
|
75
|
+
* for "anyone in this set can do X" patterns - equivalent to OR-ing
|
|
76
76
|
* `userHasRole` over the list.
|
|
77
77
|
*
|
|
78
78
|
* @param {object|null} user
|