diff --git a/keyserver/src/updaters/account-updaters.js b/keyserver/src/updaters/account-updaters.js index a8657b39d..121269e69 100644 --- a/keyserver/src/updaters/account-updaters.js +++ b/keyserver/src/updaters/account-updaters.js @@ -1,238 +1,241 @@ // @flow import invariant from 'invariant'; import bcrypt from 'twin-bcrypt'; import type { ResetPasswordRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, LogInResponse, } from 'lib/types/account-types.js'; import type { ClientAvatar, UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import { updateTypes } from 'lib/types/update-types.js'; import type { UpdateData } from 'lib/types/update-types.js'; import type { PasswordUpdate, UserInfo, UserInfos, } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { getUploadURL } from '../fetchers/upload-fetchers.js'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js'; import { handleAsyncPromise } from '../responders/handlers.js'; import type { Viewer } from '../session/viewer.js'; async function accountUpdater( viewer: Viewer, update: PasswordUpdate, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const newPassword = update.updatedFields.password; if (!newPassword) { // If it's an old client it may have given us an email, // but we don't store those anymore return; } const verifyQuery = SQL` SELECT username, hash FROM users WHERE id = ${viewer.userID} `; const [verifyResult] = await dbQuery(verifyQuery); if (verifyResult.length === 0) { throw new ServerError('internal_error'); } const verifyRow = verifyResult[0]; if (!bcrypt.compareSync(update.currentPassword, verifyRow.hash)) { throw new ServerError('invalid_credentials'); } const changedFields = { hash: bcrypt.hashSync(newPassword) }; const saveQuery = SQL` UPDATE users SET ${changedFields} WHERE id = ${viewer.userID} `; await dbQuery(saveQuery); const updateDatas = [ { type: updateTypes.UPDATE_CURRENT_USER, userID: viewer.userID, time: Date.now(), }, ]; await createUpdates(updateDatas, { viewer, updatesForCurrentSession: 'broadcast', }); } // eslint-disable-next-line no-unused-vars async function checkAndSendVerificationEmail(viewer: Viewer): Promise { // We don't want to crash old clients that call this, // but we have nothing we can do because we no longer store email addresses } async function checkAndSendPasswordResetEmail( // eslint-disable-next-line no-unused-vars request: ResetPasswordRequest, ): Promise { // We don't want to crash old clients that call this, // but we have nothing we can do because we no longer store email addresses } /* eslint-disable no-unused-vars */ async function updatePassword( viewer: Viewer, request: UpdatePasswordRequest, ): Promise { /* eslint-enable no-unused-vars */ // We have no way to handle this request anymore throw new ServerError('deprecated'); } async function updateUserSettings( viewer: Viewer, request: UpdateUserSettingsRequest, ) { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const createOrUpdateSettingsQuery = SQL` INSERT INTO settings (user, name, data) VALUES ${[[viewer.id, request.name, request.data]]} ON DUPLICATE KEY UPDATE data = VALUE(data) `; await dbQuery(createOrUpdateSettingsQuery); } async function updateUserAvatar( viewer: Viewer, request: UpdateUserAvatarRequest, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const newAvatarValue = request.type === 'remove' ? null : JSON.stringify(request); const mediaID = request.type === 'image' ? request.uploadID : null; const query = SQL` START TRANSACTION; UPDATE uploads SET container = NULL WHERE uploader = ${viewer.userID} AND container = ${viewer.userID} AND ( ${mediaID} IS NULL OR EXISTS ( SELECT 1 FROM uploads WHERE id = ${mediaID} AND uploader = ${viewer.userID} AND container IS NULL AND thread IS NULL ) ); UPDATE uploads SET container = ${viewer.userID} WHERE id = ${mediaID} AND uploader = ${viewer.userID} AND container IS NULL AND thread IS NULL; UPDATE users SET avatar = ${newAvatarValue} WHERE id = ${viewer.userID} AND ( ${mediaID} IS NULL OR EXISTS ( SELECT 1 FROM uploads WHERE id = ${mediaID} AND uploader = ${viewer.userID} AND container = ${viewer.userID} AND thread IS NULL ) ); COMMIT; SELECT id AS upload_id, secret AS upload_secret FROM uploads WHERE id = ${mediaID} AND uploader = ${viewer.userID} AND container = ${viewer.userID}; `; const [resultSet] = await dbQuery(query, { multipleStatements: true }); const selectResult = resultSet.pop(); const knownUserInfos: UserInfos = await fetchKnownUserInfos(viewer); - const userUpdatesPromise = createUserAvatarUpdates( - knownUserInfos, - viewer.userID, - ); + const userUpdatesPromise = createUserAvatarUpdates(viewer, knownUserInfos); handleAsyncPromise(userUpdatesPromise); if (request.type === 'remove') { return null; } else if (request.type !== 'image') { return request; } else { const [{ upload_id, upload_secret }] = selectResult; const uploadID = upload_id.toString(); invariant( uploadID === request.uploadID, 'uploadID of upload should match uploadID of UpdateUserAvatarRequest', ); return { type: 'image', uri: getUploadURL(uploadID, upload_secret), }; } } async function createUserAvatarUpdates( + viewer: Viewer, knownUserInfos: UserInfos, - updatedUserID: string, ): Promise { - const usersToUpdate: $ReadOnlyArray = values(knownUserInfos).filter( - (user: UserInfo): boolean => user.id !== updatedUserID, - ); const time = Date.now(); - const updateDatas: $ReadOnlyArray = usersToUpdate.map( + const userUpdates: $ReadOnlyArray = values(knownUserInfos).map( (user: UserInfo): UpdateData => ({ type: updateTypes.UPDATE_USER, userID: user.id, time, - updatedUserID: updatedUserID, + updatedUserID: viewer.userID, }), ); - await createUpdates(updateDatas); + const currentUserUpdate: UpdateData = { + type: updateTypes.UPDATE_CURRENT_USER, + userID: viewer.userID, + time, + }; + + await createUpdates([...userUpdates, currentUserUpdate], { + viewer, + updatesForCurrentSession: 'return', + }); } export { accountUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updateUserSettings, updatePassword, updateUserAvatar, };