mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
fix: #8969, export csv to file
This commit is contained in:
@@ -102,5 +102,7 @@
|
|||||||
|
|
||||||
"alerts.prompt-email": "Emails: ",
|
"alerts.prompt-email": "Emails: ",
|
||||||
"alerts.email-sent-to": "An invitation email has been sent to %1",
|
"alerts.email-sent-to": "An invitation email has been sent to %1",
|
||||||
"alerts.x-users-found": "%1 user(s) found, (%2 seconds)"
|
"alerts.x-users-found": "%1 user(s) found, (%2 seconds)",
|
||||||
|
"export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.",
|
||||||
|
"export-users-completed": "Users exported as csv, click here to download."
|
||||||
}
|
}
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"profile-exported": "<strong>%1</strong> profile exported, click to download",
|
"profile-exported": "<strong>%1</strong> profile exported, click to download",
|
||||||
"posts-exported": "<strong>%1</strong> posts exported, click to download",
|
"posts-exported": "<strong>%1</strong> posts exported, click to download",
|
||||||
"uploads-exported": "<strong>%1</strong> uploads exported, click to download",
|
"uploads-exported": "<strong>%1</strong> uploads exported, click to download",
|
||||||
|
"users-csv-exported": "Users csv exported, click to download",
|
||||||
|
|
||||||
"email-confirmed": "Email Confirmed",
|
"email-confirmed": "Email Confirmed",
|
||||||
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
|
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
|
||||||
|
|||||||
@@ -13,6 +13,34 @@ define('admin/manage/users', [
|
|||||||
ajaxify.go(window.location.pathname + '?' + qs);
|
ajaxify.go(window.location.pathname + '?' + qs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.export-csv').on('click', function () {
|
||||||
|
socket.once('event:export-users-csv', function () {
|
||||||
|
app.removeAlert('export-users-start');
|
||||||
|
app.alert({
|
||||||
|
alert_id: 'export-users',
|
||||||
|
type: 'success',
|
||||||
|
title: '[[global:alert.success]]',
|
||||||
|
message: '[[admin/manage/users:export-users-completed]]',
|
||||||
|
clickfn: function () {
|
||||||
|
window.location.href = config.relative_path + '/api/admin/users/csv';
|
||||||
|
},
|
||||||
|
timeout: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socket.emit('admin.user.exportUsersCSV', {}, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return app.alertError(err);
|
||||||
|
}
|
||||||
|
app.alert({
|
||||||
|
alert_id: 'export-users-start',
|
||||||
|
message: '[[admin/manage/users:export-users-started]]',
|
||||||
|
timeout: (ajaxify.data.userCount / 5000) * 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
function getSelectedUids() {
|
function getSelectedUids() {
|
||||||
var uids = [];
|
var uids = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const nconf = require('nconf');
|
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
|
|
||||||
const user = require('../../user');
|
const user = require('../../user');
|
||||||
@@ -242,7 +241,7 @@ async function render(req, res, data) {
|
|||||||
filterBy.forEach(function (filter) {
|
filterBy.forEach(function (filter) {
|
||||||
data['filterBy_' + validator.escape(String(filter))] = true;
|
data['filterBy_' + validator.escape(String(filter))] = true;
|
||||||
});
|
});
|
||||||
|
data.userCount = await db.getObjectField('global', 'userCount');
|
||||||
if (data.adminInviteOnly) {
|
if (data.adminInviteOnly) {
|
||||||
data.showInviteButton = await privileges.users.isAdministrator(req.uid);
|
data.showInviteButton = await privileges.users.isAdministrator(req.uid);
|
||||||
} else {
|
} else {
|
||||||
@@ -252,19 +251,27 @@ async function render(req, res, data) {
|
|||||||
res.render('admin/manage/users', data);
|
res.render('admin/manage/users', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
usersController.getCSV = async function (req, res) {
|
usersController.getCSV = async function (req, res, next) {
|
||||||
var referer = req.headers.referer;
|
await events.log({
|
||||||
|
|
||||||
if (!referer || !referer.replace(nconf.get('url'), '').startsWith('/admin/manage/users')) {
|
|
||||||
return res.status(403).send('[[error:invalid-origin]]');
|
|
||||||
}
|
|
||||||
events.log({
|
|
||||||
type: 'getUsersCSV',
|
type: 'getUsersCSV',
|
||||||
uid: req.uid,
|
uid: req.uid,
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
});
|
});
|
||||||
const data = await user.getUsersCSV();
|
const path = require('path');
|
||||||
res.attachment('users.csv');
|
const { baseDir } = require('../../constants').paths;
|
||||||
res.setHeader('Content-Type', 'text/csv');
|
res.sendFile('users.csv', {
|
||||||
res.end(data);
|
root: path.join(baseDir, 'build/export'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/csv',
|
||||||
|
'Content-Disposition': 'attachment; filename=users.csv',
|
||||||
|
},
|
||||||
|
}, function (err) {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
res.locals.isAPI = false;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
const db = require('../../database');
|
const db = require('../../database');
|
||||||
const api = require('../../api');
|
const api = require('../../api');
|
||||||
@@ -157,3 +158,27 @@ User.loadGroups = async function (socket, uids) {
|
|||||||
});
|
});
|
||||||
return { users: userData };
|
return { users: userData };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.exportUsersCSV = async function (socket) {
|
||||||
|
await events.log({
|
||||||
|
type: 'exportUsersCSV',
|
||||||
|
uid: socket.uid,
|
||||||
|
ip: socket.ip,
|
||||||
|
});
|
||||||
|
setTimeout(async function () {
|
||||||
|
try {
|
||||||
|
await user.exportUsersCSV();
|
||||||
|
socket.emit('event:export-users-csv');
|
||||||
|
const notifications = require('../../notifications');
|
||||||
|
const n = await notifications.create({
|
||||||
|
bodyShort: '[[notifications:users-csv-exported]]',
|
||||||
|
path: '/api/admin/users/csv',
|
||||||
|
nid: 'users:csv:export',
|
||||||
|
from: socket.uid,
|
||||||
|
});
|
||||||
|
await notifications.push(n, [socket.uid]);
|
||||||
|
} catch (err) {
|
||||||
|
winston.error(err);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
|
|
||||||
|
const { baseDir } = require('../constants').paths;
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const batch = require('../batch');
|
const batch = require('../batch');
|
||||||
@@ -36,11 +39,35 @@ module.exports = function (User) {
|
|||||||
await batch.processSortedSet('users:joindate', async (uids) => {
|
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||||
const usersData = await User.getUsersFields(uids, data.fields);
|
const usersData = await User.getUsersFields(uids, data.fields);
|
||||||
csvContent += usersData.reduce((memo, user) => {
|
csvContent += usersData.reduce((memo, user) => {
|
||||||
memo += user.email + ',' + user.username + ',' + user.uid + '\n';
|
memo += data.fields.map(field => user[field]).join(',') + '\n';
|
||||||
return memo;
|
return memo;
|
||||||
}, '');
|
}, '');
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return csvContent;
|
return csvContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.exportUsersCSV = async function () {
|
||||||
|
winston.verbose('[user/exportUsersCSV] Exporting User CSV data');
|
||||||
|
|
||||||
|
const data = await plugins.hooks.fire('filter:user.csvFields', { fields: ['email', 'username', 'uid'] });
|
||||||
|
const fd = await fs.promises.open(
|
||||||
|
path.join(baseDir, 'build/export', 'users.csv'),
|
||||||
|
'w'
|
||||||
|
);
|
||||||
|
fs.promises.appendFile(fd, data.fields.join(',') + '\n');
|
||||||
|
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||||
|
const usersData = await User.getUsersFields(uids, data.fields.slice());
|
||||||
|
let line = '';
|
||||||
|
usersData.forEach(function (user) {
|
||||||
|
line += data.fields.map(field => user[field]).join(',') + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.promises.appendFile(fd, line);
|
||||||
|
}, {
|
||||||
|
batch: 5000,
|
||||||
|
interval: 250,
|
||||||
|
});
|
||||||
|
await fd.close();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- IF showInviteButton -->
|
<!-- IF showInviteButton -->
|
||||||
<button component="user/invite" class="btn btn-success"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button>
|
<button component="user/invite" class="btn btn-success"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button>
|
||||||
<!-- ENDIF showInviteButton -->
|
<!-- ENDIF showInviteButton -->
|
||||||
<a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary">[[admin/manage/users:download-csv]]</a>
|
<a target="_blank" href="#" class="btn btn-primary export-csv">[[admin/manage/users:download-csv]]</a>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
|
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
|||||||
Reference in New Issue
Block a user