diff --git a/keyserver/addons/rust-node-addon/index.js b/keyserver/addons/rust-node-addon/index.js index 946d01dcd..5db55e7c8 100644 --- a/keyserver/addons/rust-node-addon/index.js +++ b/keyserver/addons/rust-node-addon/index.js @@ -1,48 +1,49 @@ // @flow import invariant from 'invariant'; import { createRequire } from 'module'; const { platform, arch } = process; const importMetaURL = import.meta.url; invariant(importMetaURL, 'import.meta.url should be set'); const require = createRequire(importMetaURL); type RustAPI = { +registerUser: ( userId: string, deviceId: string, username: string, password: string, userPublicKey: string, ) => Promise, + +deleteUser: (userId: string) => Promise, }; async function getRustAPI(): Promise { let nativeBinding = null; if (platform === 'darwin' && arch === 'x64') { // $FlowFixMe nativeBinding = require('./napi/rust-node-addon.darwin-x64.node'); } else if (platform === 'darwin' && arch === 'arm64') { // $FlowFixMe nativeBinding = require('./napi/rust-node-addon.darwin-arm64.node'); } else if (platform === 'linux' && arch === 'x64') { // $FlowFixMe nativeBinding = require('./napi/rust-node-addon.linux-x64-gnu.node'); } else if (platform === 'linux' && arch === 'arm64') { // $FlowFixMe nativeBinding = require('./napi/rust-node-addon.linux-arm64-gnu.node'); } else { throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); } if (!nativeBinding) { throw new Error('Failed to load native binding'); } - const { registerUser } = nativeBinding; - return { registerUser }; + const { registerUser, deleteUser } = nativeBinding; + return { registerUser, deleteUser }; } export { getRustAPI }; diff --git a/keyserver/addons/rust-node-addon/src/delete_user.rs b/keyserver/addons/rust-node-addon/src/delete_user.rs new file mode 100644 index 000000000..eaaf1fc41 --- /dev/null +++ b/keyserver/addons/rust-node-addon/src/delete_user.rs @@ -0,0 +1,30 @@ +use crate::identity::identity_service_client::IdentityServiceClient; +use crate::identity::DeleteUserRequest; +use crate::IDENTITY_SERVICE_SOCKET_ADDR; +use napi::bindgen_prelude::{Error, Result, Status}; +use tonic::Request; +use tracing::instrument; + +#[napi] +#[instrument(skip_all)] +pub async fn delete_user(user_id: String) -> Result<()> { + let mut identity_client = + IdentityServiceClient::connect(IDENTITY_SERVICE_SOCKET_ADDR.as_str()) + .await + .map_err(|_| { + Error::new( + Status::GenericFailure, + "Unable to connect to identity service".to_string(), + ) + })?; + + let request = Request::new(DeleteUserRequest { + user_id: user_id.clone(), + }); + identity_client + .delete_user(request) + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + + Ok(()) +} diff --git a/keyserver/addons/rust-node-addon/src/lib.rs b/keyserver/addons/rust-node-addon/src/lib.rs index 996b15da8..4ff9191a1 100644 --- a/keyserver/addons/rust-node-addon/src/lib.rs +++ b/keyserver/addons/rust-node-addon/src/lib.rs @@ -1,17 +1,18 @@ +pub mod delete_user; pub mod identity_client; pub mod identity { tonic::include_proto!("identity"); } pub mod tunnelbroker_client; #[macro_use] extern crate napi_derive; use lazy_static::lazy_static; use std::env::var; lazy_static! { pub static ref IDENTITY_SERVICE_SOCKET_ADDR: String = var("COMM_IDENTITY_SERVICE_SOCKET_ADDR") .unwrap_or("https://[::1]:50051".to_string()); } diff --git a/keyserver/src/deleters/account-deleters.js b/keyserver/src/deleters/account-deleters.js index 583982432..09e55c34a 100644 --- a/keyserver/src/deleters/account-deleters.js +++ b/keyserver/src/deleters/account-deleters.js @@ -1,142 +1,146 @@ // @flow +import { getRustAPI } from 'rust-node-addon'; import bcrypt from 'twin-bcrypt'; import type { LogOutResponse, DeleteAccountRequest, } from 'lib/types/account-types.js'; import { updateTypes } from 'lib/types/update-types.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { promiseAll } from 'lib/utils/promises.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js'; import { rescindPushNotifs } from '../push/rescind.js'; import { handleAsyncPromise } from '../responders/handlers.js'; import { createNewAnonymousCookie } from '../session/cookies.js'; import type { Viewer } from '../session/viewer.js'; 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]; const requestPasswordConsistentWithDB = !!row.hash === !!request.password; const shouldValidatePassword = !!row.hash; if ( !requestPasswordConsistentWithDB || (shouldValidatePassword && !bcrypt.compareSync(request.password, row.hash)) ) { throw new ServerError('invalid_credentials'); } } const deletedUserID = viewer.userID; await rescindPushNotifs(SQL`n.user = ${deletedUserID}`, SQL`NULL`); + const rustAPIPromise = getRustAPI(); 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` START TRANSACTION; DELETE FROM users WHERE id = ${deletedUserID}; DELETE FROM ids WHERE id = ${deletedUserID}; DELETE c, i FROM cookies c LEFT JOIN ids i ON i.id = c.id WHERE c.user = ${deletedUserID}; DELETE FROM memberships WHERE user = ${deletedUserID}; DELETE FROM focused WHERE user = ${deletedUserID}; DELETE n, i FROM notifications n LEFT JOIN ids i ON i.id = n.id WHERE n.user = ${deletedUserID}; DELETE u, i FROM updates u LEFT JOIN ids i ON i.id = u.id WHERE u.user = ${deletedUserID}; DELETE s, i FROM sessions s LEFT JOIN ids i ON i.id = s.id WHERE s.user = ${deletedUserID}; DELETE r, i FROM reports r LEFT JOIN ids i ON i.id = r.id WHERE r.user = ${deletedUserID}; DELETE FROM relationships_undirected WHERE user1 = ${deletedUserID}; DELETE FROM relationships_undirected WHERE user2 = ${deletedUserID}; DELETE FROM relationships_directed WHERE user1 = ${deletedUserID}; DELETE FROM relationships_directed WHERE user2 = ${deletedUserID}; COMMIT; `; const promises = {}; promises.deletion = dbQuery(deletionQuery, { multipleStatements: true }); if (request) { promises.anonymousViewerData = createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }); } - const { anonymousViewerData } = await promiseAll(promises); + promises.rustAPI = rustAPIPromise; + const { anonymousViewerData, rustAPI } = await promiseAll(promises); + handleAsyncPromise(rustAPI.deleteUser(deletedUserID)); if (anonymousViewerData) { viewer.setNewCookie(anonymousViewerData); } 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 time = Date.now(); 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 };