Blame Raw
· · · 265 lines (9.4 KB)
0 contributors
1import { promises as fsPromises } from 'node:fs';
2
3import storage from 'node-persist';
4import express from 'express';
5import lodash from 'lodash';
6import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
7import {
8 KEY_PREFIX,
9 toKey,
10 requireAdminMiddleware,
11 getUserAvatar,
12 getAllUserHandles,
13 getPasswordSalt,
14 getPasswordHash,
15 getUserDirectories,
16 ensurePublicDirectoriesExist,
17} from '../users.js';
18import { DEFAULT_USER } from '../constants.js';
19
20export const router = express.Router();
21
22/**
23 * Slugifies a given text string.
24 * - Converts to lowercase
25 * - Trims whitespace
26 * - Replaces spaces and special characters with hyphens
27 * - Removes leading and trailing hyphens
28 * - Uses lodash.deburr to remove diacritical marks
29 * @param {string} text Text to slugify
30 * @returns {string} Slugified text
31 */
32function slugify(text) {
33 return lodash.deburr(String(text ?? '').toLowerCase().trim()).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
34}
35
36router.post('/get', requireAdminMiddleware, async (_request, response) => {
37 try {
38 /** @type {import('../users.js').User[]} */
39 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
40
41 /** @type {Promise<import('../users.js').UserViewModel>[]} */
42 const viewModelPromises = users
43 .map(user => new Promise(resolve => {
44 getUserAvatar(user.handle).then(avatar =>
45 resolve({
46 handle: user.handle,
47 name: user.name,
48 avatar: avatar,
49 admin: user.admin,
50 enabled: user.enabled,
51 created: user.created,
52 password: !!user.password,
53 }),
54 );
55 }));
56
57 const viewModels = await Promise.all(viewModelPromises);
58 viewModels.sort((x, y) => (x.created ?? 0) - (y.created ?? 0));
59 return response.json(viewModels);
60 } catch (error) {
61 console.error('User list failed:', error);
62 return response.sendStatus(500);
63 }
64});
65
66router.post('/disable', requireAdminMiddleware, async (request, response) => {
67 try {
68 if (!request.body.handle) {
69 console.warn('Disable user failed: Missing required fields');
70 return response.status(400).json({ error: 'Missing required fields' });
71 }
72
73 if (request.body.handle === request.user.profile.handle) {
74 console.warn('Disable user failed: Cannot disable yourself');
75 return response.status(400).json({ error: 'Cannot disable yourself' });
76 }
77
78 /** @type {import('../users.js').User} */
79 const user = await storage.getItem(toKey(request.body.handle));
80
81 if (!user) {
82 console.error('Disable user failed: User not found');
83 return response.status(404).json({ error: 'User not found' });
84 }
85
86 user.enabled = false;
87 await storage.setItem(toKey(request.body.handle), user);
88 return response.sendStatus(204);
89 } catch (error) {
90 console.error('User disable failed:', error);
91 return response.sendStatus(500);
92 }
93});
94
95router.post('/enable', requireAdminMiddleware, async (request, response) => {
96 try {
97 if (!request.body.handle) {
98 console.warn('Enable user failed: Missing required fields');
99 return response.status(400).json({ error: 'Missing required fields' });
100 }
101
102 /** @type {import('../users.js').User} */
103 const user = await storage.getItem(toKey(request.body.handle));
104
105 if (!user) {
106 console.error('Enable user failed: User not found');
107 return response.status(404).json({ error: 'User not found' });
108 }
109
110 user.enabled = true;
111 await storage.setItem(toKey(request.body.handle), user);
112 return response.sendStatus(204);
113 } catch (error) {
114 console.error('User enable failed:', error);
115 return response.sendStatus(500);
116 }
117});
118
119router.post('/promote', requireAdminMiddleware, async (request, response) => {
120 try {
121 if (!request.body.handle) {
122 console.warn('Promote user failed: Missing required fields');
123 return response.status(400).json({ error: 'Missing required fields' });
124 }
125
126 /** @type {import('../users.js').User} */
127 const user = await storage.getItem(toKey(request.body.handle));
128
129 if (!user) {
130 console.error('Promote user failed: User not found');
131 return response.status(404).json({ error: 'User not found' });
132 }
133
134 user.admin = true;
135 await storage.setItem(toKey(request.body.handle), user);
136 return response.sendStatus(204);
137 } catch (error) {
138 console.error('User promote failed:', error);
139 return response.sendStatus(500);
140 }
141});
142
143router.post('/demote', requireAdminMiddleware, async (request, response) => {
144 try {
145 if (!request.body.handle) {
146 console.warn('Demote user failed: Missing required fields');
147 return response.status(400).json({ error: 'Missing required fields' });
148 }
149
150 if (request.body.handle === request.user.profile.handle) {
151 console.warn('Demote user failed: Cannot demote yourself');
152 return response.status(400).json({ error: 'Cannot demote yourself' });
153 }
154
155 /** @type {import('../users.js').User} */
156 const user = await storage.getItem(toKey(request.body.handle));
157
158 if (!user) {
159 console.error('Demote user failed: User not found');
160 return response.status(404).json({ error: 'User not found' });
161 }
162
163 user.admin = false;
164 await storage.setItem(toKey(request.body.handle), user);
165 return response.sendStatus(204);
166 } catch (error) {
167 console.error('User demote failed:', error);
168 return response.sendStatus(500);
169 }
170});
171
172router.post('/create', requireAdminMiddleware, async (request, response) => {
173 try {
174 if (!request.body.handle || !request.body.name) {
175 console.warn('Create user failed: Missing required fields');
176 return response.status(400).json({ error: 'Missing required fields' });
177 }
178
179 const handles = await getAllUserHandles();
180 const handle = slugify(request.body.handle);
181
182 if (!handle) {
183 console.warn('Create user failed: Invalid handle');
184 return response.status(400).json({ error: 'Invalid handle' });
185 }
186
187 if (handles.some(x => x === handle)) {
188 console.warn('Create user failed: User with that handle already exists');
189 return response.status(409).json({ error: 'User already exists' });
190 }
191
192 const salt = getPasswordSalt();
193 const password = request.body.password ? getPasswordHash(request.body.password, salt) : '';
194
195 const newUser = {
196 handle: handle,
197 name: request.body.name || 'Anonymous',
198 created: Date.now(),
199 password: password,
200 salt: salt,
201 admin: !!request.body.admin,
202 enabled: true,
203 };
204
205 await storage.setItem(toKey(handle), newUser);
206
207 // Create user directories
208 console.info('Creating data directories for', newUser.handle);
209 await ensurePublicDirectoriesExist();
210 const directories = getUserDirectories(newUser.handle);
211 await checkForNewContent([directories], [CONTENT_TYPES.SETTINGS]);
212 return response.json({ handle: newUser.handle });
213 } catch (error) {
214 console.error('User create failed:', error);
215 return response.sendStatus(500);
216 }
217});
218
219router.post('/delete', requireAdminMiddleware, async (request, response) => {
220 try {
221 if (!request.body.handle) {
222 console.warn('Delete user failed: Missing required fields');
223 return response.status(400).json({ error: 'Missing required fields' });
224 }
225
226 if (request.body.handle === request.user.profile.handle) {
227 console.warn('Delete user failed: Cannot delete yourself');
228 return response.status(400).json({ error: 'Cannot delete yourself' });
229 }
230
231 if (request.body.handle === DEFAULT_USER.handle) {
232 console.warn('Delete user failed: Cannot delete default user');
233 return response.status(400).json({ error: 'Sorry, but the default user cannot be deleted. It is required as a fallback.' });
234 }
235
236 await storage.removeItem(toKey(request.body.handle));
237
238 if (request.body.purge) {
239 const directories = getUserDirectories(request.body.handle);
240 console.info('Deleting data directories for', request.body.handle);
241 await fsPromises.rm(directories.root, { recursive: true, force: true });
242 }
243
244 return response.sendStatus(204);
245 } catch (error) {
246 console.error('User delete failed:', error);
247 return response.sendStatus(500);
248 }
249});
250
251router.post('/slugify', requireAdminMiddleware, async (request, response) => {
252 try {
253 if (!request.body.text) {
254 console.warn('Slugify failed: Missing required fields');
255 return response.status(400).json({ error: 'Missing required fields' });
256 }
257
258 const text = slugify(request.body.text);
259
260 return response.send(text);
261 } catch (error) {
262 console.error('Slugify failed:', error);
263 return response.sendStatus(500);
264 }
265});