diff --git a/server/src/deleters/account-deleters.js b/server/src/deleters/account-deleters.js index e333a9d97..182419317 100644 --- a/server/src/deleters/account-deleters.js +++ b/server/src/deleters/account-deleters.js @@ -1,112 +1,125 @@ // @flow import type { LogOutResponse, DeleteAccountRequest, } from 'lib/types/account-types'; import type { Viewer } from '../session/viewer'; +import type { UserInfo } from 'lib/types/user-types'; import { updateTypes } from 'lib/types/update-types'; import bcrypt from 'twin-bcrypt'; import { ServerError } from 'lib/utils/errors'; import { promiseAll } from 'lib/utils/promises'; +import { values } from 'lib/utils/objects'; import { dbQuery, SQL } from '../database'; import { createNewAnonymousCookie } from '../session/cookies'; -import { fetchAllUserIDs } from '../fetchers/user-fetchers'; +import { fetchKnownUserInfos } from '../fetchers/user-fetchers'; import { createUpdates } from '../creators/update-creator'; import { handleAsyncPromise } from '../responders/handlers'; import { rescindPushNotifs } from '../push/rescind'; async function deleteAccount( viewer: Viewer, request?: DeleteAccountRequest, ): Promise { if (!viewer.loggedIn || (!request && !viewer.isScriptViewer)) { throw new ServerError('not_logged_in'); } if (request) { const hashQuery = SQL`SELECT hash FROM users WHERE id = ${viewer.userID}`; const [result] = await dbQuery(hashQuery); if (result.length === 0) { throw new ServerError('internal_error'); } const row = result[0]; if (!bcrypt.compareSync(request.password, row.hash)) { throw new ServerError('invalid_credentials'); } } const deletedUserID = viewer.userID; await rescindPushNotifs(SQL`n.user = ${deletedUserID}`, SQL`NULL`); + const knownUserInfos = await fetchKnownUserInfos(viewer); + const usersToUpdate = values(knownUserInfos).filter( + userID => userID !== deletedUserID, + ); // TODO: if this results in any orphaned orgs, convert them to chats const deletionQuery = SQL` DELETE u, iu, v, iv, c, ic, m, f, n, ino, up, iup, s, si, ru, rd FROM users u LEFT JOIN ids iu ON iu.id = u.id LEFT JOIN verifications v ON v.user = u.id LEFT JOIN ids iv ON iv.id = v.id LEFT JOIN cookies c ON c.user = u.id LEFT JOIN ids ic ON ic.id = c.id LEFT JOIN memberships m ON m.user = u.id LEFT JOIN focused f ON f.user = u.id LEFT JOIN notifications n ON n.user = u.id LEFT JOIN ids ino ON ino.id = n.id LEFT JOIN updates up ON up.user = u.id LEFT JOIN ids iup ON iup.id = up.id LEFT JOIN sessions s ON u.id = s.user LEFT JOIN ids si ON si.id = s.id LEFT JOIN relationships_undirected ru ON (ru.user1 = u.id OR ru.user2 = u.id) LEFT JOIN relationships_directed rd ON (rd.user1 = u.id OR rd.user2 = u.id) WHERE u.id = ${deletedUserID} `; const promises = {}; promises.deletion = dbQuery(deletionQuery); if (request) { promises.anonymousViewerData = createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }); } const { anonymousViewerData } = await promiseAll(promises); if (anonymousViewerData) { viewer.setNewCookie(anonymousViewerData); } - const deletionUpdatesPromise = createAccountDeletionUpdates(deletedUserID); + const deletionUpdatesPromise = createAccountDeletionUpdates( + usersToUpdate, + deletedUserID, + ); if (request) { handleAsyncPromise(deletionUpdatesPromise); } else { await deletionUpdatesPromise; } if (request) { return { currentUserInfo: { id: viewer.id, anonymous: true, }, }; } return null; } async function createAccountDeletionUpdates( + knownUserInfos: $ReadOnlyArray, deletedUserID: string, ): Promise { - const allUserIDs = await fetchAllUserIDs(); const time = Date.now(); - const updateDatas = allUserIDs.map(userID => ({ - type: updateTypes.DELETE_ACCOUNT, - userID, - time, - deletedUserID, - })); + const updateDatas = []; + for (const userInfo of knownUserInfos) { + const { id: userID } = userInfo; + updateDatas.push({ + type: updateTypes.DELETE_ACCOUNT, + userID, + time, + deletedUserID, + }); + } await createUpdates(updateDatas); } export { deleteAccount }; diff --git a/server/src/fetchers/user-fetchers.js b/server/src/fetchers/user-fetchers.js index f3629d8f7..11e97ce99 100644 --- a/server/src/fetchers/user-fetchers.js +++ b/server/src/fetchers/user-fetchers.js @@ -1,161 +1,161 @@ // @flow import type { UserInfos, CurrentUserInfo, LoggedInUserInfo, } from 'lib/types/user-types'; import type { Viewer } from '../session/viewer'; import { ServerError } from 'lib/utils/errors'; import { dbQuery, SQL } from '../database'; async function fetchUserInfos(userIDs: string[]): Promise { if (userIDs.length <= 0) { return {}; } const query = SQL` SELECT id, username FROM users WHERE id IN (${userIDs}) `; const [result] = await dbQuery(query); const userInfos = {}; for (let row of result) { const id = row.id.toString(); userInfos[id] = { id, username: row.username, }; } for (let userID of userIDs) { if (!userInfos[userID]) { userInfos[userID] = { id: userID, username: null, }; } } return userInfos; } async function fetchKnownUserInfos( viewer: Viewer, userIDs?: $ReadOnlyArray, -) { +): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } if (userIDs && userIDs.length === 0) { return {}; } const query = SQL` SELECT DISTINCT u.id, u.username FROM relationships_undirected r LEFT JOIN users u ON r.user1 = u.id OR r.user2 = u.id `; if (userIDs) { query.append(SQL` WHERE (r.user1 = ${viewer.userID} AND r.user2 IN (${userIDs})) OR (r.user1 IN (${userIDs}) AND r.user2 = ${viewer.userID}) `); } else { query.append(SQL` WHERE r.user1 = ${viewer.userID} OR r.user2 = ${viewer.userID} `); } const [result] = await dbQuery(query); const userInfos = {}; for (let row of result) { const id = row.id.toString(); userInfos[id] = { id, username: row.username, }; } return userInfos; } async function verifyUserIDs( userIDs: $ReadOnlyArray, ): Promise { if (userIDs.length === 0) { return []; } const query = SQL`SELECT id FROM users WHERE id IN (${userIDs})`; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function verifyUserOrCookieIDs( ids: $ReadOnlyArray, ): Promise { if (ids.length === 0) { return []; } const query = SQL` SELECT id FROM users WHERE id IN (${ids}) UNION SELECT id FROM cookies WHERE id IN (${ids}) `; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function fetchCurrentUserInfo(viewer: Viewer): Promise { if (!viewer.loggedIn) { return { id: viewer.cookieID, anonymous: true }; } const currentUserInfos = await fetchLoggedInUserInfos([viewer.userID]); if (currentUserInfos.length === 0) { throw new ServerError('unknown_error'); } return currentUserInfos[0]; } async function fetchLoggedInUserInfos( userIDs: $ReadOnlyArray, ): Promise { const query = SQL` SELECT id, username, email, email_verified FROM users WHERE id IN (${userIDs}) `; const [result] = await dbQuery(query); return result.map(row => ({ id: row.id.toString(), username: row.username, email: row.email, emailVerified: !!row.email_verified, })); } async function fetchAllUserIDs(): Promise { const query = SQL`SELECT id FROM users`; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function fetchUsername(id: string): Promise { const query = SQL`SELECT username FROM users WHERE id = ${id}`; const [result] = await dbQuery(query); if (result.length === 0) { return null; } const row = result[0]; return row.username; } export { fetchUserInfos, verifyUserIDs, verifyUserOrCookieIDs, fetchCurrentUserInfo, fetchLoggedInUserInfos, fetchAllUserIDs, fetchUsername, fetchKnownUserInfos, };