From bffa28b93dc8b8c1c2787b4e112853ccc8a4ebbc Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 17 Dec 2023 13:39:50 -0500 Subject: [PATCH] Update user auth services --- public/robots.txt | 1 - src/config/auth/localStrat.ts | 4 +- .../nextConnect/middleware/getCurrentUser.ts | 5 +- .../likes/brewery-post-likes/index.ts | 12 +- src/controllers/users/auth/index.ts | 104 +++--- src/controllers/users/profile/index.ts | 107 +++--- src/emails/ForgotEmail.tsx | 6 +- src/emails/{Welcome.tsx => WelcomeEmail.tsx} | 6 +- .../user-follows/useGetUsersFollowedByUser.ts | 2 +- .../user-follows/useGetUsersFollowingUser.ts | 2 +- src/pages/users/[id].tsx | 5 +- src/pages/users/reset-password.tsx | 6 +- .../account/UpdateUserAvatarByIdParams.ts | 55 ---- src/services/users/auth/createNewUser.ts | 44 --- src/services/users/auth/deleteUserById.ts | 28 -- src/services/users/auth/findUserByEmail.ts | 13 - src/services/users/auth/findUserById.ts | 37 --- src/services/users/auth/findUserByIdPublic.ts | 23 -- src/services/users/auth/findUserByUsername.ts | 13 - src/services/users/auth/index.ts | 307 ++++++++++++++++++ .../users/auth/schema/PublicUserSchema.ts | 11 - .../users/auth/sendConfirmationEmail.ts | 29 -- .../users/auth/sendResetPasswordEmail.ts | 30 -- src/services/users/auth/types/index.ts | 47 +++ .../users/auth/updateUserProfileById.ts | 33 -- .../users/auth/updateUserToBeConfirmedById.ts | 37 --- .../users/follows/getUsersFollowedByUser.ts | 27 -- .../users/follows/getUsersFollowingUser.ts | 27 -- src/services/users/profile/index.ts | 193 +++++++++++ .../schema/FollowInfoSchema.ts | 0 src/services/users/profile/types/index.ts | 38 +++ 31 files changed, 700 insertions(+), 552 deletions(-) rename src/emails/{Welcome.tsx => WelcomeEmail.tsx} (91%) delete mode 100644 src/services/users/account/UpdateUserAvatarByIdParams.ts delete mode 100644 src/services/users/auth/createNewUser.ts delete mode 100644 src/services/users/auth/deleteUserById.ts delete mode 100644 src/services/users/auth/findUserByEmail.ts delete mode 100644 src/services/users/auth/findUserById.ts delete mode 100644 src/services/users/auth/findUserByIdPublic.ts delete mode 100644 src/services/users/auth/findUserByUsername.ts create mode 100644 src/services/users/auth/index.ts delete mode 100644 src/services/users/auth/schema/PublicUserSchema.ts delete mode 100644 src/services/users/auth/sendConfirmationEmail.ts delete mode 100644 src/services/users/auth/sendResetPasswordEmail.ts create mode 100644 src/services/users/auth/types/index.ts delete mode 100644 src/services/users/auth/updateUserProfileById.ts delete mode 100644 src/services/users/auth/updateUserToBeConfirmedById.ts delete mode 100644 src/services/users/follows/getUsersFollowedByUser.ts delete mode 100644 src/services/users/follows/getUsersFollowingUser.ts create mode 100644 src/services/users/profile/index.ts rename src/services/users/{follows => profile}/schema/FollowInfoSchema.ts (100%) create mode 100644 src/services/users/profile/types/index.ts diff --git a/public/robots.txt b/public/robots.txt index e80fa28..064ec2f 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -3,4 +3,3 @@ Disallow: /api/ Disallow: /login/ Disallow: /register/ Disallow: /users/ -Disallow: /account/ diff --git a/src/config/auth/localStrat.ts b/src/config/auth/localStrat.ts index 614bd45..84df138 100644 --- a/src/config/auth/localStrat.ts +++ b/src/config/auth/localStrat.ts @@ -1,11 +1,11 @@ -import findUserByUsername from '@/services/users/auth/findUserByUsername'; import Local from 'passport-local'; +import { findUserByUsername } from '@/services/users/auth'; import ServerError from '../util/ServerError'; import { validatePassword } from './passwordFns'; const localStrat = new Local.Strategy(async (username, password, done) => { try { - const user = await findUserByUsername(username); + const user = await findUserByUsername({ username }); if (!user) { throw new ServerError('Username or password is incorrect.', 401); } diff --git a/src/config/nextConnect/middleware/getCurrentUser.ts b/src/config/nextConnect/middleware/getCurrentUser.ts index 24f5f43..e907a27 100644 --- a/src/config/nextConnect/middleware/getCurrentUser.ts +++ b/src/config/nextConnect/middleware/getCurrentUser.ts @@ -1,9 +1,10 @@ import { NextApiResponse } from 'next'; import { NextHandler } from 'next-connect'; -import findUserById from '@/services/users/auth/findUserById'; + import ServerError from '@/config/util/ServerError'; import { getLoginSession } from '../../auth/session'; import { UserExtendedNextApiRequest } from '../../auth/types'; +import { findUserById } from '@/services/users/auth'; /** Get the current user from the session. Adds the user to the request object. */ const getCurrentUser = async ( @@ -12,7 +13,7 @@ const getCurrentUser = async ( next: NextHandler, ) => { const session = await getLoginSession(req); - const user = await findUserById(session?.id); + const user = await findUserById({ userId: session?.id }); if (!user) { throw new ServerError('User is not logged in.', 401); diff --git a/src/controllers/likes/brewery-post-likes/index.ts b/src/controllers/likes/brewery-post-likes/index.ts index 9839030..6eeb790 100644 --- a/src/controllers/likes/brewery-post-likes/index.ts +++ b/src/controllers/likes/brewery-post-likes/index.ts @@ -1,12 +1,13 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; import ServerError from '@/config/util/ServerError'; -import DBClient from '@/prisma/DBClient'; + import { createBreweryPostLikeService, findBreweryPostLikeService, getBreweryPostLikeCountService, removeBreweryPostLikeService, } from '@/services/likes/brewery-post-like'; +import { getBreweryPostByIdService } from '@/services/posts/brewery-post'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse, NextApiRequest } from 'next'; import { z } from 'zod'; @@ -18,9 +19,7 @@ export const sendBreweryPostLikeRequest = async ( const id = req.query.id! as string; const user = req.user!; - const breweryPost = await DBClient.instance.breweryPost.findUnique({ - where: { id }, - }); + const breweryPost = await getBreweryPostByIdService({ breweryPostId: id }); if (!breweryPost) { throw new ServerError('Could not find a brewery post with that id', 404); @@ -59,10 +58,7 @@ export const getBreweryPostLikeCount = async ( ) => { const id = req.query.id! as string; - const breweryPost = await DBClient.instance.breweryPost.findUnique({ - where: { id }, - }); - + const breweryPost = await getBreweryPostByIdService({ breweryPostId: id }); if (!breweryPost) { throw new ServerError('Could not find a brewery post with that id', 404); } diff --git a/src/controllers/users/auth/index.ts b/src/controllers/users/auth/index.ts index d012315..4477f9e 100644 --- a/src/controllers/users/auth/index.ts +++ b/src/controllers/users/auth/index.ts @@ -3,24 +3,33 @@ import localStrat from '@/config/auth/localStrat'; import { getLoginSession, setLoginSession } from '@/config/auth/session'; import { UserExtendedNextApiRequest } from '@/config/auth/types'; import ServerError from '@/config/util/ServerError'; -import createNewUser from '@/services/users/auth/createNewUser'; -import findUserByEmail from '@/services/users/auth/findUserByEmail'; + import { NextApiRequest, NextApiResponse } from 'next'; import { expressWrapper } from 'next-connect'; import passport from 'passport'; import { z } from 'zod'; -import findUserByUsername from '@/services/users/auth/findUserByUsername'; import GetUserSchema from '@/services/users/auth/schema/GetUserSchema'; -import sendConfirmationEmail from '@/services/users/auth/sendConfirmationEmail'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import type { NextFunction } from 'express'; import { verifyConfirmationToken } from '@/config/jwt'; -import updateUserToBeConfirmedById from '@/services/users/auth/updateUserToBeConfirmedById'; -import DBClient from '@/prisma/DBClient'; -import sendResetPasswordEmail from '@/services/users/auth/sendResetPasswordEmail'; + import { hashPassword } from '@/config/auth/passwordFns'; -import deleteUserById from '@/services/users/auth/deleteUserById'; + +import { + createNewUser, + deleteUserById, + findUserByEmail, + findUserByUsername, + sendConfirmationEmail, + sendResetPasswordEmail, + updateUserById, + updateUserPassword, + updateUserToBeConfirmedById, +} from '@/services/users/auth'; + +import { EditUserRequest, UserRouteRequest } from '@/controllers/users/profile/types'; import { CheckEmailRequest, CheckUsernameRequest, @@ -29,7 +38,6 @@ import { TokenValidationRequest, UpdatePasswordRequest, } from './types'; -import { EditUserRequest, UserRouteRequest } from '../profile/types'; export const authenticateUser = expressWrapper( async ( @@ -88,10 +96,12 @@ export const registerUser = async ( req: RegisterUserRequest, res: NextApiResponse>, ) => { - const [usernameTaken, emailTaken] = await Promise.all([ - findUserByUsername(req.body.username), - findUserByEmail(req.body.email), - ]); + const [usernameTaken, emailTaken] = ( + await Promise.all([ + findUserByUsername({ username: req.body.username }), + findUserByEmail({ email: req.body.email }), + ]) + ).map((user) => !!user); if (usernameTaken) { throw new ServerError( @@ -114,7 +124,11 @@ export const registerUser = async ( username: user.username, }); - await sendConfirmationEmail(user); + await sendConfirmationEmail({ + email: user.email, + username: user.username, + userId: user.id, + }); res.status(201).json({ success: true, @@ -141,7 +155,7 @@ export const confirmUser = async ( throw new ServerError('Could not confirm user.', 401); } - await updateUserToBeConfirmedById(id); + await updateUserToBeConfirmedById({ userId: id }); res.status(200).json({ message: 'User confirmed successfully.', @@ -156,12 +170,14 @@ export const resetPassword = async ( ) => { const { email } = req.body; - const user = await DBClient.instance.user.findUnique({ - where: { email }, - }); + const user = await findUserByEmail({ email }); if (user) { - await sendResetPasswordEmail(user); + await sendResetPasswordEmail({ + email: user.email, + username: user.username, + userId: user.id, + }); } res.status(200).json({ @@ -188,7 +204,7 @@ export const sendCurrentUser = async ( export const checkEmail = async (req: CheckEmailRequest, res: NextApiResponse) => { const { email: emailToCheck } = req.query; - const email = await findUserByEmail(emailToCheck); + const email = await findUserByEmail({ email: emailToCheck }); res.json({ success: true, @@ -201,7 +217,7 @@ export const checkEmail = async (req: CheckEmailRequest, res: NextApiResponse) = export const checkUsername = async (req: CheckUsernameRequest, res: NextApiResponse) => { const { username: usernameToCheck } = req.query; - const username = await findUserByUsername(usernameToCheck); + const username = await findUserByUsername({ username: usernameToCheck }); res.json({ success: true, @@ -215,14 +231,10 @@ export const updatePassword = async ( req: UpdatePasswordRequest, res: NextApiResponse>, ) => { - const { password } = req.body; - const hash = await hashPassword(password); - const user = req.user!; - await DBClient.instance.user.update({ - data: { hash }, - where: { id: user.id }, - }); + const { password } = req.body; + + await updateUserPassword({ userId: user.id, password: await hashPassword(password) }); res.json({ message: 'Updated user password.', @@ -237,7 +249,11 @@ export const resendConfirmation = async ( ) => { const user = req.user!; - await sendConfirmationEmail(user); + await sendConfirmationEmail({ + userId: user.id, + username: user.username, + email: user.email, + }); res.status(200).json({ message: `Resent the confirmation email for ${user.username}.`, statusCode: 200, @@ -251,31 +267,9 @@ export const editUserInfo = async ( ) => { const { email, firstName, lastName, username } = req.body; - const [usernameIsTaken, emailIsTaken] = await Promise.all([ - findUserByUsername(username), - findUserByEmail(email), - ]); - - const emailChanged = req.user!.email !== email; - const usernameChanged = req.user!.username !== username; - - if (emailIsTaken && emailChanged) { - throw new ServerError('Email is already taken', 400); - } - - if (usernameIsTaken && usernameChanged) { - throw new ServerError('Username is already taken', 400); - } - - const updatedUser = await DBClient.instance.user.update({ - where: { id: req.user!.id }, - data: { - email, - firstName, - lastName, - username, - accountIsVerified: emailChanged ? false : undefined, - }, + const updatedUser = await updateUserById({ + userId: req.user!.id, + data: { email, firstName, lastName, username }, }); res.json({ @@ -291,7 +285,7 @@ export const deleteAccount = async ( res: NextApiResponse>, ) => { const { id } = req.query; - const deletedUser = await deleteUserById(id); + const deletedUser = await deleteUserById({ userId: id }); if (!deletedUser) { throw new ServerError('Could not find a user with that id.', 400); diff --git a/src/controllers/users/profile/index.ts b/src/controllers/users/profile/index.ts index de93f05..357a26e 100644 --- a/src/controllers/users/profile/index.ts +++ b/src/controllers/users/profile/index.ts @@ -1,17 +1,23 @@ import ServerError from '@/config/util/ServerError'; -import DBClient from '@/prisma/DBClient'; -import findUserById from '@/services/users/auth/findUserById'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { z } from 'zod'; import { NextHandler } from 'next-connect'; -import updateUserAvatarById, { - UpdateUserAvatarByIdParams, -} from '@/services/users/account/UpdateUserAvatarByIdParams'; + import { UserExtendedNextApiRequest } from '@/config/auth/types'; -import updateUserProfileById from '@/services/users/auth/updateUserProfileById'; -import getUsersFollowingUser from '@/services/users/follows/getUsersFollowingUser'; -import getUsersFollowedByUser from '@/services/users/follows/getUsersFollowedByUser'; + +import { findUserById } from '@/services/users/auth'; + +import { + createUserFollow, + deleteUserFollow, + findUserFollow, + getUsersFollowedByUser, + getUsersFollowingUser, + updateUserAvatar, + updateUserProfileById, +} from '@/services/users/profile'; import { UserRouteRequest, GetUserFollowInfoRequest, @@ -26,24 +32,19 @@ export const followUser = async ( ) => { const { id } = req.query; - const user = await findUserById(id); + const user = await findUserById({ userId: id }); if (!user) { throw new ServerError('User not found', 404); } const currentUser = req.user!; - const userIsFollowedBySessionUser = await DBClient.instance.userFollow.findFirst({ - where: { - followerId: currentUser.id, - followingId: id, - }, + const userIsFollowedBySessionUser = await findUserFollow({ + followerId: currentUser.id, + followingId: id, }); if (!userIsFollowedBySessionUser) { - await DBClient.instance.userFollow.create({ - data: { followerId: currentUser.id, followingId: id }, - }); - + await createUserFollow({ followerId: currentUser.id, followingId: id }); res.status(200).json({ message: 'Now following user.', success: true, @@ -53,14 +54,7 @@ export const followUser = async ( return; } - await DBClient.instance.userFollow.delete({ - where: { - followerId_followingId: { - followerId: currentUser.id, - followingId: id, - }, - }, - }); + await deleteUserFollow({ followerId: currentUser.id, followingId: id }); res.status(200).json({ message: 'No longer following user.', @@ -76,7 +70,7 @@ export const getUserFollowers = async ( // eslint-disable-next-line @typescript-eslint/naming-convention const { id, page_num, page_size } = req.query; - const user = await findUserById(id); + const user = await findUserById({ userId: id }); if (!user) { throw new ServerError('User not found', 404); } @@ -84,20 +78,17 @@ export const getUserFollowers = async ( const pageNum = parseInt(page_num, 10); const pageSize = parseInt(page_size, 10); - const following = await getUsersFollowingUser({ + const { follows, count } = await getUsersFollowingUser({ userId: id, pageNum, pageSize, }); - const followingCount = await DBClient.instance.userFollow.count({ - where: { following: { id } }, - }); - res.setHeader('X-Total-Count', followingCount); + res.setHeader('X-Total-Count', count); res.json({ message: 'Retrieved users that are followed by queried user', - payload: following, + payload: follows, success: true, statusCode: 200, }); @@ -110,7 +101,7 @@ export const getUsersFollowed = async ( // eslint-disable-next-line @typescript-eslint/naming-convention const { id, page_num, page_size } = req.query; - const user = await findUserById(id); + const user = await findUserById({ userId: id }); if (!user) { throw new ServerError('User not found', 404); } @@ -118,20 +109,17 @@ export const getUsersFollowed = async ( const pageNum = parseInt(page_num, 10); const pageSize = parseInt(page_size, 10); - const following = await getUsersFollowedByUser({ + const { follows, count } = await getUsersFollowedByUser({ userId: id, pageNum, pageSize, }); - const followingCount = await DBClient.instance.userFollow.count({ - where: { follower: { id } }, - }); - res.setHeader('X-Total-Count', followingCount); + res.setHeader('X-Total-Count', count); res.json({ message: 'Retrieved users that are followed by queried user', - payload: following, + payload: follows, success: true, statusCode: 200, }); @@ -143,33 +131,27 @@ export const checkIfUserIsFollowedBySessionUser = async ( ) => { const { id } = req.query; - const user = await findUserById(id); + const user = await findUserById({ userId: id }); if (!user) { throw new ServerError('User not found', 404); } const currentUser = req.user!; - const userIsFollowedBySessionUser = await DBClient.instance.userFollow.findFirst({ - where: { followerId: currentUser.id, followingId: id }, + const userFollow = await findUserFollow({ + followerId: currentUser.id, + followingId: id, }); - if (!userIsFollowedBySessionUser) { - res.status(200).json({ - message: 'User is not followed by the current user.', - success: true, - statusCode: 200, - payload: { isFollowed: false }, - }); - - return; - } + const isFollowed = !!userFollow; res.status(200).json({ - message: 'User is followed by the current user.', + message: isFollowed + ? 'User is followed by the session user.' + : 'User is not followed by the session user.', success: true, statusCode: 200, - payload: { isFollowed: true }, + payload: { isFollowed }, }); }; @@ -180,7 +162,7 @@ export const checkIfUserCanEditUser = async ( ) => { const authenticatedUser = req.user!; - const userToUpdate = await findUserById(req.query.id); + const userToUpdate = await findUserById({ userId: req.query.id }); if (!userToUpdate) { throw new ServerError('User not found', 404); } @@ -209,13 +191,10 @@ export const checkIfUserCanUpdateProfile = async { const { file, user } = req; - const avatar: UpdateUserAvatarByIdParams['data']['avatar'] = { - alt: file.originalname, - path: file.path, - caption: '', - }; - - await updateUserAvatarById({ id: user!.id, data: { avatar } }); + await updateUserAvatar({ + userId: user!.id, + data: { alt: file.originalname, path: file.path, caption: '' }, + }); res.status(200).json({ message: 'User avatar updated successfully.', statusCode: 200, @@ -227,7 +206,7 @@ export const updateProfile = async (req: UpdateProfileRequest, res: NextApiRespo const user = req.user!; const { body } = req; - await updateUserProfileById({ id: user!.id, data: { bio: body.bio } }); + await updateUserProfileById({ userId: user!.id, data: { bio: body.bio } }); res.status(200).json({ message: 'Profile updated successfully.', diff --git a/src/emails/ForgotEmail.tsx b/src/emails/ForgotEmail.tsx index 5819674..8a2bad7 100644 --- a/src/emails/ForgotEmail.tsx +++ b/src/emails/ForgotEmail.tsx @@ -3,12 +3,12 @@ import { Tailwind } from '@react-email/tailwind'; import { FC } from 'react'; -interface ForgotEmailProps { +interface ResetPasswordEmailProps { name?: string; url?: string; } -const ForgotEmail: FC = ({ name, url }) => { +const ResetPasswordEmail: FC = ({ name, url }) => { return ( @@ -36,4 +36,4 @@ const ForgotEmail: FC = ({ name, url }) => { ); }; -export default ForgotEmail; +export default ResetPasswordEmail; diff --git a/src/emails/Welcome.tsx b/src/emails/WelcomeEmail.tsx similarity index 91% rename from src/emails/Welcome.tsx rename to src/emails/WelcomeEmail.tsx index 9380079..92e9513 100644 --- a/src/emails/Welcome.tsx +++ b/src/emails/WelcomeEmail.tsx @@ -3,13 +3,13 @@ import { Tailwind } from '@react-email/tailwind'; import { FC } from 'react'; -interface WelcomeEmail { +interface WelcomeEmailProps { subject?: string; name?: string; url?: string; } -const Welcome: FC = ({ name, url }) => ( +const WelcomeEmail: FC = ({ name, url }) => (
@@ -43,4 +43,4 @@ const Welcome: FC = ({ name, url }) => ( ); -export default Welcome; +export default WelcomeEmail; diff --git a/src/hooks/data-fetching/user-follows/useGetUsersFollowedByUser.ts b/src/hooks/data-fetching/user-follows/useGetUsersFollowedByUser.ts index 85cbbc9..be56aae 100644 --- a/src/hooks/data-fetching/user-follows/useGetUsersFollowedByUser.ts +++ b/src/hooks/data-fetching/user-follows/useGetUsersFollowedByUser.ts @@ -17,7 +17,7 @@ * - `mutate` A function to mutate the data. * - `error` The error object, if any. */ -import FollowInfoSchema from '@/services/users/follows/schema/FollowInfoSchema'; +import FollowInfoSchema from '@/services/users/profile/schema/FollowInfoSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import useSWRInfinite from 'swr/infinite'; import { z } from 'zod'; diff --git a/src/hooks/data-fetching/user-follows/useGetUsersFollowingUser.ts b/src/hooks/data-fetching/user-follows/useGetUsersFollowingUser.ts index b1e7fb9..7555995 100644 --- a/src/hooks/data-fetching/user-follows/useGetUsersFollowingUser.ts +++ b/src/hooks/data-fetching/user-follows/useGetUsersFollowingUser.ts @@ -1,4 +1,4 @@ -import FollowInfoSchema from '@/services/users/follows/schema/FollowInfoSchema'; +import FollowInfoSchema from '@/services/users/profile/schema/FollowInfoSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import useSWRInfinite from 'swr/infinite'; import { z } from 'zod'; diff --git a/src/pages/users/[id].tsx b/src/pages/users/[id].tsx index fa48c0f..dfa99aa 100644 --- a/src/pages/users/[id].tsx +++ b/src/pages/users/[id].tsx @@ -1,5 +1,5 @@ import useMediaQuery from '@/hooks/utilities/useMediaQuery'; -import findUserById from '@/services/users/auth/findUserById'; + import GetUserSchema from '@/services/users/auth/schema/GetUserSchema'; import Head from 'next/head'; @@ -7,6 +7,7 @@ import { FC } from 'react'; import { z } from 'zod'; import withPageAuthRequired from '@/util/withPageAuthRequired'; import UserHeader from '@/components/UserPage/UserHeader'; +import { findUserById } from '@/services/users/auth'; interface UserInfoPageProps { user: z.infer; @@ -39,7 +40,7 @@ export default UserInfoPage; export const getServerSideProps = withPageAuthRequired( async (context) => { const { id } = context.params!; - const user = await findUserById(id as string); + const user = await findUserById({ userId: id as string }); return user ? { props: { user: JSON.parse(JSON.stringify(user)) } } : { notFound: true }; diff --git a/src/pages/users/reset-password.tsx b/src/pages/users/reset-password.tsx index 0c426ba..78dea22 100644 --- a/src/pages/users/reset-password.tsx +++ b/src/pages/users/reset-password.tsx @@ -1,7 +1,7 @@ import { setLoginSession } from '@/config/auth/session'; import { verifyResetPasswordToken } from '@/config/jwt'; import ServerError from '@/config/util/ServerError'; -import findUserById from '@/services/users/auth/findUserById'; +import { findUserById } from '@/services/users/auth'; import { GetServerSideProps, NextApiResponse, NextPage } from 'next'; @@ -29,14 +29,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = await verifyResetPasswordToken(token as string); - const user = await findUserById(id); + const user = await findUserById({ userId: id as string }); if (!user) { throw new ServerError('User not found', 404); } await setLoginSession(context.res as NextApiResponse, user); - return { redirect: { destination: '/account', permanent: false } }; + return { redirect: { destination: '/users/account', permanent: false } }; } catch (error) { return { props: {} }; } diff --git a/src/services/users/account/UpdateUserAvatarByIdParams.ts b/src/services/users/account/UpdateUserAvatarByIdParams.ts deleted file mode 100644 index 56ab286..0000000 --- a/src/services/users/account/UpdateUserAvatarByIdParams.ts +++ /dev/null @@ -1,55 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import GetUserSchema from '@/services/users/auth/schema/GetUserSchema'; -import { z } from 'zod'; - -export interface UpdateUserAvatarByIdParams { - id: string; - data: { - avatar: { - alt: string; - path: string; - caption: string; - }; - }; -} -const updateUserAvatarById = async ({ id, data }: UpdateUserAvatarByIdParams) => { - const user: z.infer = await DBClient.instance.user.update({ - where: { id }, - data: { - userAvatar: data.avatar - ? { - upsert: { - create: { - alt: data.avatar.alt, - path: data.avatar.path, - caption: data.avatar.caption, - }, - update: { - alt: data.avatar.alt, - path: data.avatar.path, - caption: data.avatar.caption, - }, - }, - } - : undefined, - }, - select: { - id: true, - username: true, - email: true, - bio: true, - userAvatar: true, - accountIsVerified: true, - createdAt: true, - firstName: true, - lastName: true, - updatedAt: true, - dateOfBirth: true, - role: true, - }, - }); - - return user; -}; - -export default updateUserAvatarById; diff --git a/src/services/users/auth/createNewUser.ts b/src/services/users/auth/createNewUser.ts deleted file mode 100644 index b34c61a..0000000 --- a/src/services/users/auth/createNewUser.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { hashPassword } from '@/config/auth/passwordFns'; -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import { CreateUserValidationSchema } from './schema/CreateUserValidationSchemas'; -import GetUserSchema from './schema/GetUserSchema'; - -const createNewUser = async ({ - email, - password, - firstName, - lastName, - dateOfBirth, - username, -}: z.infer) => { - const hash = await hashPassword(password); - const user: z.infer = await DBClient.instance.user.create({ - data: { - username, - email, - hash, - firstName, - lastName, - dateOfBirth: new Date(dateOfBirth), - }, - select: { - id: true, - username: true, - email: true, - firstName: true, - lastName: true, - dateOfBirth: true, - createdAt: true, - accountIsVerified: true, - updatedAt: true, - role: true, - userAvatar: true, - bio: true, - }, - }); - - return user; -}; - -export default createNewUser; diff --git a/src/services/users/auth/deleteUserById.ts b/src/services/users/auth/deleteUserById.ts deleted file mode 100644 index dd62693..0000000 --- a/src/services/users/auth/deleteUserById.ts +++ /dev/null @@ -1,28 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import GetUserSchema from './schema/GetUserSchema'; - -const deleteUserById = async (id: string) => { - 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, - role: true, - userAvatar: true, - bio: true, - }, - }); - - return deletedUser; -}; - -export default deleteUserById; diff --git a/src/services/users/auth/findUserByEmail.ts b/src/services/users/auth/findUserByEmail.ts deleted file mode 100644 index 6e505e1..0000000 --- a/src/services/users/auth/findUserByEmail.ts +++ /dev/null @@ -1,13 +0,0 @@ -import DBClient from '../../../prisma/DBClient'; - -const findUserByEmail = async (email: string) => - DBClient.instance.user.findFirst({ - where: { email }, - select: { - id: true, - username: true, - hash: true, - }, - }); - -export default findUserByEmail; diff --git a/src/services/users/auth/findUserById.ts b/src/services/users/auth/findUserById.ts deleted file mode 100644 index 05b1182..0000000 --- a/src/services/users/auth/findUserById.ts +++ /dev/null @@ -1,37 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import GetUserSchema from './schema/GetUserSchema'; - -const findUserById = async (id: string) => { - const user: z.infer | null = - await DBClient.instance.user.findUnique({ - where: { id }, - select: { - id: true, - username: true, - email: true, - firstName: true, - lastName: true, - dateOfBirth: true, - createdAt: true, - accountIsVerified: true, - updatedAt: true, - role: true, - userAvatar: { - select: { - path: true, - alt: true, - caption: true, - createdAt: true, - id: true, - updatedAt: true, - }, - }, - bio: true, - }, - }); - - return user; -}; - -export default findUserById; diff --git a/src/services/users/auth/findUserByIdPublic.ts b/src/services/users/auth/findUserByIdPublic.ts deleted file mode 100644 index f7509d5..0000000 --- a/src/services/users/auth/findUserByIdPublic.ts +++ /dev/null @@ -1,23 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; - -import PublicUserSchema from './schema/PublicUserSchema'; - -const findUserByIdPublic = async (id: string) => { - const user: z.infer | null = - await DBClient.instance.user.findUnique({ - where: { id }, - select: { - id: true, - username: true, - firstName: true, - lastName: true, - createdAt: true, - role: true, - }, - }); - - return user; -}; - -export default findUserByIdPublic; diff --git a/src/services/users/auth/findUserByUsername.ts b/src/services/users/auth/findUserByUsername.ts deleted file mode 100644 index b4a57ca..0000000 --- a/src/services/users/auth/findUserByUsername.ts +++ /dev/null @@ -1,13 +0,0 @@ -import DBClient from '../../../prisma/DBClient'; - -const findUserByUsername = async (username: string) => - DBClient.instance.user.findFirst({ - where: { username }, - select: { - id: true, - username: true, - hash: true, - }, - }); - -export default findUserByUsername; diff --git a/src/services/users/auth/index.ts b/src/services/users/auth/index.ts new file mode 100644 index 0000000..050f2a6 --- /dev/null +++ b/src/services/users/auth/index.ts @@ -0,0 +1,307 @@ +/* eslint-disable import/prefer-default-export */ +import { hashPassword } from '@/config/auth/passwordFns'; +import DBClient from '@/prisma/DBClient'; +import { BASE_URL } from '@/config/env'; +import { generateConfirmationToken, generateResetPasswordToken } from '@/config/jwt'; +import sendEmail from '@/config/sparkpost/sendEmail'; + +import { ReactElement } from 'react'; +import ServerError from '@/config/util/ServerError'; +import { render } from '@react-email/render'; +import WelcomeEmail from '@/emails/WelcomeEmail'; +import ResetPasswordEmail from '@/emails/ForgotEmail'; + +import { + CreateNewUser, + DeleteUserById, + FindUserByEmail, + FindUserByUsername, + FindUserById, + SendConfirmationEmail, + SendResetPasswordEmail, + UpdateUserToBeConfirmedById, + UpdateUserPassword, + UpdateUserById, +} from './types'; + +/** + * The select object for retrieving users. + * + * Satisfies the GetUserSchema zod schema. + * + * @example + * const users = await DBClient.instance.user.findMany({ + * select: userSelect, + * }); + */ +const userSelect = { + id: true, + username: true, + email: true, + firstName: true, + lastName: true, + dateOfBirth: true, + createdAt: true, + accountIsVerified: true, + updatedAt: true, + role: true, + userAvatar: true, + bio: true, +} as const; + +/** + * The select object for retrieving users without sensitive information. + * + * @example + * const user = await DBClient.instance.user.findUnique({ + * where: { id: userId }, + * select: AuthUserSelect, + * }); + */ +const authUserSelect = { + id: true, + username: true, + hash: true, +} as const; + +/** + * Creates a new user. + * + * @param args The arguments for service. + * @param args.email The email of the user to create. + * @param args.password The password of the user to create. + * @param args.firstName The first name of the user to create. + * @param args.lastName The last name of the user to create. + * @param args.dateOfBirth The date of birth of the user to create. + * @param args.username The username of the user to create. + * @returns The user. + */ +export const createNewUser: CreateNewUser = async ({ + email, + password, + firstName, + lastName, + dateOfBirth, + username, +}) => { + const hash = await hashPassword(password); + + const user = await DBClient.instance.user.create({ + data: { + username, + email, + hash, + firstName, + lastName, + dateOfBirth: new Date(dateOfBirth), + }, + select: userSelect, + }); + + return user; +}; + +/** + * Deletes a user by id. + * + * @param args The arguments for service. + * @param args.userId The id of the user to delete. + * @returns The user that was deleted if found, otherwise null. + */ +export const deleteUserById: DeleteUserById = ({ userId }) => { + return DBClient.instance.user.delete({ where: { id: userId }, select: authUserSelect }); +}; + +/** + * Finds a user by username. + * + * @param args The arguments for service. + * @param args.username The username of the user to find. + * @returns The user if found, otherwise null. + */ + +export const findUserByUsername: FindUserByUsername = async ({ username }) => { + return DBClient.instance.user.findUnique({ + where: { username }, + select: authUserSelect, + }); +}; + +/** + * Finds a user by email. + * + * @param args The arguments for service. + * @param args.email The email of the user to find. + */ +export const findUserByEmail: FindUserByEmail = async ({ email }) => { + return DBClient.instance.user.findUnique({ where: { email }, select: userSelect }); +}; + +/** + * Finds a user by id. + * + * @param args The arguments for service. + * @param args.userId The id of the user to find. + * @returns The user if found, otherwise null. + */ +export const findUserById: FindUserById = ({ userId }) => { + return DBClient.instance.user.findUnique({ where: { id: userId }, select: userSelect }); +}; + +/** + * Sends a confirmation email to the user using React Email and SparkPost. + * + * @param args The arguments for service. + * @param args.userId The id of the user to send the confirmation email to. + * @param args.username The username of the user to send the confirmation email to. + * @param args.email The email of the user to send the confirmation email to. + * @returns The user if found, otherwise null. + */ +export const sendConfirmationEmail: SendConfirmationEmail = async ({ + userId, + username, + email, +}) => { + const confirmationToken = generateConfirmationToken({ id: userId, username }); + const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`; + + const name = username; + const address = email; + const subject = 'Confirm your email'; + + const component = WelcomeEmail({ name, url, subject })! as ReactElement< + unknown, + string + >; + + const html = render(component); + const text = render(component, { plainText: true }); + + await sendEmail({ address, subject, text, html }); +}; + +/** + * Sends a reset password email to the specified user. + * + * @param args The arguments for service. + * @param args.userId The id of the user to send the reset password email to. + * @param args.username The username of the user to send the reset password email to. + * @param args.email The email of the user to send the reset password email to. + * @returns A promise that resolves to void. + */ +export const sendResetPasswordEmail: SendResetPasswordEmail = async ({ + userId, + username, + email, +}) => { + const token = generateResetPasswordToken({ id: userId, username }); + + const url = `${BASE_URL}/users/reset-password?token=${token}`; + + const component = ResetPasswordEmail({ name: username, url })! as ReactElement< + unknown, + string + >; + + const html = render(component); + const text = render(component, { plainText: true }); + + await sendEmail({ + address: email, + subject: 'Reset Password', + html, + text, + }); +}; + +/** + * Updates a user to be confirmed by id. + * + * @param args The arguments for service. + * @param args.userId The id of the user to update. + * @returns The user. + */ +export const updateUserToBeConfirmedById: UpdateUserToBeConfirmedById = async ({ + userId, +}) => { + return DBClient.instance.user.update({ + where: { id: userId }, + data: { accountIsVerified: true, updatedAt: new Date() }, + select: userSelect, + }); +}; + +export const updateUserPassword: UpdateUserPassword = async ({ password, userId }) => { + const hash = await hashPassword(password); + + const user = await DBClient.instance.user.update({ + where: { id: userId }, + data: { hash, updatedAt: new Date() }, + select: authUserSelect, + }); + + return user; +}; + +/** + * Updates a user by id. + * + * @param args The arguments for service. + * @param args.userId The id of the user to update. + * @param args.data The data to update the user with. + * @param args.data.email The email of the user to update. + * @param args.data.firstName The first name of the user to update. + * @param args.data.lastName The last name of the user to update. + * @param args.data.username The username of the user to update. + */ +export const updateUserById: UpdateUserById = async ({ userId, data }) => { + const user = await DBClient.instance.user.findUnique({ + where: { id: userId }, + select: userSelect, + }); + + if (!user) { + throw new ServerError('User not found', 404); + } + + const updatedFields = { + email: data.email !== user.email, + username: data.username !== user.username, + firstName: data.firstName !== user.firstName, + lastName: data.lastName !== user.lastName, + } as const; + + if (updatedFields.email) { + const emailIsTaken = await findUserByEmail({ email: data.email }); + if (emailIsTaken) { + throw new ServerError('Email is already taken', 400); + } + + await sendConfirmationEmail({ + userId, + username: data.username, + email: data.email, + }); + } + + if (updatedFields.username) { + const usernameIsTaken = await findUserByUsername({ username: data.username }); + if (usernameIsTaken) { + throw new ServerError('Username is already taken', 400); + } + } + + const updatedUser = await DBClient.instance.user.update({ + where: { id: userId }, + data: { + email: updatedFields.email ? data.email : undefined, + username: updatedFields.username ? data.username : undefined, + firstName: updatedFields.firstName ? data.firstName : undefined, + lastName: updatedFields.lastName ? data.lastName : undefined, + accountIsVerified: updatedFields.email ? false : undefined, + }, + select: userSelect, + }); + + return updatedUser; +}; diff --git a/src/services/users/auth/schema/PublicUserSchema.ts b/src/services/users/auth/schema/PublicUserSchema.ts deleted file mode 100644 index ba51010..0000000 --- a/src/services/users/auth/schema/PublicUserSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import GetUserSchema from '@/services/users/auth/schema/GetUserSchema'; - -const PublicUserSchema = GetUserSchema.pick({ - id: true, - name: true, - createdAt: true, - username: true, - role: true, -}); - -export default PublicUserSchema; diff --git a/src/services/users/auth/sendConfirmationEmail.ts b/src/services/users/auth/sendConfirmationEmail.ts deleted file mode 100644 index 4cf6e19..0000000 --- a/src/services/users/auth/sendConfirmationEmail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { generateConfirmationToken } from '@/config/jwt'; -import sendEmail from '@/config/sparkpost/sendEmail'; - -import Welcome from '@/emails/Welcome'; -import { render } from '@react-email/render'; -import { z } from 'zod'; -import { BASE_URL } from '@/config/env'; -import { ReactElement } from 'react'; -import GetUserSchema from './schema/GetUserSchema'; - -type UserSchema = z.infer; - -const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => { - const confirmationToken = generateConfirmationToken({ id, username }); - - const subject = 'Confirm your email'; - const name = username; - const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`; - const address = email; - - const component = Welcome({ name, url, subject })! as ReactElement; - - const html = render(component); - const text = render(component, { plainText: true }); - - await sendEmail({ address, subject, text, html }); -}; - -export default sendConfirmationEmail; diff --git a/src/services/users/auth/sendResetPasswordEmail.ts b/src/services/users/auth/sendResetPasswordEmail.ts deleted file mode 100644 index a6410db..0000000 --- a/src/services/users/auth/sendResetPasswordEmail.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BASE_URL } from '@/config/env'; -import { generateResetPasswordToken } from '@/config/jwt'; -import sendEmail from '@/config/sparkpost/sendEmail'; -import ForgotEmail from '@/emails/ForgotEmail'; -import { User } from '@prisma/client'; -import type { ReactElement } from 'react'; -import { render } from '@react-email/render'; - -const sendResetPasswordEmail = async (user: User) => { - const token = generateResetPasswordToken({ id: user.id, username: user.username }); - - const url = `${BASE_URL}/users/reset-password?token=${token}`; - - const component = ForgotEmail({ name: user.username, url })! as ReactElement< - unknown, - string - >; - - const html = render(component); - const text = render(component, { plainText: true }); - - await sendEmail({ - address: user.email, - subject: 'Reset Password', - html, - text, - }); -}; - -export default sendResetPasswordEmail; diff --git a/src/services/users/auth/types/index.ts b/src/services/users/auth/types/index.ts new file mode 100644 index 0000000..6389667 --- /dev/null +++ b/src/services/users/auth/types/index.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import GetUserSchema from '../schema/GetUserSchema'; +import { CreateUserValidationSchema } from '../schema/CreateUserValidationSchemas'; + +type User = z.infer; +type AuthUser = { username: string; hash: string; id: string }; + +export type CreateNewUser = ( + args: z.infer, +) => Promise; + +export type DeleteUserById = (args: { userId: string }) => Promise; + +export type FindUserById = (args: { userId: string }) => Promise; + +export type FindUserByUsername = (args: { username: string }) => Promise; + +export type FindUserByEmail = (args: { email: string }) => Promise; + +export type UpdateUserPassword = (args: { + userId: string; + password: string; +}) => Promise; + +export type SendConfirmationEmail = (args: { + userId: string; + username: string; + email: string; +}) => Promise; + +export type SendResetPasswordEmail = (args: { + userId: string; + username: string; + email: string; +}) => Promise; + +export type UpdateUserToBeConfirmedById = (args: { userId: string }) => Promise; + +export type UpdateUserById = (args: { + userId: string; + data: { + email: string; + firstName: string; + lastName: string; + username: string; + }; +}) => Promise; diff --git a/src/services/users/auth/updateUserProfileById.ts b/src/services/users/auth/updateUserProfileById.ts deleted file mode 100644 index 204aba9..0000000 --- a/src/services/users/auth/updateUserProfileById.ts +++ /dev/null @@ -1,33 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import GetUserSchema from './schema/GetUserSchema'; - -interface UpdateUserProfileByIdParams { - id: string; - data: { bio: string }; -} - -const updateUserProfileById = async ({ id, data }: UpdateUserProfileByIdParams) => { - const user: z.infer = await DBClient.instance.user.update({ - where: { id }, - data: { bio: data.bio }, - select: { - id: true, - username: true, - email: true, - bio: true, - userAvatar: true, - accountIsVerified: true, - createdAt: true, - firstName: true, - lastName: true, - updatedAt: true, - dateOfBirth: true, - role: true, - }, - }); - - return user; -}; - -export default updateUserProfileById; diff --git a/src/services/users/auth/updateUserToBeConfirmedById.ts b/src/services/users/auth/updateUserToBeConfirmedById.ts deleted file mode 100644 index 2db875f..0000000 --- a/src/services/users/auth/updateUserToBeConfirmedById.ts +++ /dev/null @@ -1,37 +0,0 @@ -import GetUserSchema from '@/services/users/auth/schema/GetUserSchema'; -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; - -const updateUserToBeConfirmedById = async (id: string) => { - const user: z.infer = await DBClient.instance.user.update({ - where: { id }, - data: { accountIsVerified: true, updatedAt: new Date() }, - select: { - id: true, - username: true, - email: true, - accountIsVerified: true, - createdAt: true, - firstName: true, - lastName: true, - updatedAt: true, - dateOfBirth: true, - role: true, - bio: true, - userAvatar: { - select: { - id: true, - path: true, - alt: true, - caption: true, - createdAt: true, - updatedAt: true, - }, - }, - }, - }); - - return user; -}; - -export default updateUserToBeConfirmedById; diff --git a/src/services/users/follows/getUsersFollowedByUser.ts b/src/services/users/follows/getUsersFollowedByUser.ts deleted file mode 100644 index 3f58d19..0000000 --- a/src/services/users/follows/getUsersFollowedByUser.ts +++ /dev/null @@ -1,27 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import FollowInfoSchema from './schema/FollowInfoSchema'; - -interface GetFollowingInfoByUserIdArgs { - userId: string; - pageNum: number; - pageSize: number; -} -const getUsersFollowedByUser = async ({ - userId, - pageNum, - pageSize, -}: GetFollowingInfoByUserIdArgs): Promise[]> => { - const usersFollowedByQueriedUser = await DBClient.instance.userFollow.findMany({ - take: pageSize, - skip: (pageNum - 1) * pageSize, - where: { following: { id: userId } }, - select: { - follower: { select: { username: true, userAvatar: true, id: true } }, - }, - }); - - return usersFollowedByQueriedUser.map((u) => u.follower); -}; - -export default getUsersFollowedByUser; diff --git a/src/services/users/follows/getUsersFollowingUser.ts b/src/services/users/follows/getUsersFollowingUser.ts deleted file mode 100644 index 5379b7f..0000000 --- a/src/services/users/follows/getUsersFollowingUser.ts +++ /dev/null @@ -1,27 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { z } from 'zod'; -import FollowInfoSchema from './schema/FollowInfoSchema'; - -interface GetFollowingInfoByUserIdArgs { - userId: string; - pageNum: number; - pageSize: number; -} -const getUsersFollowingUser = async ({ - userId, - pageNum, - pageSize, -}: GetFollowingInfoByUserIdArgs): Promise[]> => { - const usersFollowingQueriedUser = await DBClient.instance.userFollow.findMany({ - take: pageSize, - skip: (pageNum - 1) * pageSize, - where: { follower: { id: userId } }, - select: { - following: { select: { username: true, userAvatar: true, id: true } }, - }, - }); - - return usersFollowingQueriedUser.map((u) => u.following); -}; - -export default getUsersFollowingUser; diff --git a/src/services/users/profile/index.ts b/src/services/users/profile/index.ts new file mode 100644 index 0000000..b7734c9 --- /dev/null +++ b/src/services/users/profile/index.ts @@ -0,0 +1,193 @@ +import DBClient from '@/prisma/DBClient'; +import ServerError from '@/config/util/ServerError'; +import { + GetUsersFollowedByOrFollowingUser, + UpdateUserAvatar, + UpdateUserProfileById, + UserFollowService, +} from './types'; + +/** + * The select object for retrieving users. + * + * Satisfies the GetUserSchema zod schema. + * + * @example + * const users = await DBClient.instance.user.findMany({ + * select: userSelect, + * }); + */ +const userSelect = { + id: true, + username: true, + email: true, + firstName: true, + lastName: true, + dateOfBirth: true, + createdAt: true, + accountIsVerified: true, + updatedAt: true, + role: true, + userAvatar: true, + bio: true, +} as const; + +/** + * Finds a user follow by the followerId and followingId. + * + * @returns The user follow if found, otherwise null. + */ +export const findUserFollow: UserFollowService = ({ followerId, followingId }) => { + return DBClient.instance.userFollow.findFirst({ where: { followerId, followingId } }); +}; + +/** + * Creates a new user follow. + * + * @param args The arguments for service. + * @param args.followerId The follower id of the user follow to create. + * @param args.followingId The following id of the user follow to create. + * @returns The user follow. + */ +export const createUserFollow: UserFollowService = ({ followerId, followingId }) => { + return DBClient.instance.userFollow.create({ data: { followerId, followingId } }); +}; + +/** + * Deletes a user follow. + * + * @param args The arguments for service. + * @param args.followerId The follower id of the user follow to delete. + * @param args.followingId The following id of the user follow to delete. + * @returns The user follow. + */ +export const deleteUserFollow: UserFollowService = ({ followerId, followingId }) => { + return DBClient.instance.userFollow.delete({ + where: { followerId_followingId: { followerId, followingId } }, + }); +}; + +/** + * Gets the users followed by the session user. + * + * @param args The arguments for service. + * @param args.userId The id of the user to check if followed by the session user. + * @param args.pageNum The page number of the users to retrieve. + * @param args.pageSize The page size of the users to retrieve. + * @returns The users followed by the queried user and the count of users followed by the + * queried user. + */ +export const getUsersFollowedByUser: GetUsersFollowedByOrFollowingUser = async ({ + userId, + pageNum, + pageSize, +}) => { + const usersFollowedByQueriedUser = await DBClient.instance.userFollow.findMany({ + take: pageSize, + skip: (pageNum - 1) * pageSize, + where: { follower: { id: userId } }, + select: { + follower: { select: { username: true, userAvatar: true, id: true } }, + }, + }); + const count = await DBClient.instance.userFollow.count({ + where: { follower: { id: userId } }, + }); + const follows = usersFollowedByQueriedUser.map((u) => u.follower); + + return { follows, count }; +}; + +/** + * Gets the users following the session user. + * + * @param args The arguments for service. + * @param args.userId The id of the user to check if followed by the session user. + * @param args.pageNum The page number of the users to retrieve. + * @param args.pageSize The page size of the users to retrieve. + */ +export const getUsersFollowingUser: GetUsersFollowedByOrFollowingUser = async ({ + userId, + pageNum, + pageSize, +}) => { + const usersFollowingQueriedUser = await DBClient.instance.userFollow.findMany({ + take: pageSize, + skip: (pageNum - 1) * pageSize, + where: { following: { id: userId } }, + select: { + following: { select: { username: true, userAvatar: true, id: true } }, + }, + }); + + const count = await DBClient.instance.userFollow.count({ + where: { following: { id: userId } }, + }); + + const follows = usersFollowingQueriedUser.map((u) => u.following); + return { follows, count }; +}; + +/** + * Updates the user avatar of the user. + * + * @param args The arguments for service. + * @param args.userId The id of the user to update the avatar of. + * @param args.data The data to update the user avatar with. + * @param args.data.alt The alt text of the user avatar. + * @param args.data.path The path of the user avatar. + * @param args.data.caption The caption of the user avatar. + * @returns The updated user. + */ +export const updateUserAvatar: UpdateUserAvatar = async ({ userId, data }) => { + const user = await DBClient.instance.user.findUnique({ + where: { id: userId }, + select: userSelect, + }); + + if (!user) { + throw new ServerError('User not found', 404); + } + + const updatedUser = await DBClient.instance.user.update({ + where: { id: userId }, + data: { + userAvatar: { + upsert: { + create: { + alt: data.alt, + path: data.path, + caption: data.caption, + }, + update: { + alt: data.alt, + path: data.path, + caption: data.caption, + }, + }, + }, + }, + select: userSelect, + }); + + return updatedUser; +}; + +/** + * Updates a user's profile by id. + * + * @param args The arguments for service. + * @param args.userId The id of the user to update. + * @param args.data The data to update the user with. + * @param args.data.bio The bio of the user. + * @returns The user. + */ +export const updateUserProfileById: UpdateUserProfileById = async ({ userId, data }) => { + const user = await DBClient.instance.user.update({ + where: { id: userId }, + data: { bio: data.bio }, + select: userSelect, + }); + + return user; +}; diff --git a/src/services/users/follows/schema/FollowInfoSchema.ts b/src/services/users/profile/schema/FollowInfoSchema.ts similarity index 100% rename from src/services/users/follows/schema/FollowInfoSchema.ts rename to src/services/users/profile/schema/FollowInfoSchema.ts diff --git a/src/services/users/profile/types/index.ts b/src/services/users/profile/types/index.ts new file mode 100644 index 0000000..7a3663e --- /dev/null +++ b/src/services/users/profile/types/index.ts @@ -0,0 +1,38 @@ +import { UserFollow } from '@prisma/client'; +import { z } from 'zod'; + +import FollowInfoSchema from '../schema/FollowInfoSchema'; +import GetUserSchema from '../../auth/schema/GetUserSchema'; + +type FollowInfo = z.infer; +type User = z.infer; + +export type UserFollowService = (args: { + followerId: string; + followingId: string; +}) => Promise; + +export type UpdateUserProfileById = (args: { + userId: string; + data: { bio: string }; +}) => Promise; + +export type CheckIfUserIsFollowedBySessionUser = (args: { + followerId: string; + followingId: string; +}) => Promise; + +export type GetUsersFollowedByOrFollowingUser = (args: { + userId: string; + pageNum: number; + pageSize: number; +}) => Promise<{ follows: FollowInfo[]; count: number }>; + +export type UpdateUserAvatar = (args: { + userId: string; + data: { + alt: string; + path: string; + caption: string; + }; +}) => Promise;