diff --git a/src/components/Account/DeleteAccount.tsx b/src/components/Account/DeleteAccount.tsx index 73af80c..b55b33e 100644 --- a/src/components/Account/DeleteAccount.tsx +++ b/src/components/Account/DeleteAccount.tsx @@ -1,7 +1,10 @@ +import UserContext from '@/contexts/UserContext'; import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer'; + import { Switch } from '@headlessui/react'; import { useRouter } from 'next/router'; -import { Dispatch, FunctionComponent, useRef } from 'react'; +import { Dispatch, FunctionComponent, useContext, useRef } from 'react'; +import { toast } from 'react-hot-toast'; interface DeleteAccountProps { pageState: AccountPageState; @@ -13,6 +16,26 @@ const DeleteAccount: FunctionComponent = ({ }) => { const deleteRef = useRef(null); const router = useRouter(); + const { user, mutate } = useContext(UserContext); + + const onDeleteSubmit = async () => { + deleteRef.current!.close(); + const loadingToast = toast.loading( + 'Deleting your account. We are sad to see you go. 😭', + ); + const request = await fetch(`/api/users/${user?.id}`, { + method: 'DELETE', + }); + + if (!request.ok) { + throw new Error('Could not delete that user.'); + } + + toast.remove(loadingToast); + toast.success('Deleted your account. Goodbye. 😓'); + await mutate!(); + router.push('/'); + }; return (
@@ -49,10 +72,7 @@ const DeleteAccount: FunctionComponent = ({
diff --git a/src/components/Account/Security.tsx b/src/components/Account/Security.tsx index 5a03485..4e7ba4a 100644 --- a/src/components/Account/Security.tsx +++ b/src/components/Account/Security.tsx @@ -72,14 +72,14 @@ const Security: FunctionComponent = ({ dispatch, pageState }) => formValidationSchema={register('password')} /> - Confirm Password + Confirm Password {formState.errors.confirmPassword?.message} diff --git a/src/components/ui/CustomToast.tsx b/src/components/ui/CustomToast.tsx index 1ad6822..cc038f7 100644 --- a/src/components/ui/CustomToast.tsx +++ b/src/components/ui/CustomToast.tsx @@ -27,7 +27,7 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => { const alertType = toastToClassName(t.type); return (

{resolveValue(t.message, t)}

{t.type !== 'loading' && ( diff --git a/src/contexts/UserContext.ts b/src/contexts/UserContext.ts deleted file mode 100644 index ac84c98..0000000 --- a/src/contexts/UserContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import useUser from '@/hooks/auth/useUser'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; -import { createContext } from 'react'; -import { z } from 'zod'; - -const UserContext = createContext<{ - user?: z.infer; - error?: unknown; - isLoading: boolean; - mutate?: ReturnType['mutate']; -}>({ isLoading: true }); - -export default UserContext; diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx new file mode 100644 index 0000000..2cf7213 --- /dev/null +++ b/src/contexts/UserContext.tsx @@ -0,0 +1,24 @@ +import useUser from '@/hooks/auth/useUser'; +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import { ReactNode, createContext } from 'react'; +import { z } from 'zod'; + +const UserContext = createContext<{ + user?: z.infer; + error?: unknown; + isLoading: boolean; + mutate?: ReturnType['mutate']; +}>({ isLoading: true }); + +export default UserContext; + +type AuthProviderComponent = (props: { children: ReactNode }) => JSX.Element; + +export const AuthProvider: AuthProviderComponent = ({ children }) => { + const { error, isLoading, mutate, user } = useUser(); + return ( + + {children} + + ); +}; diff --git a/src/hooks/auth/useUser.ts b/src/hooks/auth/useUser.ts index 86c4a9f..39a98d7 100644 --- a/src/hooks/auth/useUser.ts +++ b/src/hooks/auth/useUser.ts @@ -46,7 +46,12 @@ const useUser = () => { return parsedPayload.data; }); - return { user, isLoading, error: error as unknown, mutate }; + return { + mutate, + isLoading, + user: error ? undefined : user, + error: error as unknown, + }; }; export default useUser; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 86a4e86..64d822b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,4 @@ -import UserContext from '@/contexts/UserContext'; +import UserContext, { AuthProvider } from '@/contexts/UserContext'; import '@/styles/globals.css'; import type { AppProps } from 'next/app'; @@ -13,15 +13,12 @@ import Layout from '@/components/ui/Layout'; import useUser from '@/hooks/auth/useUser'; import CustomToast from '@/components/ui/CustomToast'; -const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], -}); +const spaceGrotesk = Space_Grotesk({ subsets: ['latin'] }); -export default function App({ Component, pageProps }: AppProps) { +const App = ({ Component, pageProps }: AppProps) => { useEffect(() => { themeChange(false); }, []); - const { user, isLoading, error, mutate } = useUser(); return ( <> @@ -38,15 +35,17 @@ export default function App({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> - + - + ); -} +}; + +export default App; diff --git a/src/pages/api/users/[id]/edit.ts b/src/pages/api/users/[id]/index.ts similarity index 76% rename from src/pages/api/users/[id]/edit.ts rename to src/pages/api/users/[id]/index.ts index e644878..46f7c0f 100644 --- a/src/pages/api/users/[id]/edit.ts +++ b/src/pages/api/users/[id]/index.ts @@ -4,6 +4,7 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import ServerError from '@/config/util/ServerError'; import DBClient from '@/prisma/DBClient'; +import deleteUserById from '@/services/User/deleteUserById'; import findUserByEmail from '@/services/User/findUserByEmail'; import findUserById from '@/services/User/findUserById'; import findUserByUsername from '@/services/User/findUserByUsername'; @@ -21,11 +22,12 @@ const EditUserSchema = BaseCreateUserSchema.pick({ lastName: true, }); -interface EditUserRequest extends UserExtendedNextApiRequest { +interface UserRouteRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} + +interface EditUserRequest extends UserRouteRequest { body: z.infer; - query: { - id: string; - }; } const checkIfUserCanEditUser = async ( @@ -41,7 +43,7 @@ const checkIfUserCanEditUser = async ( } if (authenticatedUser.id !== userToUpdate.id) { - throw new ServerError('You are not permitted to edit this user', 403); + throw new ServerError('You are not permitted to modify this user', 403); } await next(); @@ -88,6 +90,24 @@ const editUser = async ( }); }; +const deleteUser = async ( + req: UserRouteRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + const deletedUser = await deleteUserById(id); + + if (!deletedUser) { + throw new ServerError('Could not find a user with that id.', 400); + } + + res.send({ + message: 'Successfully deleted user.', + statusCode: 200, + success: true, + }); +}; + const router = createRouter< EditUserRequest, NextApiResponse> @@ -103,6 +123,15 @@ router.put( editUser, ); +router.delete( + getCurrentUser, + validateRequest({ + querySchema: z.object({ id: z.string().cuid() }), + }), + checkIfUserCanEditUser, + deleteUser, +); + const handler = router.handler(NextConnectOptions); export default handler; diff --git a/src/pages/api/users/edit-password.ts b/src/pages/api/users/edit-password.ts index 5f29441..d62f21c 100644 --- a/src/pages/api/users/edit-password.ts +++ b/src/pages/api/users/edit-password.ts @@ -5,7 +5,6 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import DBClient from '@/prisma/DBClient'; import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; @@ -23,26 +22,15 @@ const updatePassword = async ( const hash = await hashPassword(password); const user = req.user!; - const updatedUser: z.infer = await DBClient.instance.user.update({ + await DBClient.instance.user.update({ data: { hash }, where: { id: user.id }, - select: { - id: true, - username: true, - createdAt: true, - updatedAt: true, - email: true, - firstName: true, - lastName: true, - dateOfBirth: true, - accountIsVerified: true, - }, }); + res.json({ message: 'Updated user password.', statusCode: 200, success: true, - payload: updatedUser, }); }; const router = createRouter< diff --git a/src/requests/User/sendEditUserRequest.ts b/src/requests/User/sendEditUserRequest.ts index 0bff563..17eb453 100644 --- a/src/requests/User/sendEditUserRequest.ts +++ b/src/requests/User/sendEditUserRequest.ts @@ -13,7 +13,7 @@ interface SendEditUserRequestArgs { } const sendEditUserRequest = async ({ user, data }: SendEditUserRequestArgs) => { - const response = await fetch(`/api/users/${user!.id}/edit`, { + const response = await fetch(`/api/users/${user!.id}`, { body: JSON.stringify(data), method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/src/requests/User/sendUpdatePasswordRequest.ts b/src/requests/User/sendUpdatePasswordRequest.ts index 1c17ef6..01b55c3 100644 --- a/src/requests/User/sendUpdatePasswordRequest.ts +++ b/src/requests/User/sendUpdatePasswordRequest.ts @@ -1,5 +1,4 @@ import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; @@ -21,13 +20,7 @@ const sendUpdatePasswordRequest = async (data: z.infer { + const deletedUser: z.infer | null = + await DBClient.instance.user.delete({ + where: { id }, + select: { + id: true, + username: true, + email: true, + firstName: true, + lastName: true, + dateOfBirth: true, + createdAt: true, + accountIsVerified: true, + updatedAt: true, + }, + }); + + return deletedUser; +}; + +export default deleteUserById;