From 45ea8960e076cc362f892baef1092106a6fde2cf Mon Sep 17 00:00:00 2001 From: Timothy Armes Date: Fri, 12 Apr 2024 16:23:44 +0200 Subject: [PATCH] Account deletion --- .vscode/settings.json | 1 + .../[locale]/(main)/delete-account/page.tsx | 89 +++++++++++++++++++ src/auth.ts | 6 +- src/hooks/useAuthMenuOptions.ts | 10 +-- src/messages/en.json | 44 +++++---- src/messages/fr.json | 42 +++++---- src/server/deleteAccount.ts | 32 +++++++ src/server/models/userModel.ts | 5 ++ src/server/mongodb.ts | 6 +- src/utils/navigation.ts | 5 ++ 10 files changed, 198 insertions(+), 42 deletions(-) create mode 100644 src/app/[locale]/(main)/delete-account/page.tsx create mode 100644 src/server/deleteAccount.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 39e83e3..db917fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "Spiderfy", "spiderified", "superjson", + "supprimer", "tailwindcss", "tanstack", "timothyarmes", diff --git a/src/app/[locale]/(main)/delete-account/page.tsx b/src/app/[locale]/(main)/delete-account/page.tsx new file mode 100644 index 0000000..e4a1d03 --- /dev/null +++ b/src/app/[locale]/(main)/delete-account/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; + +import { signOut } from "next-auth/react"; +import { useTranslations } from "next-intl"; + +import InfoMessage from "@/components/InfoMessage"; +import { deleteAccount } from "@/server/deleteAccount"; +import { useRouter } from "@/utils/navigation"; + +export default function Contact() { + const t = useTranslations("DeleteAccount"); + const at = useTranslations("App"); + const router = useRouter(); + + const [deleting, setDeleting] = useState(false); + const [deleted, setDeleted] = useState(false); + const [error, setError] = useState(false); + + const onDeleteAccount = async () => { + try { + setDeleting(true); + + const result = await deleteAccount(); + if (result.serverError) { + throw new Error(result.serverError); + } + + signOut({ redirect: false }); + + setDeleted(true); + } catch { + setDeleting(false); + setError(true); + } + }; + + return ( +
+
+

+ {t("title")} +

+ + {deleted ? ( + + ) : ( + <> +

+ {t("info")} +

+ + {error && ( + + )} + +
+ + + +
+ + )} +
+
+ ); +} diff --git a/src/auth.ts b/src/auth.ts index be3bccb..7650a54 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,11 +4,15 @@ import NextAuth from "next-auth"; import { clientPromise } from "@/server/mongodb"; import Email from "@/utils/nodemailerProvider"; +export const adapter = MongoDBAdapter(clientPromise, { + databaseName: "userData", +}); + export const { handlers: { GET, POST }, auth, } = NextAuth({ - adapter: MongoDBAdapter(clientPromise, { databaseName: "userData" }), + adapter, providers: [ Email({ diff --git a/src/hooks/useAuthMenuOptions.ts b/src/hooks/useAuthMenuOptions.ts index 50bad97..5196581 100644 --- a/src/hooks/useAuthMenuOptions.ts +++ b/src/hooks/useAuthMenuOptions.ts @@ -43,11 +43,11 @@ export const useAuthMenuOptions = () => { onClick: () => router.push("/zones"), disabled: signingOut, }, - // { - // title: t("deleteAccount"), - // onClick: handleSignOut, - // disabled: signingOut, - // }, + { + title: t("deleteAccount"), + onClick: () => router.push("/delete-account"), + disabled: signingOut, + }, { title: t("signOut"), onClick: handleSignOut, diff --git a/src/messages/en.json b/src/messages/en.json index 9e4ab91..c161c13 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -220,24 +220,6 @@ "ERR_NO_TOURNAMENT_ID": "The link you provided is incorrect" }, - "SignIn": { - "title": "Sign In", - "info": "Sign in to access additional features, such as receiving email notifications for tournaments in your area. There's no need to create an account, just sign in with your email address and we'll send you a link.", - - "emailLabel": "Email", - "emailPlaceholder": "Your email", - "signInButton": "Sign In", - "failure": "Oops, something went wrong. Please try again.", - "checkEmail": "Please check your email for a sign-in link.", - - "email": { - "subject": "Sign in to {host}", - "button": "Sign in", - "ignore": "If you didn't request this, you can safely ignore this email.", - "textMessage": "Click the following link to sign in to {host}: {url}\n\n" - } - }, - "Zones": { "title": "Your Zones", "info": "You can set up zones to receive email notifications for tournaments in these areas. These zones can also be used to filter tournaments on the main map.", @@ -259,5 +241,31 @@ "deleteZoneTitle": "Delete Zone", "deleteZoneInfo": "Are you sure you want to delete this zone? This action cannot be undone." + }, + + "SignIn": { + "title": "Sign In", + "info": "Sign in to access additional features, such as receiving email notifications for tournaments in your area. There's no need to create an account, just sign in with your email address and we'll send you a link.", + + "emailLabel": "Email", + "emailPlaceholder": "Your email", + "signInButton": "Sign In", + "failure": "Oops, something went wrong. Please try again.", + "checkEmail": "Please check your email for a sign-in link.", + + "email": { + "subject": "Sign in to {host}", + "button": "Sign in", + "ignore": "If you didn't request this, you can safely ignore this email.", + "textMessage": "Click the following link to sign in to {host}: {url}\n\n" + } + }, + + "DeleteAccount": { + "title": "Delete Account", + "info": "Are you sure you want to delete your account? This action cannot be undone.", + "deleteButton": "Delete Account", + "success": "Your account has been deleted.", + "failure": "Oops, something went wrong. Please try again later." } } diff --git a/src/messages/fr.json b/src/messages/fr.json index efe78f3..34c3250 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -221,23 +221,6 @@ "ERR_NO_TOURNAMENT_ID": "L'identifiant du tournoi fourni est incorrect" }, - "SignIn": { - "title": "Se connecter", - "info": "Connectez-vous pour accéder à des fonctionnalités supplémentaires, telles que la réception de notifications par e-mail pour les tournois dans votre région. Pas besoin de créer un compte, connectez-vous simplement avec votre adresse e-mail et nous vous enverrons un lien.", - "emailLabel": "E-mail", - "emailPlaceholder": "Votre e-mail", - "signInButton": "Se connecter", - "failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer.", - "checkEmail": "Veuillez vérifier votre e-mail pour un lien de connexion.", - - "email": { - "subject": "Connexion à {host}", - "button": "Se connecter", - "ignore": "Si vous n'avez pas demandé cela, vous pouvez ignorer en toute sécurité cet e-mail.", - "textMessage": "Cliquez sur le lien suivant pour vous connecter à {host}: {url}\n\n" - } - }, - "Zones": { "title": "Vos régions", "info": "Vous pouvez définir des zones personnalisées pour recevoir des notifications par e-mail pour les tournois dans ces zones. Ces zones peuvent aussi être utiliser pour filtrer les tournois sur la carte.", @@ -259,5 +242,30 @@ "deleteZoneTitle": "Supprimer la région", "deleteZoneInfo": "Êtes-vous sûr de vouloir supprimer cette région\u00a0?" + }, + + "SignIn": { + "title": "Se connecter", + "info": "Connectez-vous pour accéder à des fonctionnalités supplémentaires, telles que la réception de notifications par e-mail pour les tournois dans votre région. Pas besoin de créer un compte, connectez-vous simplement avec votre adresse e-mail et nous vous enverrons un lien.", + "emailLabel": "E-mail", + "emailPlaceholder": "Votre e-mail", + "signInButton": "Se connecter", + "failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer.", + "checkEmail": "Veuillez vérifier votre e-mail pour un lien de connexion.", + + "email": { + "subject": "Connexion à {host}", + "button": "Se connecter", + "ignore": "Si vous n'avez pas demandé cela, vous pouvez ignorer en toute sécurité cet e-mail.", + "textMessage": "Cliquez sur le lien suivant pour vous connecter à {host}: {url}\n\n" + } + }, + + "DeleteAccount": { + "title": "Supprimer le compte", + "info": "Êtes-vous sûr de vouloir supprimer votre compte\u00a0? Cette action est irréversible.", + "deleteButton": "Supprimer le compte", + "success": "Votre compte a été supprimé avec succès.", + "failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer." } } diff --git a/src/server/deleteAccount.ts b/src/server/deleteAccount.ts new file mode 100644 index 0000000..4b9ee1b --- /dev/null +++ b/src/server/deleteAccount.ts @@ -0,0 +1,32 @@ +"use server"; + +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +import { adapter, auth } from "@/auth"; +import { collections, dbConnect } from "@/server/mongodb"; +import { errorLog } from "@/utils/logger"; + +import { action } from "./safeAction"; + +export const deleteAccount = action(z.void(), async () => { + try { + await dbConnect(); + + const user = await auth(); + if (!user?.user) { + throw new Error("You must be logged in to delete your account"); + } + + await collections.zones!.deleteMany({ + userId: new ObjectId(user.user!.id!), + }); + + await adapter.deleteUser!(user.user!.id!); + + return true; + } catch (error) { + errorLog(error); + throw error; + } +}); diff --git a/src/server/models/userModel.ts b/src/server/models/userModel.ts index acdff54..8bd75f1 100644 --- a/src/server/models/userModel.ts +++ b/src/server/models/userModel.ts @@ -4,3 +4,8 @@ export type UserModel = { email: string; emailVerified?: Date; }; + +export type VerificationModel = { + identifier: string; + expires: Date; +}; diff --git a/src/server/mongodb.ts b/src/server/mongodb.ts index 409f854..514ae22 100644 --- a/src/server/mongodb.ts +++ b/src/server/mongodb.ts @@ -1,7 +1,7 @@ import mongoDB, { MongoClient } from "mongodb"; import { TournamentModel } from "@/server/models/tournamentModel"; -import { UserModel } from "@/server/models/userModel"; +import { UserModel, VerificationModel } from "@/server/models/userModel"; import { ZoneModel } from "@/server/models/zoneModel"; import { ClubModel } from "./models/clubModel"; @@ -11,6 +11,7 @@ export const collections: { clubs?: mongoDB.Collection; users?: mongoDB.Collection; zones?: mongoDB.Collection; + userVerificationTokens?: mongoDB.Collection; } = {}; if (!process.env.MONGODB_URI) { @@ -52,6 +53,9 @@ export async function dbConnect() { const userData: mongoDB.Db = p.db("userData"); collections.users = userData.collection("userData"); + collections.userVerificationTokens = userData.collection( + "userVerificationTokens", + ); collections.zones = userData.collection("zones"); const tournamentData: mongoDB.Db = p.db("tournamentsFranceDB"); diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts index 92adadd..181187a 100644 --- a/src/utils/navigation.ts +++ b/src/utils/navigation.ts @@ -41,6 +41,11 @@ export const pathnames = { fr: "/contactez-nous", en: "/contact-us", }, + + "/delete-account": { + fr: "/supprimer-compte", + en: "/delete-account", + }, } satisfies Pathnames; export const { Link, redirect, usePathname, useRouter, getPathname } =