From 197c6234261660959a309b97276e355e9f554d31 Mon Sep 17 00:00:00 2001 From: Matchu Date: Wed, 10 Mar 2021 05:19:51 -0800 Subject: [PATCH] delete-user.js script I already had a script for this lying around, and adapted it a tiny bit to the repository! Part of me thought about building it in as a support tool. I might've if: - this CLI didn't already exist - we already had tighter permissioning, this is pretty high stakes!! --- package.json | 1 + scripts/delete-user.js | 252 ++++++++++++++++++++++++++ scripts/exported-user-data/.gitignore | 3 + 3 files changed, 256 insertions(+) create mode 100644 scripts/delete-user.js create mode 100644 scripts/exported-user-data/.gitignore diff --git a/package.json b/package.json index 096ab7e..6c03981 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/build-cached-data.js", "cache-asset-manifests": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/cache-asset-manifests.js", + "delete-user": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/delete-user.js", "export-users-to-auth0": "ts-node --compiler=typescript-cached-transpile --transpile-only -r dotenv/config scripts/export-users-to-auth0.js" }, "eslintConfig": { diff --git a/scripts/delete-user.js b/scripts/delete-user.js new file mode 100644 index 0000000..b375823 --- /dev/null +++ b/scripts/delete-user.js @@ -0,0 +1,252 @@ +const fsp = require("fs").promises; +const path = require("path"); + +const argv = require("yargs").argv; +const inquirer = require("inquirer"); + +const connectToDb = require("../src/server/db"); + +async function findUser(db, usernameOrEmail) { + const [ + rows, + _, + ] = await db.execute( + "SELECT * FROM openneo_id.users WHERE name = ? OR email = ? LIMIT 1", + [usernameOrEmail, usernameOrEmail] + ); + if (rows.length === 0) { + throw new Error("user not found"); + } + + const user = rows[0]; + console.log(`Name: ${user.name}`); + console.log(`Email: ${user.email}`); + console.log(`Sign in count: ${user.sign_in_count}`); + console.log(`Last sign in: ${user.last_sign_in_at}`); + + return user; +} + +async function main() { + const [usernameOrEmail] = argv._; + + const { user, password } = await inquirer.prompt([ + { name: "user", message: "MySQL admin user:" }, + { name: "password", type: "password" }, + ]); + const db = await connectToDb({ user, password }); + + console.log("Loading ID user..."); + const idUser = await findUser(db, usernameOrEmail); + console.log("Loading Impress user..."); + const impressUser = await findImpressUser(db, idUser.id); + + console.log("Loading other user data... (1)"); + const [ + closetHangers, + closetLists, + contributions, + neopetsConnections, + outfits, + ] = await Promise.all([ + findAllForUser(db, impressUser.id, "closet_hangers"), + findAllForUser(db, impressUser.id, "closet_lists"), + findAllForUser(db, impressUser.id, "contributions"), + findAllForUser(db, impressUser.id, "neopets_connections"), + findAllForUser(db, impressUser.id, "outfits"), + ]); + + console.log("Loading other user data... (2)"); + const itemOutfitRelationships = await findAllForOutfits( + db, + outfits.map((o) => o.id), + "item_outfit_relationships" + ); + + const userDataToExport = { + idUser, + impressUser, + closetHangers, + closetLists, + contributions, + neopetsConnections, + outfits, + itemOutfitRelationships, + }; + + const userDataToExportAsJson = JSON.stringify(userDataToExport, null, 4); + const userDataFilePath = path.join( + __dirname, + "exported-user-data", + `${idUser.name}-${Date.now()}.json` + ); + await fsp.writeFile(userDataFilePath, userDataToExportAsJson, "utf8"); + console.log(`Wrote to ${userDataFilePath}.`); + + const { shouldDelete } = await inquirer.prompt([ + { + type: "confirm", + default: false, + name: "shouldDelete", + message: "Delete this user?", + }, + ]); + + if (!shouldDelete) { + console.log("Okay, we won't delete this user. Goodbye!"); + return; + } + + const { shouldDeleteConfirm } = await inquirer.prompt([ + { + type: "confirm", + default: false, + name: "shouldDeleteConfirm", + message: "Are you sure?", + }, + ]); + + if (!shouldDeleteConfirm) { + console.log("Okay, we won't delete this user. Goodbye!"); + return; + } + + await Promise.all([ + deleteAllForUser(db, impressUser.id, "closet_hangers"), + deleteAllForUser(db, impressUser.id, "closet_lists"), + deleteAllForUser(db, impressUser.id, "contributions"), + deleteAllForUser(db, impressUser.id, "neopets_connections"), + deleteAllForUser(db, impressUser.id, "outfits"), + deleteAllForOutfits( + db, + outfits.map((o) => o.id), + "item_outfit_relationships" + ), + ]); + + await deleteImpressUser(db, idUser.id); + await deleteUser(db, idUser.id); +} + +async function deleteUser(db, id) { + const [ + results, + _, + ] = await db.execute("DELETE FROM openneo_id.users WHERE id = ? LIMIT 1", [ + id, + ]); + if (results.affectedRows === 0) { + throw new Error("failed to delete impress user"); + } + + console.log(` - Deleted user.`); +} + +async function findImpressUser(db, remoteId) { + const [ + rows, + _, + ] = await db.execute( + "SELECT * FROM openneo_impress.users WHERE remote_id = ? LIMIT 1", + [remoteId] + ); + if (rows.length === 0) { + throw new Error("impress user not found"); + } + + return rows[0]; +} + +async function deleteImpressUser(db, remoteId) { + const [ + results, + _, + ] = await db.execute( + "DELETE FROM openneo_impress.users WHERE remote_id = ? LIMIT 1", + [remoteId] + ); + if (results.affectedRows === 0) { + throw new Error("failed to delete user"); + } + + console.log(` - Deleted impress user.`); +} + +async function findAllForUser(db, impressUserId, table) { + const [ + rows, + _, + ] = await db.execute( + `SELECT * FROM openneo_impress.${table} WHERE user_id = ?`, + [impressUserId] + ); + + console.log(` - Found ${rows.length} ${table}`); + + return rows; +} + +async function deleteAllForUser(db, impressUserId, table) { + const [ + results, + _, + ] = await db.execute( + `DELETE FROM openneo_impress.${table} WHERE user_id = ?`, + [impressUserId] + ); + + console.log(` - Deleted ${results.affectedRows} ${table}`); +} + +async function findAllForOutfits(db, outfitIds, table) { + if (outfitIds.length === 0) { + console.log(` - Skipped searching for ${table}`); + return []; + } + + // mysql2 doesn't seem to have a way to interpolate an array into a prepared + // statement, so we create placeholder "?"s in the query to fill in! This + // keeps us safe from injections, while still being lightweight. + const placeholders = outfitIds.map(() => "?").join(", "); + + const [ + rows, + _, + ] = await db.execute( + `SELECT * FROM openneo_impress.${table} WHERE outfit_id IN (${placeholders})`, + [...outfitIds] + ); + + console.log(` - Found ${rows.length} ${table}`); + + return rows; +} + +async function deleteAllForOutfits(db, outfitIds, table) { + if (outfitIds.length === 0) { + console.log(` - Skipped deleting ${table}`); + return []; + } + + // mysql2 doesn't seem to have a way to interpolate an array into a prepared + // statement, so we create placeholder "?"s in the query to fill in! This + // keeps us safe from injections, while still being lightweight. + const placeholders = outfitIds.map(() => "?").join(", "); + + const [ + results, + _, + ] = await db.execute( + `DELETE FROM openneo_impress.${table} WHERE outfit_id IN (${placeholders})`, + [...outfitIds] + ); + + console.log(` - Deleted ${results.affectedRows} ${table}`); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .then(() => process.exit()); diff --git a/scripts/exported-user-data/.gitignore b/scripts/exported-user-data/.gitignore new file mode 100644 index 0000000..42cffd5 --- /dev/null +++ b/scripts/exported-user-data/.gitignore @@ -0,0 +1,3 @@ +# This is for `delete-user.js` output! Don't publish anything in here! +* +!.gitignore