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.
- package/README.md +3 -3
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +2313 -0
- package/admin/dist/domma/domma-tools.min.js +10 -0
- package/admin/index.html +4 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +8 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +18 -10
- package/admin/js/templates/action-editor.html +171 -0
- package/admin/js/templates/actions-list.html +19 -0
- package/admin/js/templates/api-reference.html +1411 -0
- package/admin/js/templates/block-editor.html +158 -0
- package/admin/js/templates/blocks.html +8 -0
- package/admin/js/templates/collection-editor.html +47 -0
- package/admin/js/templates/collection-entries.html +3 -0
- package/admin/js/templates/collections.html +51 -4
- package/admin/js/templates/documentation.html +258 -0
- package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
- package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
- package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
- package/admin/js/templates/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +39 -0
- package/admin/js/templates/pages.html +6 -1
- package/admin/js/templates/pro-docs.html +259 -0
- package/admin/js/templates/role-editor.html +59 -0
- package/admin/js/templates/roles.html +10 -0
- package/admin/js/templates/settings.html +167 -23
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -26
- package/admin/js/templates/view-editor.html +201 -0
- package/admin/js/templates/view-preview.html +51 -0
- package/admin/js/templates/views-list.html +19 -0
- package/admin/js/views/action-editor.js +1 -0
- package/admin/js/views/actions-list.js +1 -0
- package/admin/js/views/api-reference.js +1 -0
- package/admin/js/views/block-editor.js +8 -0
- package/admin/js/views/blocks.js +4 -0
- package/admin/js/views/collection-editor.js +3 -3
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +8 -0
- package/admin/js/views/form-submissions.js +1 -0
- package/admin/js/views/forms.js +1 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/media.js +1 -1
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/page-editor.js +34 -15
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +10 -10
- package/admin/js/views/pro-docs.js +1 -0
- package/admin/js/views/role-editor.js +1 -0
- package/admin/js/views/roles.js +4 -0
- package/admin/js/views/settings.js +3 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +4 -7
- package/admin/js/views/view-editor.js +1 -0
- package/admin/js/views/view-preview.js +1 -0
- package/admin/js/views/views-list.js +1 -0
- package/bin/cli.js +1 -1
- package/config/auth.json +1 -0
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/navigation.json +5 -15
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +16 -6
- package/package.json +25 -10
- package/plugins/example-analytics/stats.json +17 -12
- package/plugins/form-builder/data/forms/contacts.json +62 -62
- package/plugins/form-builder/data/forms/enquiries.json +103 -0
- package/plugins/form-builder/data/forms/feedback.json +17 -16
- package/plugins/form-builder/data/forms/notes.json +79 -0
- package/plugins/form-builder/data/forms/to-do.json +100 -0
- package/plugins/form-builder/data/submissions/contacts.json +1 -26
- package/plugins/form-builder/data/submissions/notes.json +1 -0
- package/plugins/form-builder/data/submissions/to-do.json +1 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/plugin.js +233 -0
- package/plugins/theme-roller/plugin.json +31 -0
- package/plugins/theme-roller/public/active-theme.css +0 -0
- package/plugins/theme-roller/public/inject-head-late.html +1 -0
- package/public/css/forms.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +194 -129
- package/scripts/pro.js +254 -0
- package/scripts/reset.js +33 -8
- package/scripts/seed.js +677 -128
- package/scripts/setup.js +1 -0
- package/server/middleware/auth.js +136 -120
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -146
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +79 -27
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
- package/server/routes/api/layouts.js +49 -39
- package/server/routes/api/media.js +118 -92
- package/server/routes/api/navigation.js +40 -36
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -88
- package/server/routes/api/users.js +27 -19
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -181
- package/server/services/actions.js +387 -0
- package/server/services/adapterRegistry.js +98 -0
- package/server/services/adapters/FileAdapter.js +192 -0
- package/server/services/adapters/MongoAdapter.js +220 -0
- package/server/services/blocks.js +162 -0
- package/server/services/collections.js +74 -86
- package/server/services/connectionManager.js +102 -0
- package/server/services/content.js +312 -307
- package/server/services/email.js +126 -0
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -747
- package/server/services/permissionRegistry.js +173 -0
- package/server/services/presetCollections.js +251 -0
- package/server/services/renderer.js +98 -2
- package/server/services/roles.js +227 -0
- package/server/services/rowAccess.js +104 -0
- package/server/services/userProfiles.js +199 -0
- package/server/services/users.js +281 -212
- package/server/services/views.js +280 -0
- package/server/templates/page.html +124 -113
- package/plugins/form-builder/admin/templates/form-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1444
- package/plugins/form-builder/admin/views/form-settings.js +0 -38
- package/plugins/form-builder/admin/views/form-submissions.js +0 -295
- package/plugins/form-builder/admin/views/forms-list.js +0 -164
- package/plugins/form-builder/config.js +0 -9
- package/plugins/form-builder/data/forms/consent.json +0 -104
- package/plugins/form-builder/data/forms/contact-details.json +0 -99
- package/plugins/form-builder/data/submissions/consent.json +0 -13
- package/plugins/form-builder/plugin.json +0 -52
- package/plugins/form-builder/public/inject-body.html +0 -352
- package/plugins/form-builder/public/inject-head.html +0 -58
- package/plugins/form-builder/public/package.json +0 -1
- package/scripts/copy-domma.js +0 -48
- package/server/services/userTypes.js +0 -167
- /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
- /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
package/server/services/users.js
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* @returns {Promise<
|
|
180
|
-
*/
|
|
181
|
-
export async function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|