domma-cms 0.3.0 → 0.5.2

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 (150) 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 +167 -23
  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/navigation.json +5 -15
  69. package/config/plugins.json +19 -29
  70. package/config/server.json +6 -6
  71. package/config/site.json +16 -6
  72. package/package.json +25 -10
  73. package/plugins/example-analytics/stats.json +17 -12
  74. package/plugins/form-builder/data/forms/contacts.json +62 -62
  75. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  76. package/plugins/form-builder/data/forms/feedback.json +17 -16
  77. package/plugins/form-builder/data/forms/notes.json +79 -0
  78. package/plugins/form-builder/data/forms/to-do.json +100 -0
  79. package/plugins/form-builder/data/submissions/contacts.json +1 -26
  80. package/plugins/form-builder/data/submissions/notes.json +1 -0
  81. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  82. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  83. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  84. package/plugins/theme-roller/config.js +1 -0
  85. package/plugins/theme-roller/plugin.js +233 -0
  86. package/plugins/theme-roller/plugin.json +31 -0
  87. package/plugins/theme-roller/public/active-theme.css +0 -0
  88. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  89. package/public/css/forms.css +1 -0
  90. package/public/css/site.css +1 -1
  91. package/public/js/forms.js +1 -0
  92. package/public/js/site.js +1 -1
  93. package/scripts/build.js +194 -129
  94. package/scripts/pro.js +254 -0
  95. package/scripts/reset.js +33 -8
  96. package/scripts/seed.js +677 -128
  97. package/scripts/setup.js +1 -0
  98. package/server/middleware/auth.js +136 -120
  99. package/server/routes/api/actions.js +200 -0
  100. package/server/routes/api/auth.js +292 -146
  101. package/server/routes/api/blocks.js +84 -0
  102. package/server/routes/api/collections.js +79 -27
  103. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
  104. package/server/routes/api/layouts.js +49 -39
  105. package/server/routes/api/media.js +118 -92
  106. package/server/routes/api/navigation.js +40 -36
  107. package/server/routes/api/pages.js +132 -118
  108. package/server/routes/api/plugins.js +6 -3
  109. package/server/routes/api/settings.js +104 -88
  110. package/server/routes/api/users.js +27 -19
  111. package/server/routes/api/views.js +148 -0
  112. package/server/routes/public.js +124 -108
  113. package/server/server.js +269 -181
  114. package/server/services/actions.js +387 -0
  115. package/server/services/adapterRegistry.js +98 -0
  116. package/server/services/adapters/FileAdapter.js +192 -0
  117. package/server/services/adapters/MongoAdapter.js +220 -0
  118. package/server/services/blocks.js +162 -0
  119. package/server/services/collections.js +74 -86
  120. package/server/services/connectionManager.js +102 -0
  121. package/server/services/content.js +312 -307
  122. package/server/services/email.js +126 -0
  123. package/server/services/forms.js +173 -0
  124. package/server/services/markdown.js +1378 -747
  125. package/server/services/permissionRegistry.js +173 -0
  126. package/server/services/presetCollections.js +251 -0
  127. package/server/services/renderer.js +98 -2
  128. package/server/services/roles.js +227 -0
  129. package/server/services/rowAccess.js +104 -0
  130. package/server/services/userProfiles.js +199 -0
  131. package/server/services/users.js +281 -212
  132. package/server/services/views.js +280 -0
  133. package/server/templates/page.html +124 -113
  134. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  135. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  136. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  137. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  138. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  139. package/plugins/form-builder/config.js +0 -9
  140. package/plugins/form-builder/data/forms/consent.json +0 -104
  141. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  142. package/plugins/form-builder/data/submissions/consent.json +0 -13
  143. package/plugins/form-builder/plugin.json +0 -52
  144. package/plugins/form-builder/public/inject-body.html +0 -352
  145. package/plugins/form-builder/public/inject-head.html +0 -58
  146. package/plugins/form-builder/public/package.json +0 -1
  147. package/scripts/copy-domma.js +0 -48
  148. package/server/services/userTypes.js +0 -167
  149. /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
  150. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -1,212 +1,281 @@
