const path = require('path'); const fsPromises = require('fs').promises; const storage = require('node-persist'); const express = require('express'); const crypto = require('crypto'); const { jsonParser } = require('../express-common'); const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } = require('../users'); const { SETTINGS_FILE } = require('../constants'); const contentManager = require('./content-manager'); const { color, Cache } = require('../util'); const { checkForNewContent } = require('./content-manager'); const RESET_CACHE = new Cache(5 * 60 * 1000); const router = express.Router(); router.post('/logout', async (request, response) => { try { if (!request.session) { console.error('Session not available'); return response.sendStatus(500); } request.session.handle = null; return response.sendStatus(204); } catch (error) { console.error(error); return response.sendStatus(500); } }); router.get('/me', async (request, response) => { try { if (!request.user) { return response.sendStatus(403); } const user = request.user.profile; const viewModel = { handle: user.handle, name: user.name, avatar: await getUserAvatar(user.handle), admin: user.admin, password: !!user.password, created: user.created, }; return response.json(viewModel); } catch (error) { console.error(error); return response.sendStatus(500); } }); router.post('/change-avatar', jsonParser, async (request, response) => { try { if (!request.body.handle) { console.log('Change avatar failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) { console.log('Change avatar failed: Unauthorized'); return response.status(403).json({ error: 'Unauthorized' }); } // Avatar is not a data URL or not an empty string if (!request.body.avatar.startsWith('data:image/') && request.body.avatar !== '') { console.log('Change avatar failed: Invalid data URL'); return response.status(400).json({ error: 'Invalid data URL' }); } /** @type {import('../users').User} */ const user = await storage.getItem(toKey(request.body.handle)); if (!user) { console.log('Change avatar failed: User not found'); return response.status(404).json({ error: 'User not found' }); } await storage.setItem(toAvatarKey(request.body.handle), request.body.avatar); return response.sendStatus(204); } catch (error) { console.error(error); return response.sendStatus(500); } }); router.post('/change-password', jsonParser, async (request, response) => { try { if (!request.body.handle) { console.log('Change password failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) { console.log('Change password failed: Unauthorized'); return response.status(403).json({ error: 'Unauthorized' }); } /** @type {import('../users').User} */ const user = await storage.getItem(toKey(request.body.handle)); if (!user) { console.log('Change password failed: User not found'); return response.status(404).json({ error: 'User not found' }); } if (!user.enabled) { console.log('Change password failed: User is disabled'); return response.status(403).json({ error: 'User is disabled' }); } if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) { console.log('Change password failed: Incorrect password'); return response.status(403).json({ error: 'Incorrect password' }); } if (request.body.newPassword) { const salt = getPasswordSalt(); user.password = getPasswordHash(request.body.newPassword, salt); user.salt = salt; } else { user.password = ''; user.salt = ''; } await storage.setItem(toKey(request.body.handle), user); return response.sendStatus(204); } catch (error) { console.error(error); return response.sendStatus(500); } }); router.post('/backup', jsonParser, async (request, response) => { try { const handle = request.body.handle; if (!handle) { console.log('Backup failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } if (handle !== request.user.profile.handle && !request.user.profile.admin) { console.log('Backup failed: Unauthorized'); return response.status(403).json({ error: 'Unauthorized' }); } await createBackupArchive(handle, response); } catch (error) { console.error('Backup failed', error); return response.sendStatus(500); } }); router.post('/reset-settings', jsonParser, async (request, response) => { try { const password = request.body.password; if (request.user.profile.password && request.user.profile.password !== getPasswordHash(password, request.user.profile.salt)) { console.log('Reset settings failed: Incorrect password'); return response.status(403).json({ error: 'Incorrect password' }); } const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE); await fsPromises.rm(pathToFile, { force: true }); await contentManager.checkForNewContent([request.user.directories], [contentManager.CONTENT_TYPES.SETTINGS]); return response.sendStatus(204); } catch (error) { console.error('Reset settings failed', error); return response.sendStatus(500); } }); router.post('/change-name', jsonParser, async (request, response) => { try { if (!request.body.name || !request.body.handle) { console.log('Change name failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) { console.log('Change name failed: Unauthorized'); return response.status(403).json({ error: 'Unauthorized' }); } /** @type {import('../users').User} */ const user = await storage.getItem(toKey(request.body.handle)); if (!user) { console.log('Change name failed: User not found'); return response.status(404).json({ error: 'User not found' }); } user.name = request.body.name; await storage.setItem(toKey(request.body.handle), user); return response.sendStatus(204); } catch (error) { console.error('Change name failed', error); return response.sendStatus(500); } }); router.post('/reset-step1', jsonParser, async (request, response) => { try { const resetCode = String(crypto.randomInt(1000, 9999)); console.log(); console.log(color.magenta(`${request.user.profile.name}, your account reset code is: `) + color.red(resetCode)); console.log(); RESET_CACHE.set(request.user.profile.handle, resetCode); return response.sendStatus(204); } catch (error) { console.error('Recover step 1 failed:', error); return response.sendStatus(500); } }); router.post('/reset-step2', jsonParser, async (request, response) => { try { if (!request.body.code) { console.log('Recover step 2 failed: Missing required fields'); return response.status(400).json({ error: 'Missing required fields' }); } if (request.user.profile.password && request.user.profile.password !== getPasswordHash(request.body.password, request.user.profile.salt)) { console.log('Recover step 2 failed: Incorrect password'); return response.status(400).json({ error: 'Incorrect password' }); } const code = RESET_CACHE.get(request.user.profile.handle); if (!code || code !== request.body.code) { console.log('Recover step 2 failed: Incorrect code'); return response.status(400).json({ error: 'Incorrect code' }); } console.log('Resetting account data:', request.user.profile.handle); await fsPromises.rm(request.user.directories.root, { recursive: true, force: true }); await ensurePublicDirectoriesExist(); await checkForNewContent([request.user.directories]); RESET_CACHE.remove(request.user.profile.handle); return response.sendStatus(204); } catch (error) { console.error('Recover step 2 failed:', error); return response.sendStatus(500); } }); module.exports = { router, };