Blame Raw
Cohee · 51ad27fb · · 269 lines (9.7 KB)
1 contributor
1import path from 'node:path';
2import { promises as fsPromises } from 'node:fs';
3import crypto from 'node:crypto';
4
5import storage from 'node-persist';
6import express from 'express';
7
8import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey, getAccountVersion } from '../users.js';
9import { SETTINGS_FILE } from '../constants.js';
10import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
11import { color, Cache, getConfigValue } from '../util.js';
12
13const RESET_CACHE = new Cache(5 * 60 * 1000);
14
15export const router = express.Router();
16
17router.post('/logout', async (request, response) => {
18 try {
19 if (!request.session) {
20 console.error('Session not available');
21 return response.sendStatus(500);
22 }
23
24 request.session.handle = null;
25 request.session.csrfToken = null;
26 request.session.version = null;
27 request.session = null;
28 return response.sendStatus(204);
29 } catch (error) {
30 console.error(error);
31 return response.sendStatus(500);
32 }
33});
34
35router.get('/me', async (request, response) => {
36 try {
37 if (!request.user) {
38 return response.sendStatus(403);
39 }
40
41 const user = request.user.profile;
42 const viewModel = {
43 handle: user.handle,
44 name: user.name,
45 avatar: await getUserAvatar(user.handle),
46 admin: user.admin,
47 password: !!user.password,
48 created: user.created,
49 };
50
51 return response.json(viewModel);
52 } catch (error) {
53 console.error(error);
54 return response.sendStatus(500);
55 }
56});
57
58router.post('/change-avatar', async (request, response) => {
59 try {
60 if (!request.body.handle) {
61 console.warn('Change avatar failed: Missing required fields');
62 return response.status(400).json({ error: 'Missing required fields' });
63 }
64
65 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
66 console.error('Change avatar failed: Unauthorized');
67 return response.status(403).json({ error: 'Unauthorized' });
68 }
69
70 // Avatar is not a data URL or not an empty string
71 if (!request.body.avatar.startsWith('data:image/') && request.body.avatar !== '') {
72 console.warn('Change avatar failed: Invalid data URL');
73 return response.status(400).json({ error: 'Invalid data URL' });
74 }
75
76 /** @type {import('../users.js').User} */
77 const user = await storage.getItem(toKey(request.body.handle));
78
79 if (!user) {
80 console.error('Change avatar failed: User not found');
81 return response.status(404).json({ error: 'User not found' });
82 }
83
84 await storage.setItem(toAvatarKey(request.body.handle), request.body.avatar);
85
86 return response.sendStatus(204);
87 } catch (error) {
88 console.error(error);
89 return response.sendStatus(500);
90 }
91});
92
93router.post('/change-password', async (request, response) => {
94 try {
95 if (!request.body.handle) {
96 console.warn('Change password failed: Missing required fields');
97 return response.status(400).json({ error: 'Missing required fields' });
98 }
99
100 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
101 console.error('Change password failed: Unauthorized');
102 return response.status(403).json({ error: 'Unauthorized' });
103 }
104
105 /** @type {import('../users.js').User} */
106 const user = await storage.getItem(toKey(request.body.handle));
107
108 if (!user) {
109 console.error('Change password failed: User not found');
110 return response.status(404).json({ error: 'User not found' });
111 }
112
113 if (!user.enabled) {
114 console.error('Change password failed: User is disabled');
115 return response.status(403).json({ error: 'User is disabled' });
116 }
117
118 if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
119 console.error('Change password failed: Incorrect password');
120 return response.status(403).json({ error: 'Incorrect password' });
121 }
122
123 if (request.body.newPassword) {
124 const salt = getPasswordSalt();
125 user.password = getPasswordHash(request.body.newPassword, salt);
126 user.salt = salt;
127 } else {
128 user.password = '';
129 user.salt = '';
130 }
131
132 await storage.setItem(toKey(request.body.handle), user);
133
134 // Update session version to keep the current session valid after password change
135 if (request.session && request.session.handle === user.handle) {
136 request.session.version = getAccountVersion(user);
137 }
138
139 return response.sendStatus(204);
140 } catch (error) {
141 console.error(error);
142 return response.sendStatus(500);
143 }
144});
145
146router.post('/backup', async (request, response) => {
147 try {
148 const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean');
149
150 if (!allowFullDataBackup) {
151 console.warn('Backup failed: Full data backup is disabled in configuration');
152 return response.status(403).json({ error: 'Full data backup is disabled' });
153 }
154
155 const handle = request.body.handle;
156
157 if (!handle) {
158 console.warn('Backup failed: Missing required fields');
159 return response.status(400).json({ error: 'Missing required fields' });
160 }
161
162 if (handle !== request.user.profile.handle && !request.user.profile.admin) {
163 console.error('Backup failed: Unauthorized');
164 return response.status(403).json({ error: 'Unauthorized' });
165 }
166
167 await createBackupArchive(handle, response);
168 } catch (error) {
169 console.error('Backup failed', error);
170 return response.sendStatus(500);
171 }
172});
173
174router.post('/reset-settings', async (request, response) => {
175 try {
176 const password = request.body.password;
177
178 if (request.user.profile.password && request.user.profile.password !== getPasswordHash(password, request.user.profile.salt)) {
179 console.warn('Reset settings failed: Incorrect password');
180 return response.status(403).json({ error: 'Incorrect password' });
181 }
182
183 const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE);
184 await fsPromises.rm(pathToFile, { force: true });
185 await checkForNewContent([request.user.directories], [CONTENT_TYPES.SETTINGS]);
186
187 return response.sendStatus(204);
188 } catch (error) {
189 console.error('Reset settings failed', error);
190 return response.sendStatus(500);
191 }
192});
193
194router.post('/change-name', async (request, response) => {
195 try {
196 if (!request.body.name || !request.body.handle) {
197 console.warn('Change name failed: Missing required fields');
198 return response.status(400).json({ error: 'Missing required fields' });
199 }
200
201 if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
202 console.error('Change name failed: Unauthorized');
203 return response.status(403).json({ error: 'Unauthorized' });
204 }
205
206 /** @type {import('../users.js').User} */
207 const user = await storage.getItem(toKey(request.body.handle));
208
209 if (!user) {
210 console.warn('Change name failed: User not found');
211 return response.status(404).json({ error: 'User not found' });
212 }
213
214 user.name = request.body.name;
215 await storage.setItem(toKey(request.body.handle), user);
216
217 return response.sendStatus(204);
218 } catch (error) {
219 console.error('Change name failed', error);
220 return response.sendStatus(500);
221 }
222});
223
224router.post('/reset-step1', async (request, response) => {
225 try {
226 const resetCode = String(crypto.randomInt(1000, 9999));
227 console.log();
228 console.log(color.magenta(`${request.user.profile.name}, your account reset code is: `) + color.red(resetCode));
229 console.log();
230 RESET_CACHE.set(request.user.profile.handle, resetCode);
231 return response.sendStatus(204);
232 } catch (error) {
233 console.error('Recover step 1 failed:', error);
234 return response.sendStatus(500);
235 }
236});
237
238router.post('/reset-step2', async (request, response) => {
239 try {
240 if (!request.body.code) {
241 console.warn('Recover step 2 failed: Missing required fields');
242 return response.status(400).json({ error: 'Missing required fields' });
243 }
244
245 if (request.user.profile.password && request.user.profile.password !== getPasswordHash(request.body.password, request.user.profile.salt)) {
246 console.warn('Recover step 2 failed: Incorrect password');
247 return response.status(400).json({ error: 'Incorrect password' });
248 }
249
250 const code = RESET_CACHE.get(request.user.profile.handle);
251
252 if (!code || code !== request.body.code) {
253 console.warn('Recover step 2 failed: Incorrect code');
254 return response.status(400).json({ error: 'Incorrect code' });
255 }
256
257 console.info('Resetting account data:', request.user.profile.handle);
258 await fsPromises.rm(request.user.directories.root, { recursive: true, force: true });
259
260 await ensurePublicDirectoriesExist();
261 await checkForNewContent([request.user.directories]);
262
263 RESET_CACHE.remove(request.user.profile.handle);
264 return response.sendStatus(204);
265 } catch (error) {
266 console.error('Recover step 2 failed:', error);
267 return response.sendStatus(500);
268 }
269});