From 8c193aa0ab2f730f6981d54d2204992c59044c7c Mon Sep 17 00:00:00 2001 From: edinstance Date: Fri, 25 Oct 2024 21:47:47 +0100 Subject: [PATCH 1/5] feat: created a delete user function --- convex/users.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/convex/users.ts b/convex/users.ts index 396a284..71f7e99 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -4,7 +4,7 @@ import { customMutation, customQuery, } from "convex-helpers/server/customFunctions"; -import { ConvexError } from "convex/values"; +import { ConvexError, v } from "convex/values"; import { mutation, query } from "./_generated/server"; export const viewer = query({ @@ -118,3 +118,39 @@ export const authenticatedMutation = customMutation( }; }), ); + +export const deleteUserById = mutation({ + args: { + userId: v.id("users"), + }, + handler: async (ctx, args) => { + if (args.userId === null) { + throw new ConvexError(SIGN_IN_ERROR_MESSAGE); + } + + const userResults = await ctx.db + .query("userResults") + .filter((q) => q.eq(q.field("userId"), args.userId)) + .collect(); + for (const result of userResults) { + await ctx.db.delete(result._id); + } + + const maps = await ctx.db + .query("maps") + .filter((q) => q.eq(q.field("submittedBy"), args.userId)) + .collect(); + for (const map of maps) { + await ctx.db.patch(map._id, { submittedBy: undefined }); + } + + const authAccounts = await ctx.db.query("authAccounts").filter(q => q.eq(q.field("userId"), args.userId)).collect(); + + // Iterate over the queried documents and delete each one + for (const account of authAccounts) { + await ctx.db.delete(account._id); + } + + await ctx.db.delete(args.userId); + }, +}); From c3c8ecabea997208858d2b7b9bfbab808b376b2d Mon Sep 17 00:00:00 2001 From: edinstance Date: Fri, 25 Oct 2024 21:47:59 +0100 Subject: [PATCH 2/5] feat: created a page for user settings --- app/header.tsx | 46 ++++++++++++++++++++++++++++------------- app/settings/page.tsx | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 app/settings/page.tsx diff --git a/app/header.tsx b/app/header.tsx index c95af52..d6160ed 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -1,6 +1,13 @@ "use client"; -import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/convex/_generated/api"; import { useAuthActions } from "@convex-dev/auth/react"; import { ChevronDownIcon, @@ -8,17 +15,10 @@ import { HamburgerMenuIcon, } from "@radix-ui/react-icons"; import { Authenticated, Unauthenticated, useQuery } from "convex/react"; -import dynamic from "next/dynamic"; +import { UserCircleIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { api } from "@/convex/_generated/api"; +import { useState } from "react"; const links = [ { href: "/", label: "Watch" }, @@ -109,11 +109,26 @@ export default function Header() { +
- + + + + + + + Settings + + + + + + +
+ + diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..4a4dd5a --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Page, PageTitle } from "@/components/Page"; +import { Button } from "@/components/ui/button"; +import { api } from "@/convex/_generated/api"; +import { useAuthActions } from "@convex-dev/auth/react"; +import { useMutation, useQuery } from "convex/react"; +import { redirect, useRouter } from "next/navigation"; + +export default function Settings() { + const { signOut } = useAuthActions(); + const deleteAccount = useMutation(api.users.deleteUserById); + const user = useQuery(api.users.getUserOrNull); + + const router = useRouter(); + + const handleDelete = async () => { + if (confirm("Are you sure you want to delete your account?")) { + if (user) { + await signOut(); + await deleteAccount({ userId: user._id }); + + router.push("/"); + } + } + }; + + return ( + + Settings +
+

Delete your account

+

+ If you delete your account, all of your data will be permanently + removed. This includes your game results, and your user information. + Any maps you created and that were approved will not be removed but + you will no longer be the creator of them. This action cannot be + undone. +

+
+ +
+
+
+ ); +} \ No newline at end of file From fb01d710e2acb5b846fb89524c1a5c2105d55ae3 Mon Sep 17 00:00:00 2001 From: edinstance Date: Fri, 25 Oct 2024 21:52:39 +0100 Subject: [PATCH 3/5] feat: added an admin step for deleting users --- convex/users.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/convex/users.ts b/convex/users.ts index 71f7e99..cbde167 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -150,7 +150,16 @@ export const deleteUserById = mutation({ for (const account of authAccounts) { await ctx.db.delete(account._id); } - + + const admin = await ctx.db + .query("admins") + .withIndex("by_userId", (q) => q.eq("userId", args.userId)) + .first(); + + if (admin) { + await ctx.db.delete(admin._id); + } + await ctx.db.delete(args.userId); }, }); From b73c56724cb3a059ab246b4218d59bafba0bc6cd Mon Sep 17 00:00:00 2001 From: edinstance Date: Sat, 26 Oct 2024 02:05:09 +0100 Subject: [PATCH 4/5] fix: updated the delete user mutation to use the authenticated mutation --- convex/users.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/convex/users.ts b/convex/users.ts index cbde167..4e45f59 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -119,18 +119,17 @@ export const authenticatedMutation = customMutation( }), ); -export const deleteUserById = mutation({ - args: { - userId: v.id("users"), - }, +export const deleteUserById = authenticatedMutation({ handler: async (ctx, args) => { - if (args.userId === null) { + + const userId = ctx.userId; + if (userId === null) { throw new ConvexError(SIGN_IN_ERROR_MESSAGE); } const userResults = await ctx.db .query("userResults") - .filter((q) => q.eq(q.field("userId"), args.userId)) + .filter((q) => q.eq(q.field("userId"), userId)) .collect(); for (const result of userResults) { await ctx.db.delete(result._id); @@ -138,13 +137,13 @@ export const deleteUserById = mutation({ const maps = await ctx.db .query("maps") - .filter((q) => q.eq(q.field("submittedBy"), args.userId)) + .filter((q) => q.eq(q.field("submittedBy"), userId)) .collect(); for (const map of maps) { await ctx.db.patch(map._id, { submittedBy: undefined }); } - const authAccounts = await ctx.db.query("authAccounts").filter(q => q.eq(q.field("userId"), args.userId)).collect(); + const authAccounts = await ctx.db.query("authAccounts").filter(q => q.eq(q.field("userId"), userId)).collect(); // Iterate over the queried documents and delete each one for (const account of authAccounts) { @@ -153,13 +152,13 @@ export const deleteUserById = mutation({ const admin = await ctx.db .query("admins") - .withIndex("by_userId", (q) => q.eq("userId", args.userId)) + .withIndex("by_userId", (q) => q.eq("userId", userId)) .first(); if (admin) { await ctx.db.delete(admin._id); } - await ctx.db.delete(args.userId); + await ctx.db.delete(userId); }, }); From 03317aa08a4ef807f0854d1301c12e96ad3a2821 Mon Sep 17 00:00:00 2001 From: edinstance Date: Sun, 27 Oct 2024 17:11:17 +0000 Subject: [PATCH 5/5] feat: updated delete user to use promises rather than multiple awaits --- convex/users.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/convex/users.ts b/convex/users.ts index 4e45f59..932b42e 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -131,8 +131,11 @@ export const deleteUserById = authenticatedMutation({ .query("userResults") .filter((q) => q.eq(q.field("userId"), userId)) .collect(); + + const promises: Promise[] = []; + for (const result of userResults) { - await ctx.db.delete(result._id); + promises.push(ctx.db.delete(result._id)); } const maps = await ctx.db @@ -140,14 +143,14 @@ export const deleteUserById = authenticatedMutation({ .filter((q) => q.eq(q.field("submittedBy"), userId)) .collect(); for (const map of maps) { - await ctx.db.patch(map._id, { submittedBy: undefined }); + promises.push(ctx.db.patch(map._id, { submittedBy: undefined })); } const authAccounts = await ctx.db.query("authAccounts").filter(q => q.eq(q.field("userId"), userId)).collect(); // Iterate over the queried documents and delete each one for (const account of authAccounts) { - await ctx.db.delete(account._id); + promises.push(ctx.db.delete(account._id)); } const admin = await ctx.db @@ -156,9 +159,11 @@ export const deleteUserById = authenticatedMutation({ .first(); if (admin) { - await ctx.db.delete(admin._id); + promises.push(ctx.db.delete(admin._id)); } - await ctx.db.delete(userId); + promises.push(ctx.db.delete(userId)); + + await Promise.all(promises); }, });