mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-30 18:46:01 +01:00 
			
		
		
		
	refactor: move export generation logic to v3 controller, GET/HEAD routes for exports
re: #10384
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
|  | ||||
|  | ||||
| define('forum/account/consent', ['forum/account/header', 'alerts'], function (header, alerts) { | ||||
| define('forum/account/consent', ['forum/account/header', 'alerts', 'api'], function (header, alerts, api) { | ||||
| 	const Consent = {}; | ||||
|  | ||||
| 	Consent.init = function () { | ||||
| @@ -17,18 +17,15 @@ define('forum/account/consent', ['forum/account/header', 'alerts'], function (he | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		handleExport($('[data-action="export-profile"]'), 'user.exportProfile', '[[user:consent.export-profile-success]]'); | ||||
| 		handleExport($('[data-action="export-posts"]'), 'user.exportPosts', '[[user:consent.export-posts-success]]'); | ||||
| 		handleExport($('[data-action="export-uploads"]'), 'user.exportUploads', '[[user:consent.export-uploads-success]]'); | ||||
| 		handleExport($('[data-action="export-profile"]'), 'profile', '[[user:consent.export-profile-success]]'); | ||||
| 		handleExport($('[data-action="export-posts"]'), 'posts', '[[user:consent.export-posts-success]]'); | ||||
| 		handleExport($('[data-action="export-uploads"]'), 'uploads', '[[user:consent.export-uploads-success]]'); | ||||
|  | ||||
| 		function handleExport(el, method, success) { | ||||
| 		function handleExport(el, type, success) { | ||||
| 			el.on('click', function () { | ||||
| 				socket.emit(method, { uid: ajaxify.data.uid }, function (err) { | ||||
| 					if (err) { | ||||
| 						return alerts.error(err); | ||||
| 					} | ||||
| 				api.post(`/users/${ajaxify.data.uid}/exports/${type}`).then(() => { | ||||
| 					alerts.success(success); | ||||
| 				}); | ||||
| 				}).catch(alerts.error); | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const validator = require('validator'); | ||||
| const winston = require('winston'); | ||||
|  | ||||
| const db = require('../database'); | ||||
| const user = require('../user'); | ||||
| @@ -441,3 +442,37 @@ usersAPI.changePicture = async (caller, data) => { | ||||
| 		'icon:bgColor': data.bgColor, | ||||
| 	}, ['picture', 'icon:bgColor']); | ||||
| }; | ||||
|  | ||||
| usersAPI.generateExport = async (caller, { uid, type }) => { | ||||
| 	const count = await db.incrObjectField('locks', `export:${uid}${type}`); | ||||
| 	if (count > 1) { | ||||
| 		throw new Error('[[error:already-exporting]]'); | ||||
| 	} | ||||
|  | ||||
| 	const child = require('child_process').fork(`./src/user/jobs/export-${type}.js`, [], { | ||||
| 		env: process.env, | ||||
| 	}); | ||||
| 	child.send({ uid }); | ||||
| 	child.on('error', async (err) => { | ||||
| 		winston.error(err.stack); | ||||
| 		await db.deleteObjectField('locks', `export:${uid}${type}`); | ||||
| 	}); | ||||
| 	child.on('exit', async () => { | ||||
| 		await db.deleteObjectField('locks', `export:${uid}${type}`); | ||||
| 		const userData = await user.getUserFields(uid, ['username', 'userslug']); | ||||
| 		const { displayname } = userData; | ||||
| 		const n = await notifications.create({ | ||||
| 			bodyShort: `[[notifications:${type}-exported, ${displayname}]]`, | ||||
| 			path: `/api/user/${userData.userslug}/export/${type}`, | ||||
| 			nid: `${type}:export:${uid}`, | ||||
| 			from: uid, | ||||
| 		}); | ||||
| 		await notifications.push(n, [caller.uid]); | ||||
| 		await events.log({ | ||||
| 			type: `export:${type}`, | ||||
| 			uid: caller.uid, | ||||
| 			targetUid: uid, | ||||
| 			ip: caller.ip, | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const path = require('path'); | ||||
| const winston = require('winston'); | ||||
|  | ||||
| const user = require('../user'); | ||||
| const privileges = require('../privileges'); | ||||
| @@ -90,7 +91,10 @@ userController.exportProfile = async function (req, res, next) { | ||||
| 	sendExport(`${res.locals.uid}_profile.json`, 'application/json', res, next); | ||||
| }; | ||||
|  | ||||
| // DEPRECATED; Remove in NodeBB v3.0.0 | ||||
| function sendExport(filename, type, res, next) { | ||||
| 	winston.warn(`[users/export] Access via page API is deprecated, use GET /api/v3/users/:uid/exports/:type instead.`); | ||||
|  | ||||
| 	res.sendFile(filename, { | ||||
| 		root: path.join(__dirname, '../../build/export'), | ||||
| 		headers: { | ||||
|   | ||||
| @@ -2,6 +2,9 @@ | ||||
|  | ||||
| const util = require('util'); | ||||
| const nconf = require('nconf'); | ||||
| const path = require('path'); | ||||
| const crypto = require('crypto'); | ||||
| const fs = require('fs').promises; | ||||
|  | ||||
| const db = require('../../database'); | ||||
| const api = require('../../api'); | ||||
| @@ -15,6 +18,12 @@ const helpers = require('../helpers'); | ||||
|  | ||||
| const Users = module.exports; | ||||
|  | ||||
| const exportMetadata = new Map([ | ||||
| 	['posts', ['csv', 'text/csv']], | ||||
| 	['uploads', ['zip', 'application/zip']], | ||||
| 	['profile', ['json', 'application/json']], | ||||
| ]); | ||||
|  | ||||
| const hasAdminPrivilege = async (uid, privilege) => { | ||||
| 	const ok = await privileges.admin.can(`admin:${privilege}`, uid); | ||||
| 	if (!ok) { | ||||
| @@ -296,3 +305,52 @@ Users.confirmEmail = async (req, res) => { | ||||
| 		helpers.formatApiResponse(404, res); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| const prepareExport = async (req, res) => { | ||||
| 	const [extension] = exportMetadata.get(req.params.type); | ||||
| 	const filename = `${req.params.uid}_${req.params.type}.${extension}`; | ||||
| 	try { | ||||
| 		const stat = await fs.stat(path.join(__dirname, '../../../build/export', filename)); | ||||
| 		const modified = new Date(stat.mtimeMs); | ||||
| 		res.set('Last-Modified', modified.toUTCString()); | ||||
| 		res.set('ETag', `"${crypto.createHash('md5').update(String(stat.mtimeMs)).digest('hex')}"`); | ||||
| 		res.status(204); | ||||
| 		return true; | ||||
| 	} catch (e) { | ||||
| 		res.status(404); | ||||
| 		return false; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| Users.checkExportByType = async (req, res) => { | ||||
| 	await prepareExport(req, res); | ||||
| 	res.end(); | ||||
| }; | ||||
|  | ||||
| Users.getExportByType = async (req, res) => { | ||||
| 	const [extension, mime] = exportMetadata.get(req.params.type); | ||||
| 	const filename = `${req.params.uid}_${req.params.type}.${extension}`; | ||||
|  | ||||
| 	const exists = await prepareExport(req, res); | ||||
| 	if (!exists) { | ||||
| 		return res.end(); | ||||
| 	} | ||||
|  | ||||
| 	res.status(200); | ||||
| 	res.sendFile(filename, { | ||||
| 		root: path.join(__dirname, '../../../build/export'), | ||||
| 		headers: { | ||||
| 			'Content-Type': mime, | ||||
| 			'Content-Disposition': `attachment; filename=${filename}`, | ||||
| 		}, | ||||
| 	}, (err) => { | ||||
| 		if (err) { | ||||
| 			throw err; | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| Users.generateExportsByType = async (req, res) => { | ||||
| 	await api.users.generateExport(req, req.params); | ||||
| 	helpers.formatApiResponse(202, res); | ||||
| }; | ||||
|   | ||||
| @@ -51,6 +51,10 @@ function authenticatedRoutes() { | ||||
| 	setupApiRoute(router, 'get', '/:uid/emails/:email', [...middlewares, middleware.assert.user], controllers.write.users.getEmail); | ||||
| 	setupApiRoute(router, 'post', '/:uid/emails/:email/confirm', [...middlewares, middleware.assert.user], controllers.write.users.confirmEmail); | ||||
|  | ||||
| 	setupApiRoute(router, 'head', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.checkExportByType); | ||||
| 	setupApiRoute(router, 'get', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.getExportByType); | ||||
| 	setupApiRoute(router, 'post', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.generateExportsByType); | ||||
|  | ||||
| 	// Shorthand route to access user routes by userslug | ||||
| 	router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug); | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const winston = require('winston'); | ||||
|  | ||||
| const user = require('../../user'); | ||||
| const events = require('../../events'); | ||||
| const notifications = require('../../notifications'); | ||||
| const privileges = require('../../privileges'); | ||||
| const db = require('../../database'); | ||||
| const plugins = require('../../plugins'); | ||||
|  | ||||
| const sockets = require('..'); | ||||
| const api = require('../../api'); | ||||
|  | ||||
| module.exports = function (SocketUser) { | ||||
| 	SocketUser.updateCover = async function (socket, data) { | ||||
| 		if (!socket.uid) { | ||||
| @@ -64,6 +62,8 @@ module.exports = function (SocketUser) { | ||||
| 	}; | ||||
|  | ||||
| 	async function doExport(socket, data, type) { | ||||
| 		sockets.warnDeprecated(socket, 'POST /api/v3/users/:uid/exports/:type'); | ||||
|  | ||||
| 		if (!socket.uid) { | ||||
| 			throw new Error('[[error:invalid-uid]]'); | ||||
| 		} | ||||
| @@ -74,36 +74,6 @@ module.exports = function (SocketUser) { | ||||
|  | ||||
| 		await user.isAdminOrSelf(socket.uid, data.uid); | ||||
|  | ||||
| 		const count = await db.incrObjectField('locks', `export:${data.uid}${type}`); | ||||
| 		if (count > 1) { | ||||
| 			throw new Error('[[error:already-exporting]]'); | ||||
| 		} | ||||
|  | ||||
| 		const child = require('child_process').fork(`./src/user/jobs/export-${type}.js`, [], { | ||||
| 			env: process.env, | ||||
| 		}); | ||||
| 		child.send({ uid: data.uid }); | ||||
| 		child.on('error', async (err) => { | ||||
| 			winston.error(err.stack); | ||||
| 			await db.deleteObjectField('locks', `export:${data.uid}${type}`); | ||||
| 		}); | ||||
| 		child.on('exit', async () => { | ||||
| 			await db.deleteObjectField('locks', `export:${data.uid}${type}`); | ||||
| 			const userData = await user.getUserFields(data.uid, ['username', 'userslug']); | ||||
| 			const { displayname } = userData; | ||||
| 			const n = await notifications.create({ | ||||
| 				bodyShort: `[[notifications:${type}-exported, ${displayname}]]`, | ||||
| 				path: `/api/user/${userData.userslug}/export/${type}`, | ||||
| 				nid: `${type}:export:${data.uid}`, | ||||
| 				from: data.uid, | ||||
| 			}); | ||||
| 			await notifications.push(n, [socket.uid]); | ||||
| 			await events.log({ | ||||
| 				type: `export:${type}`, | ||||
| 				uid: socket.uid, | ||||
| 				targetUid: data.uid, | ||||
| 				ip: socket.ip, | ||||
| 			}); | ||||
| 		}); | ||||
| 		api.users.generateExport(socket, { type, ...data }); | ||||
| 	} | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user