You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
9.1 KiB
JavaScript
309 lines
9.1 KiB
JavaScript
|
|
'use strict';
|
|
|
|
const async = require('async');
|
|
const validator = require('validator');
|
|
|
|
const utils = require('../utils');
|
|
const meta = require('../meta');
|
|
const db = require('../database');
|
|
const groups = require('../groups');
|
|
const plugins = require('../plugins');
|
|
|
|
module.exports = function (User) {
|
|
User.updateProfile = async function (uid, data) {
|
|
let fields = [
|
|
'username', 'email', 'fullname', 'website', 'location',
|
|
'groupTitle', 'birthday', 'signature', 'aboutme',
|
|
];
|
|
if (!data.uid) {
|
|
throw new Error('[[error:invalid-update-uid]]');
|
|
}
|
|
const updateUid = data.uid;
|
|
|
|
const result = await plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields });
|
|
fields = result.fields;
|
|
data = result.data;
|
|
|
|
await validateData(uid, data);
|
|
|
|
const oldData = await User.getUserFields(updateUid, fields);
|
|
|
|
await async.each(fields, async function (field) {
|
|
if (!(data[field] !== undefined && typeof data[field] === 'string')) {
|
|
return;
|
|
}
|
|
|
|
data[field] = data[field].trim();
|
|
|
|
if (field === 'email') {
|
|
return await updateEmail(updateUid, data.email);
|
|
} else if (field === 'username') {
|
|
return await updateUsername(updateUid, data.username);
|
|
} else if (field === 'fullname') {
|
|
return await updateFullname(updateUid, data.fullname);
|
|
}
|
|
|
|
await User.setUserField(updateUid, field, data[field]);
|
|
});
|
|
plugins.fireHook('action:user.updateProfile', { uid: uid, data: data, fields: fields, oldData: oldData });
|
|
return await User.getUserFields(updateUid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor']);
|
|
};
|
|
|
|
async function validateData(callerUid, data) {
|
|
await isEmailAvailable(data, data.uid);
|
|
await isUsernameAvailable(data, data.uid);
|
|
await isWebsiteValid(callerUid, data);
|
|
await isAboutMeValid(callerUid, data);
|
|
await isSignatureValid(callerUid, data);
|
|
isFullnameValid(data);
|
|
isLocationValid(data);
|
|
isBirthdayValid(data);
|
|
isGroupTitleValid(data);
|
|
}
|
|
|
|
async function isEmailAvailable(data, uid) {
|
|
if (!data.email) {
|
|
return;
|
|
}
|
|
|
|
if (!utils.isEmailValid(data.email)) {
|
|
throw new Error('[[error:invalid-email]]');
|
|
}
|
|
const email = await User.getUserField(uid, 'email');
|
|
if (email === data.email) {
|
|
return;
|
|
}
|
|
const available = await User.email.available(data.email);
|
|
if (!available) {
|
|
throw new Error('[[error:email-taken]]');
|
|
}
|
|
}
|
|
|
|
async function isUsernameAvailable(data, uid) {
|
|
if (!data.username) {
|
|
return;
|
|
}
|
|
data.username = data.username.trim();
|
|
const userData = await User.getUserFields(uid, ['username', 'userslug']);
|
|
var userslug = utils.slugify(data.username);
|
|
|
|
if (data.username.length < meta.config.minimumUsernameLength) {
|
|
throw new Error('[[error:username-too-short]]');
|
|
}
|
|
|
|
if (data.username.length > meta.config.maximumUsernameLength) {
|
|
throw new Error('[[error:username-too-long]]');
|
|
}
|
|
|
|
if (!utils.isUserNameValid(data.username) || !userslug) {
|
|
throw new Error('[[error:invalid-username]]');
|
|
}
|
|
|
|
if (userslug === userData.userslug) {
|
|
return;
|
|
}
|
|
const exists = await User.existsBySlug(userslug);
|
|
if (exists) {
|
|
throw new Error('[[error:username-taken]]');
|
|
}
|
|
}
|
|
|
|
async function isWebsiteValid(callerUid, data) {
|
|
if (!data.website) {
|
|
return;
|
|
}
|
|
if (data.website.length > 255) {
|
|
throw new Error('[[error:invalid-website]]');
|
|
}
|
|
await User.checkMinReputation(callerUid, data.uid, 'min:rep:website');
|
|
}
|
|
|
|
async function isAboutMeValid(callerUid, data) {
|
|
if (!data.aboutme) {
|
|
return;
|
|
}
|
|
if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
|
|
throw new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]');
|
|
}
|
|
|
|
await User.checkMinReputation(callerUid, data.uid, 'min:rep:aboutme');
|
|
}
|
|
|
|
async function isSignatureValid(callerUid, data) {
|
|
if (!data.signature) {
|
|
return;
|
|
}
|
|
if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
|
|
throw new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]');
|
|
}
|
|
await User.checkMinReputation(callerUid, data.uid, 'min:rep:signature');
|
|
}
|
|
|
|
function isFullnameValid(data) {
|
|
if (data.fullname && (validator.isURL(data.fullname) || data.fullname.length > 255)) {
|
|
throw new Error('[[error:invalid-fullname]]');
|
|
}
|
|
}
|
|
|
|
function isLocationValid(data) {
|
|
if (data.location && (validator.isURL(data.location) || data.location.length > 255)) {
|
|
throw new Error('[[error:invalid-location]]');
|
|
}
|
|
}
|
|
|
|
function isBirthdayValid(data) {
|
|
if (!data.birthday) {
|
|
return;
|
|
}
|
|
|
|
const result = new Date(data.birthday);
|
|
if (result && result.toString() === 'Invalid Date') {
|
|
throw new Error('[[error:invalid-birthday]]');
|
|
}
|
|
}
|
|
|
|
function isGroupTitleValid(data) {
|
|
function checkTitle(title) {
|
|
if (title === 'registered-users' || groups.isPrivilegeGroup(title)) {
|
|
throw new Error('[[error:invalid-group-title]]');
|
|
}
|
|
}
|
|
if (!data.groupTitle) {
|
|
return;
|
|
}
|
|
let groupTitles = [];
|
|
if (validator.isJSON(data.groupTitle)) {
|
|
groupTitles = JSON.parse(data.groupTitle);
|
|
if (!Array.isArray(groupTitles)) {
|
|
throw new Error('[[error:invalid-group-title]]');
|
|
}
|
|
groupTitles.forEach(title => checkTitle(title));
|
|
} else {
|
|
groupTitles = [data.groupTitle];
|
|
checkTitle(data.groupTitle);
|
|
}
|
|
if (!meta.config.allowMultipleBadges && groupTitles.length > 1) {
|
|
data.groupTitle = JSON.stringify(groupTitles[0]);
|
|
}
|
|
}
|
|
|
|
User.checkMinReputation = async function (callerUid, uid, setting) {
|
|
const isSelf = parseInt(callerUid, 10) === parseInt(uid, 10);
|
|
if (!isSelf || meta.config['reputation:disabled']) {
|
|
return;
|
|
}
|
|
const reputation = await User.getUserField(uid, 'reputation');
|
|
if (reputation < meta.config[setting]) {
|
|
throw new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]');
|
|
}
|
|
};
|
|
|
|
async function updateEmail(uid, newEmail) {
|
|
let oldEmail = await User.getUserField(uid, 'email');
|
|
oldEmail = oldEmail || '';
|
|
if (oldEmail === newEmail) {
|
|
return;
|
|
}
|
|
|
|
await db.sortedSetRemove('email:uid', oldEmail.toLowerCase());
|
|
await db.sortedSetRemove('email:sorted', oldEmail.toLowerCase() + ':' + uid);
|
|
await User.auth.revokeAllSessions(uid);
|
|
|
|
await Promise.all([
|
|
db.sortedSetAddBulk([
|
|
['email:uid', uid, newEmail.toLowerCase()],
|
|
['email:sorted', 0, newEmail.toLowerCase() + ':' + uid],
|
|
['user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()],
|
|
['users:notvalidated', Date.now(), uid],
|
|
]),
|
|
User.setUserFields(uid, { email: newEmail, 'email:confirmed': 0 }),
|
|
User.reset.cleanByUid(uid),
|
|
]);
|
|
|
|
if (meta.config.requireEmailConfirmation && newEmail) {
|
|
await User.email.sendValidationEmail(uid, {
|
|
email: newEmail,
|
|
subject: '[[email:email.verify-your-email.subject]]',
|
|
template: 'verify_email',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function updateUsername(uid, newUsername) {
|
|
if (!newUsername) {
|
|
return;
|
|
}
|
|
const userData = await User.getUserFields(uid, ['username', 'userslug']);
|
|
if (userData.username === newUsername) {
|
|
return;
|
|
}
|
|
const newUserslug = utils.slugify(newUsername);
|
|
const now = Date.now();
|
|
await Promise.all([
|
|
updateUidMapping('username', uid, newUsername, userData.username),
|
|
updateUidMapping('userslug', uid, newUserslug, userData.userslug),
|
|
db.sortedSetAdd('user:' + uid + ':usernames', now, newUsername + ':' + now),
|
|
]);
|
|
await db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid);
|
|
await db.sortedSetAdd('username:sorted', 0, newUsername.toLowerCase() + ':' + uid);
|
|
}
|
|
|
|
async function updateUidMapping(field, uid, value, oldValue) {
|
|
if (value === oldValue) {
|
|
return;
|
|
}
|
|
await db.sortedSetRemove(field + ':uid', oldValue);
|
|
await User.setUserField(uid, field, value);
|
|
if (value) {
|
|
await db.sortedSetAdd(field + ':uid', uid, value);
|
|
}
|
|
}
|
|
|
|
async function updateFullname(uid, newFullname) {
|
|
const fullname = await User.getUserField(uid, 'fullname');
|
|
await updateUidMapping('fullname', uid, newFullname, fullname);
|
|
}
|
|
|
|
User.changePassword = async function (uid, data) {
|
|
if (uid <= 0 || !data || !data.uid) {
|
|
throw new Error('[[error:invalid-uid]]');
|
|
}
|
|
User.isPasswordValid(data.newPassword);
|
|
const [isAdmin, hasPassword] = await Promise.all([
|
|
User.isAdministrator(uid),
|
|
User.hasPassword(uid),
|
|
]);
|
|
|
|
if (meta.config['password:disableEdit'] && !isAdmin) {
|
|
throw new Error('[[error:no-privileges]]');
|
|
}
|
|
let isAdminOrPasswordMatch = false;
|
|
const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10);
|
|
if (
|
|
(isAdmin && !isSelf) || // Admins ok
|
|
(!hasPassword && isSelf) // Initial password set ok
|
|
) {
|
|
isAdminOrPasswordMatch = true;
|
|
} else {
|
|
isAdminOrPasswordMatch = await User.isPasswordCorrect(uid, data.currentPassword, data.ip);
|
|
}
|
|
|
|
if (!isAdminOrPasswordMatch) {
|
|
throw new Error('[[user:change_password_error_wrong_current]]');
|
|
}
|
|
|
|
const hashedPassword = await User.hashPassword(data.newPassword);
|
|
await Promise.all([
|
|
User.setUserFields(data.uid, {
|
|
password: hashedPassword,
|
|
rss_token: utils.generateUUID(),
|
|
}),
|
|
User.reset.updateExpiry(data.uid),
|
|
User.auth.revokeAllSessions(data.uid),
|
|
]);
|
|
|
|
plugins.fireHook('action:password.change', { uid: uid, targetUid: data.uid });
|
|
};
|
|
};
|