diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 97de2d1be..43bad1a89 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -7,6 +7,10 @@ import { getDiscordUser } from "../../utils/discord"; import { buildAgentLog, sanitizeString } from "../../utils/misc"; import * as George from "../../tasks/george"; import admin from "firebase-admin"; +import { deleteAllApeKeys } from "../../dal/ape-keys"; +import { deleteAllPresets } from "../../dal/preset"; +import { deleteAll as deleteAllResults } from "../../dal/result"; +import { deleteConfig } from "../../dal/config"; export async function createNewUser( req: MonkeyTypes.Request @@ -37,6 +41,24 @@ export async function deleteUser( return new MonkeyResponse("User deleted"); } +export async function resetUser( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const userInfo = await UserDAL.getUser(uid, "reset user"); + await Promise.all([ + UserDAL.resetUser(uid), + deleteAllApeKeys(uid), + deleteAllPresets(uid), + deleteAllResults(uid), + deleteConfig(uid), + ]); + Logger.logToDb("user_reset", `${userInfo.email} ${userInfo.name}`, uid); + + return new MonkeyResponse("User reset"); +} + export async function updateName( req: MonkeyTypes.Request ): Promise { diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index 5bffab143..07c9ca2b6 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -121,6 +121,15 @@ router.delete( asyncHandler(UserController.deleteUser) ); +router.patch( + "/reset", + authenticateRequest({ + requireFreshToken: true, + }), + RateLimit.userReset, + asyncHandler(UserController.resetUser) +); + router.patch( "/name", authenticateRequest({ diff --git a/backend/src/dal/ape-keys.ts b/backend/src/dal/ape-keys.ts index 3e306e7d2..df518771a 100644 --- a/backend/src/dal/ape-keys.ts +++ b/backend/src/dal/ape-keys.ts @@ -94,3 +94,7 @@ export async function deleteApeKey(uid: string, keyId: string): Promise { throw new MonkeyError(404, "ApeKey not found"); } } + +export async function deleteAllApeKeys(uid: string): Promise { + await db.collection(COLLECTION_NAME).deleteMany({ uid }); +} diff --git a/backend/src/dal/config.ts b/backend/src/dal/config.ts index 90c9f0978..a8bf854f9 100644 --- a/backend/src/dal/config.ts +++ b/backend/src/dal/config.ts @@ -16,3 +16,7 @@ export async function getConfig(uid: string): Promise { const config = await db.collection("configs").findOne({ uid }); return config; } + +export async function deleteConfig(uid: string): Promise { + return await db.collection("configs").deleteOne({ uid }); +} diff --git a/backend/src/dal/preset.ts b/backend/src/dal/preset.ts index 3322783ab..0dcfd598d 100644 --- a/backend/src/dal/preset.ts +++ b/backend/src/dal/preset.ts @@ -69,3 +69,7 @@ export async function removePreset( throw new MonkeyError(404, "Preset not found"); } } + +export async function deleteAllPresets(uid: string): Promise { + await db.collection(COLLECTION_NAME).deleteMany({ uid }); +} diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 880c9ceb1..af6c92d10 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -41,6 +41,43 @@ export async function deleteUser(uid: string): Promise { await getUsersCollection().deleteOne({ uid }); } +export async function resetUser(uid: string): Promise { + await getUsersCollection().updateOne( + { uid }, + { + $set: { + personalBests: { + custom: {}, + quote: {}, + time: {}, + words: {}, + zen: {}, + }, + lbPersonalBests: { + time: {}, + }, + completedTests: 0, + startedTests: 0, + timeTyping: 0, + lbMemory: {}, + bananas: 0, + profileDetails: { + bio: "", + keyboard: "", + socialProfiles: {}, + }, + favoriteQuotes: {}, + customThemes: [], + tags: [], + }, + $unset: { + discordAvatar: "", + discordId: "", + }, + } + ); +} + const DAY_IN_SECONDS = 24 * 60 * 60; const THIRTY_DAYS_IN_SECONDS = DAY_IN_SECONDS * 30; diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts index bd3ad9900..0b4329192 100644 --- a/backend/src/middlewares/rate-limit.ts +++ b/backend/src/middlewares/rate-limit.ts @@ -286,6 +286,13 @@ export const userDelete = rateLimit({ handler: customHandler, }); +export const userReset = rateLimit({ + windowMs: 24 * ONE_HOUR_MS, // 1 day + max: 3 * REQUEST_MULTIPLIER, + keyGenerator: getKeyWithUid, + handler: customHandler, +}); + export const userCheckName = rateLimit({ windowMs: 60 * 1000, max: 60 * REQUEST_MULTIPLIER, diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index 38249b759..1f88083dd 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -27,6 +27,10 @@ export default class Users { return await this.httpClient.delete(BASE_PATH); } + async reset(): Ape.EndpointData { + return await this.httpClient.patch(`${BASE_PATH}/reset`); + } + async updateName(name: string): Ape.EndpointData { return await this.httpClient.patch(`${BASE_PATH}/name`, { payload: { name }, diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts index 078a039dd..b96fc7efa 100644 --- a/frontend/src/ts/popups/simple-popups.ts +++ b/frontend/src/ts/popups/simple-popups.ts @@ -688,6 +688,75 @@ list["deleteAccount"] = new SimplePopup( } ); +list["resetAccount"] = new SimplePopup( + "resetAccount", + "text", + "Reset Account", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + ], + "This is the last time you can change your mind. After pressing the button everything is gone.", + "Reset", + async (_thisPopup, password: string) => { + // + try { + const user = Auth.currentUser; + if (user === null) return; + if (user.providerData.find((p) => p?.providerId === "password")) { + const credential = EmailAuthProvider.credential( + user.email as string, + password + ); + await reauthenticateWithCredential(user, credential); + } else { + await reauthenticateWithPopup(user, AccountController.gmailProvider); + } + Notifications.add("Resetting settings...", 0); + UpdateConfig.reset(); + Loader.show(); + Notifications.add("Resetting account and stats...", 0); + const response = await Ape.users.reset(); + + if (response.status !== 200) { + Loader.hide(); + return Notifications.add( + "There was an error resetting your account. Please try again.", + -1 + ); + } + Loader.hide(); + Notifications.add("Reset complete", 1); + setTimeout(() => { + location.reload(); + }, 3000); + } catch (e) { + const typedError = e as FirebaseError; + Loader.hide(); + if (typedError.code === "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + } + }, + (thisPopup) => { + const user = Auth.currentUser; + if (user === null) return; + + if (!user.providerData.find((p) => p?.providerId === "password")) { + thisPopup.inputs = []; + thisPopup.buttonText = "Reauthenticate to reset"; + } + }, + (_thisPopup) => { + // + } +); + list["clearTagPb"] = new SimplePopup( "clearTagPb", "text", @@ -1178,6 +1247,10 @@ $(".pageSettings #deleteAccount").on("click", () => { list["deleteAccount"].show(); }); +$(".pageSettings #resetAccount").on("click", () => { + list["resetAccount"].show(); +}); + $("#apeKeysPopup .generateApeKey").on("click", () => { list["generateApeKey"].show(); }); diff --git a/frontend/static/html/pages/settings.html b/frontend/static/html/pages/settings.html index 05c88a4e1..b548d7608 100644 --- a/frontend/static/html/pages/settings.html +++ b/frontend/static/html/pages/settings.html @@ -2471,6 +2471,20 @@ +