Add user backups download

This commit is contained in:
Cohee
2024-04-09 22:43:47 +03:00
parent 411a8ef8a7
commit 31cc6e51b5
7 changed files with 1137 additions and 96 deletions

View File

@@ -19,103 +19,176 @@ const {
const router = express.Router();
router.post('/get', requireAdminMiddleware, jsonParser, async (request, response) => {
/** @type {import('../users').User[]} */
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
try {
/** @type {import('../users').User[]} */
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
const viewModels = users
.sort((x, y) => x.created - y.created)
.map(user => ({
handle: user.handle,
name: user.name,
avatar: getUserAvatar(user.handle),
admin: user.admin,
enabled: user.enabled,
created: user.created,
password: !!user.password,
}));
const viewModels = users
.sort((x, y) => x.created - y.created)
.map(user => ({
handle: user.handle,
name: user.name,
avatar: getUserAvatar(user.handle),
admin: user.admin,
enabled: user.enabled,
created: user.created,
password: !!user.password,
}));
return response.json(viewModels);
return response.json(viewModels);
} catch (error) {
console.error('User list failed:', error);
return response.sendStatus(500);
}
});
router.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
if (!request.body.handle) {
console.log('Disable user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
try {
if (!request.body.handle) {
console.log('Disable user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
if (request.body.handle === request.user.profile.handle) {
console.log('Disable user failed: Cannot disable yourself');
return response.status(400).json({ error: 'Cannot disable yourself' });
}
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
if (!user) {
console.log('Disable user failed: User not found');
return response.status(404).json({ error: 'User not found' });
}
user.enabled = false;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
} catch (error) {
console.error('User disable failed:', error);
return response.sendStatus(500);
}
if (request.body.handle === request.user.profile.handle) {
console.log('Disable user failed: Cannot disable yourself');
return response.status(400).json({ error: 'Cannot disable yourself' });
}
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
if (!user) {
console.log('Disable user failed: User not found');
return response.status(404).json({ error: 'User not found' });
}
user.enabled = false;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
});
router.post('/enable', requireAdminMiddleware, jsonParser, async (request, response) => {
if (!request.body.handle) {
console.log('Enable user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
try {
if (!request.body.handle) {
console.log('Enable user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
if (!user) {
console.log('Enable user failed: User not found');
return response.status(404).json({ error: 'User not found' });
}
user.enabled = true;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
} catch (error) {
console.error('User enable failed:', error);
return response.sendStatus(500);
}
});
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
router.post('/promote', requireAdminMiddleware, jsonParser, async (request, response) => {
try {
if (!request.body.handle) {
console.log('Promote user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
if (!user) {
console.log('Enable user failed: User not found');
return response.status(404).json({ error: 'User not found' });
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
if (!user) {
console.log('Promote user failed: User not found');
return response.status(404).json({ error: 'User not found' });
}
user.admin = true;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
} catch (error) {
console.error('User promote failed:', error);
return response.sendStatus(500);
}
});
user.enabled = true;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
router.post('/demote', requireAdminMiddleware, jsonParser, async (request, response) => {
try {
if (!request.body.handle) {
console.log('Demote user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
if (request.body.handle === request.user.profile.handle) {
console.log('Demote user failed: Cannot demote yourself');
return response.status(400).json({ error: 'Cannot demote yourself' });
}
/** @type {import('../users').User} */
const user = await storage.getItem(toKey(request.body.handle));
if (!user) {
console.log('Demote user failed: User not found');
return response.status(404).json({ error: 'User not found' });
}
user.admin = false;
await storage.setItem(toKey(request.body.handle), user);
return response.sendStatus(204);
} catch (error) {
console.error('User demote failed:', error);
return response.sendStatus(500);
}
});
router.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
if (!request.body.handle || !request.body.name) {
console.log('Create user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
try {
if (!request.body.handle || !request.body.name) {
console.log('Create user failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
const handles = await getAllUserHandles();
const handle = slugify(request.body.handle, { lower: true, trim: true });
if (handles.some(x => x === handle)) {
console.log('Create user failed: User with that handle already exists');
return response.status(409).json({ error: 'User already exists' });
}
const salt = getPasswordSalt();
const password = request.body.password ? getPasswordHash(request.body.password, salt) : '';
const newUser = {
uuid: uuid.v4(),
handle: handle,
name: request.body.name || 'Anonymous',
created: Date.now(),
password: password,
salt: salt,
admin: !!request.body.admin,
enabled: true,
};
await storage.setItem(toKey(handle), newUser);
// Create user directories
console.log('Creating data directories for', newUser.handle);
await ensurePublicDirectoriesExist();
const directories = getUserDirectories(newUser.handle);
await checkForNewContent([directories]);
return response.json({ handle: newUser.handle });
} catch (error) {
console.error('User create failed:', error);
return response.sendStatus(500);
}
const handles = await getAllUserHandles();
const handle = slugify(request.body.handle, { lower: true, trim: true });
if (handles.some(x => x === handle)) {
console.log('Create user failed: User with that handle already exists');
return response.status(409).json({ error: 'User already exists' });
}
const salt = getPasswordSalt();
const password = request.body.password ? getPasswordHash(request.body.password, salt) : '';
const newUser = {
uuid: uuid.v4(),
handle: handle,
name: request.body.name || 'Anonymous',
created: Date.now(),
password: password,
salt: salt,
admin: !!request.body.admin,
enabled: true,
};
await storage.setItem(toKey(handle), newUser);
// Create user directories
console.log('Creating data directories for', newUser.handle);
await ensurePublicDirectoriesExist();
const directories = getUserDirectories(newUser.handle);
await checkForNewContent([directories]);
return response.json({ handle: newUser.handle });
});
module.exports = {

View File

@@ -1,7 +1,7 @@
const storage = require('node-persist');
const express = require('express');
const { jsonParser } = require('../express-common');
const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt } = require('../users');
const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive } = require('../users');
const router = express.Router();
@@ -44,11 +44,16 @@ router.get('/me', async (request, response) => {
router.post('/change-password', jsonParser, async (request, response) => {
try {
if (!request.body.handle || !request.body.oldPassword || !request.body.newPassword) {
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));
@@ -62,7 +67,8 @@ router.post('/change-password', jsonParser, async (request, response) => {
return response.status(403).json({ error: 'User is disabled' });
}
if (user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
const isAdminChange = request.user.profile.admin && request.body.handle !== request.user.profile.handle;
if (!isAdminChange && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
console.log('Change password failed: Incorrect password');
return response.status(401).json({ error: 'Incorrect password' });
}
@@ -78,6 +84,27 @@ router.post('/change-password', jsonParser, async (request, response) => {
}
});
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);
}
});
module.exports = {
router,
};

View File

@@ -8,9 +8,10 @@ const os = require('os');
const storage = require('node-persist');
const express = require('express');
const mime = require('mime-types');
const archiver = require('archiver');
const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, DEFAULT_AVATAR } = require('./constants');
const { getConfigValue, color, delay, setConfigValue } = require('./util');
const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = require('./util');
const { readSecret, writeSecret } = require('./endpoints/secrets');
const KEY_PREFIX = 'user:';
@@ -584,6 +585,42 @@ function requireAdminMiddleware(request, response, next) {
return response.sendStatus(403);
}
/**
* Creates an archive of the user's data root directory.
* @param {string} handle User handle
* @param {import('express').Response} response Express response object to write to
* @returns {Promise<void>} Promise that resolves when the archive is created
*/
async function createBackupArchive(handle, response) {
const directories = getUserDirectories(handle);
console.log('Backup requested for', handle);
const archive = archiver('zip');
archive.on('error', function (err) {
response.status(500).send({ error: err.message });
});
// On stream closed we can end the request
archive.on('end', function () {
console.log('Archive wrote %d bytes', archive.pointer());
response.end(); // End the Express response
});
const timestamp = generateTimestamp();
// Set the archive name
response.attachment(`${handle}-${timestamp}.zip`);
// This is the streaming magic
// @ts-ignore
archive.pipe(response);
// Append files from a sub-directory, putting its contents at the root of archive
archive.directory(directories.root, false);
archive.finalize();
}
/**
* Express router for serving files from the user's directories.
*/
@@ -614,6 +651,7 @@ module.exports = {
getCookieSessionName,
getUserAvatar,
shouldRedirectToLogin,
createBackupArchive,
tryAutoLogin,
router,
};