NodeBB/NodeBB

View on GitHub
public/src/admin/manage/users.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

define('admin/manage/users', [
    'translator', 'benchpress', 'autocomplete', 'api', 'slugify', 'bootbox', 'alerts', 'accounts/invite', 'helpers', 'admin/modules/change-email',
], function (translator, Benchpress, autocomplete, api, slugify, bootbox, alerts, AccountInvite, helpers, changeEmail) {
    const Users = {};

    Users.init = function () {
        $('#results-per-page').val(ajaxify.data.resultsPerPage).on('change', function () {
            const query = utils.params();
            query.resultsPerPage = $('#results-per-page').val();
            const qs = buildSearchQuery(query);
            ajaxify.go(window.location.pathname + '?' + qs);
        });

        $('.export-csv').on('click', function () {
            socket.once('event:export-users-csv', function () {
                alerts.remove('export-users-start');
                alerts.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 alerts.error(err);
                }
                alerts.alert({
                    alert_id: 'export-users-start',
                    message: '[[admin/manage/users:export-users-started]]',
                    timeout: (ajaxify.data.userCount / 5000) * 500,
                });
            });

            return false;
        });

        function getSelectedUids() {
            const uids = [];

            $('.users-table [component="user/select/single"]').each(function () {
                if ($(this).is(':checked')) {
                    uids.push($(this).attr('data-uid'));
                }
            });

            return uids;
        }

        function update(className, state) {
            $('.users-table [component="user/select/single"]:checked').parents('.user-row').find(className).each(function () {
                $(this).toggleClass('hidden', !state);
            });
        }

        function unselectAll() {
            $('.users-table [component="user/select/single"]').prop('checked', false);
            $('.users-table [component="user/select/all"]').prop('checked', false);
        }

        function removeRow(uid) {
            const checkboxEl = document.querySelector(`.users-table [component="user/select/single"][data-uid="${uid}"]`);
            if (checkboxEl) {
                const rowEl = checkboxEl.closest('.user-row');
                rowEl.parentNode.removeChild(rowEl);
            }
        }

        // use onSuccess instead
        function done(successMessage, className, flag) {
            return function (err) {
                if (err) {
                    return alerts.error(err);
                }
                alerts.success(successMessage);
                if (className) {
                    update(className, flag);
                }
                unselectAll();
            };
        }

        function onSuccess(successMessage, className, flag) {
            alerts.success(successMessage);
            if (className) {
                update(className, flag);
            }
            unselectAll();
        }

        $('[component="user/select/all"]').on('click', function () {
            $('.users-table [component="user/select/single"]').prop('checked', $(this).is(':checked'));
        });

        $('.manage-groups').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                alerts.error('[[error:no-users-selected]]');
                return false;
            }
            socket.emit('admin.user.loadGroups', uids, function (err, data) {
                if (err) {
                    return alerts.error(err);
                }
                Benchpress.render('admin/partials/manage_user_groups', data).then(function (html) {
                    const modal = bootbox.dialog({
                        message: html,
                        title: '[[admin/manage/users:manage-groups]]',
                        onEscape: true,
                    });
                    modal.on('shown.bs.modal', function () {
                        autocomplete.group(modal.find('.group-search'), function (ev, ui) {
                            const uid = $(ev.target).attr('data-uid');
                            api.put('/groups/' + ui.item.group.slug + '/membership/' + uid, undefined).then(() => {
                                ui.item.group.nameEscaped = translator.escape(ui.item.group.displayName);
                                app.parseAndTranslate('admin/partials/manage_user_groups', { users: [{ groups: [ui.item.group] }] }, function (html) {
                                    $('[data-uid=' + uid + '] .group-area').append(html.find('.group-area').html());
                                });
                            }).catch(alerts.error);
                        });
                    });
                    modal.on('click', '.group-area a', function () {
                        modal.modal('hide');
                    });
                    modal.on('click', '.remove-group-icon', function () {
                        const groupCard = $(this).parents('[data-group-name]');
                        const groupName = groupCard.attr('data-group-name');
                        const uid = $(this).parents('[data-uid]').attr('data-uid');
                        api.del('/groups/' + slugify(groupName) + '/membership/' + uid).then(() => {
                            groupCard.remove();
                        }).catch(alerts.error);
                        return false;
                    });
                });
            });
        });

        $('.set-reputation').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                alerts.error('[[error:no-users-selected]]');
                return false;
            }
            let currentValue = '';
            if (uids.length === 1) {
                const user = ajaxify.data.users.find(u => u && u.uid === parseInt(uids[0], 10));
                if (user) {
                    currentValue = String(user.reputation);
                }
            }
            const modal = bootbox.dialog({
                message: `<input id="new-reputation" type="text" class="form-control" value="${currentValue}">`,
                title: '[[admin/manage/users:set-reputation]]',
                onEscape: true,
                buttons: {
                    submit: {
                        label: '[[global:save]]',
                        callback: function () {
                            const newReputation = modal.find('#new-reputation').val();
                            if (!utils.isNumber(newReputation)) {
                                alerts.error('[[error:invalid-data]]');
                                return false;
                            }
                            socket.emit('admin.user.setReputation', {
                                value: newReputation,
                                uids: uids,
                            }).then(() => {
                                uids.forEach((uid) => {
                                    $(`[component="user/reputation"][data-uid="${uid}"]`).text(helpers.formattedNumber(newReputation));
                                    const user = ajaxify.data.users.find(u => u && u.uid === parseInt(uid, 10));
                                    if (user) {
                                        user.reputation = newReputation;
                                    }
                                });
                            }).catch(alerts.error);
                        },
                    },
                },
            });
            modal.on('shown.bs.modal', () => {
                modal.find('#new-reputation').selectRange(0, modal.find('#new-reputation').val().length);
            });
        });

        $('.ban-user').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                alerts.error('[[error:no-users-selected]]');
                return false; // specifically to keep the menu open
            }

            bootbox.confirm((uids.length > 1 ? '[[admin/manage/users:alerts.confirm-ban-multi]]' : '[[admin/manage/users:alerts.confirm-ban]]'), function (confirm) {
                if (confirm) {
                    Promise.all(uids.map(function (uid) {
                        return api.put('/users/' + uid + '/ban');
                    })).then(() => {
                        onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
                    }).catch(alerts.error);
                }
            });
        });

        $('.ban-user-temporary').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                alerts.error('[[error:no-users-selected]]');
                return false; // specifically to keep the menu open
            }

            Benchpress.render('modals/temporary-ban', {}).then(function (html) {
                bootbox.dialog({
                    className: 'ban-modal',
                    title: '[[user:ban-account]]',
                    message: html,
                    show: true,
                    buttons: {
                        close: {
                            label: '[[global:close]]',
                            className: 'btn-link',
                        },
                        submit: {
                            label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
                            callback: function () {
                                const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) {
                                    data[cur.name] = cur.value;
                                    return data;
                                }, {});
                                const until = formData.length > 0 ? (
                                    Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
                                ) : 0;

                                Promise.all(uids.map(function (uid) {
                                    return api.put('/users/' + uid + '/ban', {
                                        until: until,
                                        reason: formData.reason,
                                    });
                                })).then(() => {
                                    onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
                                }).catch(alerts.error);
                            },
                        },
                    },
                });
            });
        });

        $('.unban-user').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                alerts.error('[[error:no-users-selected]]');
                return false; // specifically to keep the menu open
            }

            Promise.all(uids.map(function (uid) {
                return api.del('/users/' + uid + '/ban');
            })).then(() => {
                onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false);
            });
        });

        $('.reset-lockout').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }

            socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]'));
        });

        $('.change-email').on('click', function () {
            const uids = getSelectedUids();
            if (uids.length !== 1) {
                return alerts.error('[[admin/manage/users:alerts.select-a-single-user-to-change-email]]');
            }
            changeEmail.init({
                uid: uids[0],
                onSuccess: function (newEmail) {
                    update('.notvalidated', false);
                    update('.pending', false);
                    update('.expired', false);
                    update('.validated', false);
                    update('.validated-by-admin', !!newEmail);
                    update('.no-email', !newEmail);
                    $('.users-table [component="user/select/single"]:checked').parents('.user-row').find('.validated-by-admin .email').text(newEmail);
                    // $('.users-table [component="user/select/single"]:checked').parents('.user-row').find('.no-email').
                },
            });
        });

        $('.validate-email').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }

            bootbox.confirm('[[admin/manage/users:alerts.confirm-validate-email]]', function (confirm) {
                if (!confirm) {
                    return;
                }
                socket.emit('admin.user.validateEmail', uids, function (err) {
                    if (err) {
                        return alerts.error(err);
                    }
                    alerts.success('[[admin/manage/users:alerts.validate-email-success]]');
                    update('.notvalidated', false);
                    update('.pending', false);
                    update('.expired', false);
                    update('.validated', false);
                    update('.validated-by-admin', true);
                    unselectAll();
                });
            });
        });

        $('.send-validation-email').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }
            socket.emit('admin.user.sendValidationEmail', uids, function (err) {
                if (err) {
                    return alerts.error(err);
                }
                alerts.success('[[notifications:email-confirm-sent]]');
            });
        });

        $('.change-password').on('click', async function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }
            async function changePassword(modal) {
                const newPassword = modal.find('#newPassword').val();
                const confirmPassword = modal.find('#confirmPassword').val();
                if (newPassword !== confirmPassword) {
                    throw new Error('[[[user:change-password-error-match]]');
                }
                await Promise.all(uids.map(uid => api.put('/users/' + uid + '/password', {
                    currentPassword: '',
                    newPassword: newPassword,
                })));
            }

            const modal = bootbox.dialog({
                message: `<div class="d-flex flex-column gap-2">
                    <label class="form-label">[[user:new-password]]</label>
                    <input id="newPassword" class="form-control" type="text">
                    <label class="form-label">[[user:confirm-password]]</label>
                    <input id="confirmPassword" class="form-control" type="text">
                </div>`,
                title: '[[admin/manage/users:change-password]]',
                onEscape: true,
                buttons: {
                    cancel: {
                        label: '[[admin/manage/users:alerts.button-cancel]]',
                        className: 'btn-link',
                    },
                    change: {
                        label: '[[admin/manage/users:alerts.button-change]]',
                        className: 'btn-primary',
                        callback: function () {
                            changePassword(modal).then(() => {
                                modal.modal('hide');
                            }).catch(alerts.error);
                            return false;
                        },
                    },
                },
            });
        });

        $('.password-reset-email').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }

            bootbox.confirm('[[admin/manage/users:alerts.password-reset-confirm]]', function (confirm) {
                if (confirm) {
                    socket.emit('admin.user.sendPasswordResetEmail', uids, done('[[admin/manage/users:alerts.password-reset-email-sent]]'));
                }
            });
        });

        $('.force-password-reset').on('click', function () {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }

            bootbox.confirm('[[admin/manage/users:alerts.confirm-force-password-reset]]', function (confirm) {
                if (confirm) {
                    socket.emit('admin.user.forcePasswordReset', uids, done('[[admin/manage/users:alerts.validate-force-password-reset-success]]'));
                }
            });
        });

        $('.delete-user').on('click', () => {
            handleDelete('[[admin/manage/users:alerts.confirm-delete]]', '/account');
        });

        $('.delete-user-content').on('click', () => {
            handleDelete('[[admin/manage/users:alerts.confirm-delete-content]]', '/content');
        });

        $('.delete-user-and-content').on('click', () => {
            handleDelete('[[admin/manage/users:alerts.confirm-purge]]', '');
        });

        const tableEl = document.querySelector('.users-table');
        const actionBtn = document.getElementById('action-dropdown');
        tableEl.addEventListener('change', (e) => {
            const subselector = e.target.closest('[component="user/select/single"]') || e.target.closest('[component="user/select/all"]');
            if (subselector) {
                const uids = getSelectedUids();
                if (uids.length) {
                    actionBtn.removeAttribute('disabled');
                } else {
                    actionBtn.setAttribute('disabled', 'disabled');
                }
            }
        });

        function handleDelete(confirmMsg, path) {
            const uids = getSelectedUids();
            if (!uids.length) {
                return;
            }

            bootbox.confirm(confirmMsg, function (confirm) {
                if (confirm) {
                    Promise.all(
                        uids.map(
                            uid => api.del(`/users/${uid}${path}`, {}).then(() => {
                                if (path !== '/content') {
                                    removeRow(uid);
                                }
                            })
                        )
                    ).then(() => {
                        if (path !== '/content') {
                            alerts.success('[[admin/manage/users:alerts.delete-success]]');
                        } else {
                            alerts.success('[[admin/manage/users:alerts.delete-content-success]]');
                        }
                        unselectAll();
                        if (!$('.users-table [component="user/select/single"]').length) {
                            ajaxify.refresh();
                        }
                    }).catch(alerts.error);
                }
            });
        }

        function handleUserCreate() {
            $('[data-action="create"]').on('click', function () {
                Benchpress.render('admin/partials/create_user_modal', {}).then(function (html) {
                    const modal = bootbox.dialog({
                        message: html,
                        title: '[[admin/manage/users:alerts.create]]',
                        onEscape: true,
                        buttons: {
                            cancel: {
                                label: '[[admin/manage/users:alerts.button-cancel]]',
                                className: 'btn-link',
                            },
                            create: {
                                label: '[[admin/manage/users:alerts.button-create]]',
                                className: 'btn-primary',
                                callback: function () {
                                    createUser.call(this);
                                    return false;
                                },
                            },
                        },
                    });
                    modal.on('shown.bs.modal', function () {
                        modal.find('#create-user-name').focus();
                    });
                });
                return false;
            });
        }

        function createUser() {
            const modal = this;
            const username = document.getElementById('create-user-name').value;
            const email = document.getElementById('create-user-email').value;
            const password = document.getElementById('create-user-password').value;
            const passwordAgain = document.getElementById('create-user-password-again').value;

            const errorEl = $('#create-modal-error');

            if (password !== passwordAgain) {
                return errorEl.translateHtml('[[admin/manage/users:alerts.error-x, [[admin/manage/users:alerts.error-passwords-different]]]]').removeClass('hide');
            }

            const user = {
                username: username,
                email: email,
                password: password,
            };

            api.post('/users', user)
                .then(() => {
                    modal.modal('hide');
                    modal.on('hidden.bs.modal', function () {
                        ajaxify.refresh();
                    });
                    alerts.success('[[admin/manage/users:alerts.create-success]]');
                })
                .catch(err => errorEl.translateHtml('[[admin/manage/users:alerts.error-x, ' + err.message + ']]').removeClass('hidden'));
        }

        handleSearch();
        handleUserCreate();
        handleSort();
        handleFilter();
        AccountInvite.handle();
    };

    function handleSearch() {
        function doSearch() {
            $('.fa-spinner').removeClass('hidden');
            loadSearchPage({
                searchBy: $('#user-search-by').val(),
                query: $('#user-search').val(),
                page: 1,
            });
        }
        $('#user-search').on('keyup', utils.debounce(doSearch, 250));
        $('#user-search-by').on('change', doSearch);
    }

    function loadSearchPage(query) {
        const params = utils.params();
        params.searchBy = query.searchBy;
        params.query = query.query;
        params.page = query.page;
        params.sortBy = params.sortBy || 'lastonline';
        const qs = decodeURIComponent($.param(params));
        $.get(config.relative_path + '/api/admin/manage/users?' + qs, function (data) {
            renderSearchResults(data);
            const url = config.relative_path + '/admin/manage/users?' + qs;
            if (history.pushState) {
                history.pushState({
                    url: url,
                }, null, window.location.protocol + '//' + window.location.host + url);
            }
        }).fail(function (xhrErr) {
            if (xhrErr && xhrErr.responseJSON && xhrErr.responseJSON.error) {
                alerts.error(xhrErr.responseJSON.error);
            }
        });
    }

    function renderSearchResults(data) {
        Benchpress.render('partials/paginator', { pagination: data.pagination }).then(function (html) {
            $('.pagination-container').replaceWith(html);
        });

        app.parseAndTranslate('admin/manage/users', 'users', data, function (html) {
            $('.users-table tbody tr').remove();
            $('.users-table tbody').append(html);
            html.find('.timeago').timeago();
            $('.fa-spinner').addClass('hidden');
            if (!$('#user-search').val()) {
                $('#user-found-notify').addClass('hidden');
                $('#user-notfound-notify').addClass('hidden');
                return;
            }
            if (data && data.users.length === 0) {
                $('#user-notfound-notify').translateHtml('[[admin/manage/users:search.not-found]]')
                    .removeClass('hidden');
                $('#user-found-notify').addClass('hidden');
            } else {
                $('#user-found-notify').translateHtml(
                    translator.compile('admin/manage/users:alerts.x-users-found', data.matchCount, data.timing)
                ).removeClass('hidden');
                $('#user-notfound-notify').addClass('hidden');
            }
        });
    }

    function buildSearchQuery(params) {
        if ($('#user-search').val()) {
            params.query = $('#user-search').val();
            params.searchBy = $('#user-search-by').val();
        } else {
            delete params.query;
            delete params.searchBy;
        }

        return decodeURIComponent($.param(params));
    }

    function handleSort() {
        $('.users-table thead th').on('click', function () {
            const $this = $(this);
            const sortBy = $this.attr('data-sort');
            if (!sortBy) {
                return;
            }
            const params = utils.params();
            params.sortBy = sortBy;
            if (ajaxify.data.sortBy === sortBy) {
                params.sortDirection = ajaxify.data.reverse ? 'asc' : 'desc';
            } else {
                params.sortDirection = 'desc';
            }

            const qs = buildSearchQuery(params);
            ajaxify.go('admin/manage/users?' + qs);
        });
    }

    function getFilters() {
        const filters = [];
        $('#filter-by').find('[data-filter-by]').each(function () {
            if ($(this).find('.fa-check').length) {
                filters.push($(this).attr('data-filter-by'));
            }
        });
        return filters;
    }

    function handleFilter() {
        let currentFilters = getFilters();
        $('#filter-by').on('click', 'li', function () {
            const $this = $(this);
            $this.find('i').toggleClass('fa-check', !$this.find('i').hasClass('fa-check'));
            return false;
        });

        $('#filter-by').on('hidden.bs.dropdown', function () {
            const filters = getFilters();
            let changed = filters.length !== currentFilters.length;
            if (filters.length === currentFilters.length) {
                filters.forEach(function (filter, i) {
                    if (filter !== currentFilters[i]) {
                        changed = true;
                    }
                });
            }
            currentFilters = getFilters();
            if (changed) {
                const params = utils.params();
                params.filters = filters;
                const qs = buildSearchQuery(params);
                ajaxify.go('admin/manage/users?' + qs);
            }
        });
    }

    return Users;
});