1
- /**
2
- * User Storage Service
3
- * File-based CRUD — one JSON file per user in content/users/{id}.json.
4
- * Mirrors the page-per-file pattern from the content service.
5
- */
6
- import fs from 'fs/promises';
7
- import path from 'path';
8
- import bcrypt from 'bcryptjs';
9
- import { v4 as uuidv4 } from 'uuid';
10
- import { config } from '../config.js';
11
-
12
- const USERS_DIR = path.resolve(config.content.usersDir);
13
-
14
- async function ensureDir() {
15
- await fs.mkdir(USERS_DIR, { recursive: true });
16
- }
17
-
18
- /**
19
- * Strip the password field from a user object before returning it to callers.
20
- *
21
- * @param {object} user
22
- * @returns {object}
23
- */
24
- function stripPassword(user) {
25
- const { password, ...safe } = user;
26
- return safe;
27
- }
28
-
29
- /**
30
- * Read a user file by ID.
31
- *
32
- * @param {string} id
33
- * @returns {Promise<object>} Full user object including password hash
34
- */
35
- async function readUserFile(id) {
36
- const filePath = path.join(USERS_DIR, `${id}.json`);
37
- const raw = await fs.readFile(filePath, 'utf8');
38
- return JSON.parse(raw);
39
- }
40
-
41
- /**
42
- * Write a user file.
43
- *
44
- * @param {object} user
45
- * @returns {Promise<void>}
46
- */
47
- async function writeUserFile(user) {
48
- await ensureDir();
49
- const filePath = path.join(USERS_DIR, `${user.id}.json`);
50
- await fs.writeFile(filePath, JSON.stringify(user, null, 2) + '\n', 'utf8');
51
- }
52
-
53
- /**
54
- * List all users (password stripped).
55
- *
56
- * @returns {Promise<object[]>}
57
- */
58
- export async function listUsers() {
59
- await ensureDir();
60
- let files;
61
- try {
62
- files = await fs.readdir(USERS_DIR);
63
- } catch {
64
- return [];
65
- }
66
- const jsonFiles = files.filter(f => f.endsWith('.json'));
67
- const users = await Promise.all(jsonFiles.map(async (file) => {
68
- const raw = await fs.readFile(path.join(USERS_DIR, file), 'utf8');
69
- return stripPassword(JSON.parse(raw));
70
- }));
71
- return users.sort((a, b) => a.name.localeCompare(b.name));
72
- }
73
-
74
- /**
75
- * Get a single user by ID (password stripped).
76
- *
77
- * @param {string} id
78
- * @returns {Promise<object|null>}
79
- */
80
- export async function getUserById(id) {
81
- try {
82
- return stripPassword(await readUserFile(id));
83
- } catch {
84
- return null;
85
- }
86
- }
87
-
88
- /**
89
- * Get a user by email address — includes password hash (needed for login).
90
- *
91
- * @param {string} email
92
- * @returns {Promise<object|null>}
93
- */
94
- export async function getUserByEmail(email) {
95
- await ensureDir();
96
- let files;
97
- try {
98
- files = await fs.readdir(USERS_DIR);
99
- } catch {
100
- return null;
101
- }
102
- for (const file of files.filter(f => f.endsWith('.json'))) {
103
- const raw = await fs.readFile(path.join(USERS_DIR, file), 'utf8');
104
- const user = JSON.parse(raw);
105
- if (user.email.toLowerCase() === email.toLowerCase()) return user;
106
- }
107
- return null;
108
- }
109
-
110
- /**
111
- * Create a new user.
112
- *
113
- * @param {object} data - { name, email, password, role }
114
- * @returns {Promise<object>} Created user (password stripped)
115
- */
116
- export async function createUser({ name, email, password, role = 'editor' }) {
117
- const existing = await getUserByEmail(email);
118
- if (existing) throw new Error('A user with that email already exists');
119
-
120
- const hash = await bcrypt.hash(password, config.auth.bcryptRounds);
121
- const now = new Date().toISOString();
122
- const user = {
123
- id: uuidv4(),
124
- email: email.toLowerCase().trim(),
125
- name: name.trim(),
126
- password: hash,
127
- role,
128
- isActive: true,
129
- createdAt: now,
130
- updatedAt: now,
131
- lastLogin: null
132
- };
133
-
134
- await writeUserFile(user);
135
- return stripPassword(user);
136
- }
137
-
138
- /**
139
- * Update an existing user.
140
- * Pass a new `password` field to re-hash; omit to keep the existing hash.
141
- *
142
- * @param {string} id
143
- * @param {object} updates
144
- * @returns {Promise<object>} Updated user (password stripped)
145
- */
146
- export async function updateUser(id, updates) {
147
- const user = await readUserFile(id);
148
- if (!user) throw new Error('User not found');
149
-
150
- if (updates.password) {
151
- updates.password = await bcrypt.hash(updates.password, config.auth.bcryptRounds);
152
- }
153
-
154
- const updated = {
155
- ...user,
156
- ...updates,
157
- id: user.id,
158
- updatedAt: new Date().toISOString()
159
- };
160
-
161
- await writeUserFile(updated);
162
- return stripPassword(updated);
163
- }
164
-
165
- /**
166
- * Delete a user file.
167
- *
168
- * @param {string} id
169
- * @returns {Promise<void>}
170
- */
171
- export async function deleteUser(id) {
172
- const filePath = path.join(USERS_DIR, `${id}.json`);
173
- await fs.unlink(filePath);
174
- }
175
-
176
- /**
177
- * Count total users.
178
- *
179
- * @returns {Promise<number>}
180
- */
181
- export async function countUsers() {
182
- await ensureDir();
183
- try {
184
- const files = await fs.readdir(USERS_DIR);
185
- return files.filter(f => f.endsWith('.json')).length;
186
- } catch {
187
- return 0;
188
- }
189
- }
190
-
191
- /**
192
- * Validate a plain-text password against a bcrypt hash.
193
- *
194
- * @param {string} plain
195
- * @param {string} hash
196
- * @returns {Promise<boolean>}
197
- */
198
- export async function validatePassword(plain, hash) {
199
- return bcrypt.compare(plain, hash);
200
- }
201
-
202
- /**
203
- * Record the current timestamp as the user's last login.
204
- *
205
- * @param {string} id
206
- * @returns {Promise<void>}
207
- */
208
- export async function touchLastLogin(id) {
209
- const user = await readUserFile(id);
210
- user.lastLogin = new Date().toISOString();
211
- await writeUserFile(user);
212
- }
1
+ /**
2
+ * User Storage Service
3
+ * File-based CRUD — one JSON file per user in content/users/{id}.json.
4
+ * Mirrors the page-per-file pattern from the content service.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import bcrypt from 'bcryptjs';
9
+ import {v4 as uuidv4} from 'uuid';
10
+ import {config} from '../config.js';
11
+ import {deleteProfile, ensureProfile} from './userProfiles.js';
12
+
13
+ const USERS_DIR = path.resolve(config.content.usersDir);
14
+
15
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16
+
17
+ async function ensureDir() {
18
+ await fs.mkdir(USERS_DIR, { recursive: true });
19
+ }
20
+
21
+ /**
22
+ * Strip the password field from a user object before returning it to callers.
23
+ *
24
+ * @param {object} user
25
+ * @returns {object}
26
+ */
27
+ function stripPassword(user) {
28
+ const {password, resetTokenHash, resetTokenExpiry, ...safe} = user;
29
+ return safe;
30
+ }
31
+
32
+ /**
33
+ * Read a user file by ID.
34
+ *
35
+ * @param {string} id
36
+ * @returns {Promise<object>} Full user object including password hash
37
+ */
38
+ async function readUserFile(id) {
39
+ if (!UUID_REGEX.test(id)) {
40
+ throw new Error('Invalid user ID');
41
+ }
42
+ const filePath = path.join(USERS_DIR, `${id}.json`);
43
+ const raw = await fs.readFile(filePath, 'utf8');
44
+ return JSON.parse(raw);
45
+ }
46
+
47
+ /**
48
+ * Write a user file.
49
+ *
50
+ * @param {object} user
51
+ * @returns {Promise<void>}
52
+ */
53
+ async function writeUserFile(user) {
54
+ if (!UUID_REGEX.test(user.id)) {
55
+ throw new Error('Invalid user ID');
56
+ }
57
+ await ensureDir();
58
+ const filePath = path.join(USERS_DIR, `${user.id}.json`);
59
+ await fs.writeFile(filePath, JSON.stringify(user, null, 2) + '\n', 'utf8');
60
+ }
61
+
62
+ /**
63
+ * List all users (password stripped).
64
+ *
65
+ * @returns {Promise<object[]>}
66
+ */
67
+ export async function listUsers() {
68
+ await ensureDir();
69
+ let files;
70
+ try {
71
+ files = await fs.readdir(USERS_DIR);
72
+ } catch {
73
+ return [];
74
+ }
75
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
76
+ const users = await Promise.all(jsonFiles.map(async (file) => {
77
+ const raw = await fs.readFile(path.join(USERS_DIR, file), 'utf8');
78
+ return stripPassword(JSON.parse(raw));
79
+ }));
80
+ return users.sort((a, b) => a.name.localeCompare(b.name));
81
+ }
82
+
83
+ /**
84
+ * Get a single user by ID (password stripped).
85
+ *
86
+ * @param {string} id
87
+ * @returns {Promise<object|null>}
88
+ */
89
+ export async function getUserById(id) {
90
+ try {
91
+ return stripPassword(await readUserFile(id));
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get a user by email address — includes password hash (needed for login).
99
+ *
100
+ * @param {string} email
101
+ * @returns {Promise<object|null>}
102
+ */
103
+ export async function getUserByEmail(email) {
104
+ await ensureDir();
105
+ let files;
106
+ try {
107
+ files = await fs.readdir(USERS_DIR);
108
+ } catch {
109
+ return null;
110
+ }
111
+ for (const file of files.filter(f => f.endsWith('.json'))) {
112
+ const raw = await fs.readFile(path.join(USERS_DIR, file), 'utf8');
113
+ const user = JSON.parse(raw);
114
+ if (user.email.toLowerCase() === email.toLowerCase()) return user;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Create a new user.
121
+ *
122
+ * @param {object} data - { name, email, password, role }
123
+ * @returns {Promise<object>} Created user (password stripped)
124
+ */
125
+ export async function createUser({ name, email, password, role = 'editor' }) {
126
+ const existing = await getUserByEmail(email);
127
+ if (existing) throw new Error('A user with that email already exists');
128
+
129
+ const hash = await bcrypt.hash(password, config.auth.bcryptRounds);
130
+ const now = new Date().toISOString();
131
+ const user = {
132
+ id: uuidv4(),
133
+ email: email.toLowerCase().trim(),
134
+ name: name.trim(),
135
+ password: hash,
136
+ role,
137
+ isActive: true,
138
+ createdAt: now,
139
+ updatedAt: now,
140
+ lastLogin: null
141
+ };
142
+
143
+ await writeUserFile(user);
144
+ await ensureProfile(user.id);
145
+ return stripPassword(user);
146
+ }
147
+
148
+ /**
149
+ * Update an existing user.
150
+ * Pass a new `password` field to re-hash; omit to keep the existing hash.
151
+ *
152
+ * @param {string} id
153
+ * @param {object} updates
154
+ * @returns {Promise<object>} Updated user (password stripped)
155
+ */
156
+ export async function updateUser(id, updates) {
157
+ const user = await readUserFile(id);
158
+ if (!user) throw new Error('User not found');
159
+
160
+ if (updates.password) {
161
+ updates.password = await bcrypt.hash(updates.password, config.auth.bcryptRounds);
162
+ }
163
+
164
+ const updated = {
165
+ ...user,
166
+ ...updates,
167
+ id: user.id,
168
+ updatedAt: new Date().toISOString()
169
+ };
170
+
171
+ await writeUserFile(updated);
172
+ return stripPassword(updated);
173
+ }
174
+
175
+ /**
176
+ * Delete a user file.
177
+ *
178
+ * @param {string} id
179
+ * @returns {Promise<void>}
180
+ */
181
+ export async function deleteUser(id) {
182
+ if (!UUID_REGEX.test(id)) {
183
+ throw new Error('Invalid user ID');
184
+ }
185
+ const filePath = path.join(USERS_DIR, `${id}.json`);
186
+ await fs.unlink(filePath);
187
+ try {
188
+ await deleteProfile(id);
189
+ } catch {
190
+ // Profile deletion is best-effort — never block user deletion
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Count total users.
196
+ *
197
+ * @returns {Promise<number>}
198
+ */
199
+ export async function countUsers() {
200
+ await ensureDir();
201
+ try {
202
+ const files = await fs.readdir(USERS_DIR);
203
+ return files.filter(f => f.endsWith('.json')).length;
204
+ } catch {
205
+ return 0;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Validate a plain-text password against a bcrypt hash.
211
+ *
212
+ * @param {string} plain
213
+ * @param {string} hash
214
+ * @returns {Promise<boolean>}
215
+ */
216
+ export async function validatePassword(plain, hash) {
217
+ return bcrypt.compare(plain, hash);
218
+ }
219
+
220
+ /**
221
+ * Store a hashed reset token and its expiry on a user file.
222
+ *
223
+ * @param {string} id
224
+ * @param {string} tokenHash - SHA-256 hex digest of the plaintext token
225
+ * @param {string} expiry - ISO timestamp
226
+ * @returns {Promise<void>}
227
+ */
228
+ export async function setResetToken(id, tokenHash, expiry) {
229
+ const user = await readUserFile(id);
230
+ user.resetTokenHash = tokenHash;
231
+ user.resetTokenExpiry = expiry;
232
+ await writeUserFile(user);
233
+ }
234
+
235
+ /**
236
+ * Remove the reset token fields from a user file (single-use enforcement).
237
+ *
238
+ * @param {string} id
239
+ * @returns {Promise<void>}
240
+ */
241
+ export async function clearResetToken(id) {
242
+ const user = await readUserFile(id);
243
+ delete user.resetTokenHash;
244
+ delete user.resetTokenExpiry;
245
+ await writeUserFile(user);
246
+ }
247
+
248
+ /**
249
+ * Find a user whose stored reset token hash matches the given hash.
250
+ * Returns the full user object (including hash) so the caller can check expiry.
251
+ *
252
+ * @param {string} tokenHash - SHA-256 hex digest to look up
253
+ * @returns {Promise<object|null>}
254
+ */
255
+ export async function getUserByResetToken(tokenHash) {
256
+ await ensureDir();
257
+ let files;
258
+ try {
259
+ files = await fs.readdir(USERS_DIR);
260
+ } catch {
261
+ return null;
262
+ }
263
+ for (const file of files.filter(f => f.endsWith('.json'))) {
264
+ const raw = await fs.readFile(path.join(USERS_DIR, file), 'utf8');
265
+ const user = JSON.parse(raw);
266
+ if (user.resetTokenHash === tokenHash) return user;
267
+ }
268
+ return null;
269
+ }
270
+
271
+ /**
272
+ * Record the current timestamp as the user's last login.
273
+ *
274
+ * @param {string} id
275
+ * @returns {Promise<void>}
276
+ */
277
+ export async function touchLastLogin(id) {
278
+ const user = await readUserFile(id);
279
+ user.lastLogin = new Date().toISOString();
280
+ await writeUserFile(user);
281
+ }