diff --git a/src/controllers/posts/beerPosts/index.ts b/src/controllers/posts/beerPosts/index.ts index fc1f866..600318b 100644 --- a/src/controllers/posts/beerPosts/index.ts +++ b/src/controllers/posts/beerPosts/index.ts @@ -10,6 +10,7 @@ import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations'; import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts'; import DBClient from '@/prisma/DBClient'; import createNewBeerPost from '@/services/BeerPost/createNewBeerPost'; +import getBeerPostsByPostedById from '@/services/BeerPost/getBeerPostsByPostedById'; import { BeerPostRequest, CreateBeerPostRequest, @@ -17,6 +18,7 @@ import { GetAllBeerPostsRequest, GetBeerRecommendationsRequest, } from './types'; +import { GetPostsByUserIdRequest } from '../types'; export const checkIfBeerPostOwner = async ( req: BeerPostRequestType, @@ -141,3 +143,32 @@ export const createBeerPost = async ( success: true, }); }; + +export const getBeerPostsByUserId = async ( + req: GetPostsByUserIdRequest, + res: NextApiResponse>, +) => { + const pageNum = parseInt(req.query.page_num, 10); + const pageSize = parseInt(req.query.page_size, 10); + + const { id } = req.query; + + const beerPosts = await getBeerPostsByPostedById({ + pageNum, + pageSize, + postedById: id, + }); + + const beerPostCount = await DBClient.instance.beerPost.count({ + where: { postedBy: { id } }, + }); + + res.setHeader('X-Total-Count', beerPostCount); + + res.status(200).json({ + message: `Beer posts by user ${id} fetched successfully`, + statusCode: 200, + payload: beerPosts, + success: true, + }); +}; diff --git a/src/controllers/posts/breweries/index.ts b/src/controllers/posts/breweries/index.ts new file mode 100644 index 0000000..e3b7c0c --- /dev/null +++ b/src/controllers/posts/breweries/index.ts @@ -0,0 +1,36 @@ +import DBClient from '@/prisma/DBClient'; +import getAllBreweryPostsByPostedById from '@/services/BreweryPost/getAllBreweryPostsByPostedById'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { z } from 'zod'; +import { GetPostsByUserIdRequest } from '../types'; + +// eslint-disable-next-line import/prefer-default-export +export const getBreweryPostsByUserId = async ( + req: GetPostsByUserIdRequest, + res: NextApiResponse>, +) => { + const pageNum = parseInt(req.query.page_num, 10); + const pageSize = parseInt(req.query.page_size, 10); + + const { id } = req.query; + + const breweryPosts = await getAllBreweryPostsByPostedById({ + pageNum, + pageSize, + postedById: id, + }); + + const breweryPostCount = await DBClient.instance.breweryPost.count({ + where: { postedBy: { id } }, + }); + + res.setHeader('X-Total-Count', breweryPostCount); + + res.status(200).json({ + message: `Brewery posts by user ${id} fetched successfully`, + statusCode: 200, + payload: breweryPosts, + success: true, + }); +}; diff --git a/src/controllers/posts/types/index.ts b/src/controllers/posts/types/index.ts index ec53d99..e6578ac 100644 --- a/src/controllers/posts/types/index.ts +++ b/src/controllers/posts/types/index.ts @@ -3,3 +3,7 @@ import { NextApiRequest } from 'next'; export interface GetAllPostsRequest extends NextApiRequest { query: { page_size: string; page_num: string }; } + +export interface GetPostsByUserIdRequest extends NextApiRequest { + query: { id: string; page_size: string; page_num: string }; +} diff --git a/src/controllers/auth/index.ts b/src/controllers/users/auth/index.ts similarity index 56% rename from src/controllers/auth/index.ts rename to src/controllers/users/auth/index.ts index 8008a75..9b401e7 100644 --- a/src/controllers/auth/index.ts +++ b/src/controllers/users/auth/index.ts @@ -19,11 +19,17 @@ import { verifyConfirmationToken } from '@/config/jwt'; import updateUserToBeConfirmedById from '@/services/User/updateUserToBeConfirmedById'; import DBClient from '@/prisma/DBClient'; import sendResetPasswordEmail from '@/services/User/sendResetPasswordEmail'; +import { hashPassword } from '@/config/auth/passwordFns'; +import deleteUserById from '@/services/User/deleteUserById'; import { + CheckEmailRequest, + CheckUsernameRequest, RegisterUserRequest, ResetPasswordRequest, TokenValidationRequest, + UpdatePasswordRequest, } from './types'; +import { EditUserRequest, UserRouteRequest } from '../profile/types'; export const authenticateUser = expressWrapper( async ( @@ -165,3 +171,135 @@ export const resetPassword = async ( 'If an account with that email exists, we have sent you an email to reset your password.', }); }; + +export const sendCurrentUser = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse, +) => { + const { user } = req; + res.status(200).json({ + message: `Currently logged in as ${user!.username}`, + statusCode: 200, + success: true, + payload: user, + }); +}; + +export const checkEmail = async (req: CheckEmailRequest, res: NextApiResponse) => { + const { email: emailToCheck } = req.query; + + const email = await findUserByEmail(emailToCheck); + + res.json({ + success: true, + payload: { emailIsTaken: !!email }, + statusCode: 200, + message: 'Getting email availability.', + }); +}; + +export const checkUsername = async (req: CheckUsernameRequest, res: NextApiResponse) => { + const { username: usernameToCheck } = req.query; + + const username = await findUserByUsername(usernameToCheck); + + res.json({ + success: true, + payload: { usernameIsTaken: !!username }, + statusCode: 200, + message: username ? 'Username is taken.' : 'Username is available.', + }); +}; + +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 }, + }); + + res.json({ + message: 'Updated user password.', + statusCode: 200, + success: true, + }); +}; + +export const resendConfirmation = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse, +) => { + const user = req.user!; + + await sendConfirmationEmail(user); + res.status(200).json({ + message: `Resent the confirmation email for ${user.username}.`, + statusCode: 200, + success: true, + }); +}; + +export const editUserInfo = async ( + req: EditUserRequest, + res: NextApiResponse>, +) => { + 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, + }, + }); + + res.json({ + message: 'User edited successfully', + payload: updatedUser, + success: true, + statusCode: 200, + }); +}; + +export const deleteAccount = 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, + }); +}; diff --git a/src/controllers/auth/types/index.ts b/src/controllers/users/auth/types/index.ts similarity index 55% rename from src/controllers/auth/types/index.ts rename to src/controllers/users/auth/types/index.ts index b11984f..adcadc4 100644 --- a/src/controllers/auth/types/index.ts +++ b/src/controllers/users/auth/types/index.ts @@ -1,5 +1,8 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; -import { CreateUserValidationSchema } from '@/services/User/schema/CreateUserValidationSchemas'; +import { + CreateUserValidationSchema, + UpdatePasswordSchema, +} from '@/services/User/schema/CreateUserValidationSchemas'; import TokenValidationSchema from '@/services/User/schema/TokenValidationSchema'; import { NextApiRequest } from 'next'; import { z } from 'zod'; @@ -15,3 +18,14 @@ export interface TokenValidationRequest extends UserExtendedNextApiRequest { export interface ResetPasswordRequest extends NextApiRequest { body: { email: string }; } + +export interface UpdatePasswordRequest extends UserExtendedNextApiRequest { + body: z.infer; +} +export interface CheckEmailRequest extends NextApiRequest { + query: { email: string }; +} + +export interface CheckUsernameRequest extends NextApiRequest { + query: { username: string }; +} diff --git a/src/controllers/users/profile/index.ts b/src/controllers/users/profile/index.ts new file mode 100644 index 0000000..dbe4989 --- /dev/null +++ b/src/controllers/users/profile/index.ts @@ -0,0 +1,237 @@ +import ServerError from '@/config/util/ServerError'; +import DBClient from '@/prisma/DBClient'; +import findUserById from '@/services/User/findUserById'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { z } from 'zod'; +import getUsersFollowingUser from '@/services/UserFollows/getUsersFollowingUser'; +import getUsersFollowedByUser from '@/services/UserFollows/getUsersFollowedByUser'; +import { NextHandler } from 'next-connect'; +import updateUserAvatarById, { + UpdateUserAvatarByIdParams, +} from '@/services/UserAccount/UpdateUserAvatarByIdParams'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import updateUserProfileById from '@/services/User/updateUserProfileById'; +import { + UserRouteRequest, + GetUserFollowInfoRequest, + EditUserRequest, + UpdateAvatarRequest, + UpdateProfileRequest, +} from './types'; + +export const followUser = async ( + req: UserRouteRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + const user = await findUserById(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, + }, + }); + + if (!userIsFollowedBySessionUser) { + await DBClient.instance.userFollow.create({ + data: { followerId: currentUser.id, followingId: id }, + }); + + res.status(200).json({ + message: 'Now following user.', + success: true, + statusCode: 200, + }); + + return; + } + + await DBClient.instance.userFollow.delete({ + where: { + followerId_followingId: { + followerId: currentUser.id, + followingId: id, + }, + }, + }); + + res.status(200).json({ + message: 'No longer following user.', + success: true, + statusCode: 200, + }); +}; + +export const getUserFollowers = async ( + req: GetUserFollowInfoRequest, + res: NextApiResponse>, +) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, page_num, page_size } = req.query; + + const user = await findUserById(id); + if (!user) { + throw new ServerError('User not found', 404); + } + + const pageNum = parseInt(page_num, 10); + const pageSize = parseInt(page_size, 10); + + const following = await getUsersFollowingUser({ + userId: id, + pageNum, + pageSize, + }); + const followingCount = await DBClient.instance.userFollow.count({ + where: { following: { id } }, + }); + + res.setHeader('X-Total-Count', followingCount); + + res.json({ + message: 'Retrieved users that are followed by queried user', + payload: following, + success: true, + statusCode: 200, + }); +}; + +export const getUsersFollowed = async ( + req: GetUserFollowInfoRequest, + res: NextApiResponse>, +) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, page_num, page_size } = req.query; + + const user = await findUserById(id); + if (!user) { + throw new ServerError('User not found', 404); + } + + const pageNum = parseInt(page_num, 10); + const pageSize = parseInt(page_size, 10); + + const following = await getUsersFollowedByUser({ + userId: id, + pageNum, + pageSize, + }); + const followingCount = await DBClient.instance.userFollow.count({ + where: { follower: { id } }, + }); + + res.setHeader('X-Total-Count', followingCount); + + res.json({ + message: 'Retrieved users that are followed by queried user', + payload: following, + success: true, + statusCode: 200, + }); +}; + +export const checkIfUserIsFollowedBySessionUser = async ( + req: GetUserFollowInfoRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + const user = await findUserById(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 }, + }); + + if (!userIsFollowedBySessionUser) { + res.status(200).json({ + message: 'User is not followed by the current user.', + success: true, + statusCode: 200, + payload: { isFollowed: false }, + }); + + return; + } + + res.status(200).json({ + message: 'User is followed by the current user.', + success: true, + statusCode: 200, + payload: { isFollowed: true }, + }); +}; + +export const checkIfUserCanEditUser = async ( + req: EditUserRequest, + res: NextApiResponse, + next: NextHandler, +) => { + const authenticatedUser = req.user!; + + const userToUpdate = await findUserById(req.query.id); + if (!userToUpdate) { + throw new ServerError('User not found', 404); + } + + if (authenticatedUser.id !== userToUpdate.id) { + throw new ServerError('You are not permitted to modify this user', 403); + } + + return next(); +}; + +export const checkIfUserCanUpdateProfile = async ( + req: T, + res: NextApiResponse, + next: NextHandler, +) => { + const user = req.user!; + + if (user.id !== req.query.id) { + throw new ServerError('You can only update your own profile.', 403); + } + + await next(); +}; + +export const updateAvatar = async (req: UpdateAvatarRequest, res: NextApiResponse) => { + const { file, user } = req; + + const avatar: UpdateUserAvatarByIdParams['data']['avatar'] = { + alt: file.originalname, + path: file.path, + caption: '', + }; + + await updateUserAvatarById({ id: user!.id, data: { avatar } }); + res.status(200).json({ + message: 'User avatar updated successfully.', + statusCode: 200, + success: true, + }); +}; + +export const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => { + const user = req.user!; + const { body } = req; + + await updateUserProfileById({ id: user!.id, data: { bio: body.bio } }); + + res.status(200).json({ + message: 'Profile updated successfully.', + statusCode: 200, + success: true, + }); +}; diff --git a/src/controllers/users/profile/types/index.ts b/src/controllers/users/profile/types/index.ts new file mode 100644 index 0000000..b80838b --- /dev/null +++ b/src/controllers/users/profile/types/index.ts @@ -0,0 +1,23 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import EditUserSchema from '@/services/User/schema/EditUserSchema'; +import { z } from 'zod'; + +export interface UserRouteRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} + +export interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest { + query: { id: string; page_size: string; page_num: string }; +} + +export interface EditUserRequest extends UserRouteRequest { + body: z.infer; +} + +export interface UpdateAvatarRequest extends UserExtendedNextApiRequest { + file: Express.Multer.File; +} + +export interface UpdateProfileRequest extends UserExtendedNextApiRequest { + body: { bio: string }; +} diff --git a/src/pages/api/users/[id]/follow-user.ts b/src/pages/api/users/[id]/follow-user.ts index 0bb438e..866f40e 100644 --- a/src/pages/api/users/[id]/follow-user.ts +++ b/src/pages/api/users/[id]/follow-user.ts @@ -2,9 +2,7 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; 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 findUserById from '@/services/User/findUserById'; +import { followUser } from '@/controllers/users/profile'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; @@ -21,55 +19,6 @@ const router = createRouter< NextApiResponse> >(); -const followUser = async ( - req: GetUserFollowInfoRequest, - res: NextApiResponse>, -) => { - const { id } = req.query; - - const user = await findUserById(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, - }, - }); - - if (!userIsFollowedBySessionUser) { - await DBClient.instance.userFollow.create({ - data: { followerId: currentUser.id, followingId: id }, - }); - - res.status(200).json({ - message: 'Now following user.', - success: true, - statusCode: 200, - }); - - return; - } - - await DBClient.instance.userFollow.delete({ - where: { - followerId_followingId: { - followerId: currentUser.id, - followingId: id, - }, - }, - }); - - res.status(200).json({ - message: 'No longer following user.', - success: true, - statusCode: 200, - }); -}; - router.post( validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getCurrentUser, diff --git a/src/pages/api/users/[id]/followers.ts b/src/pages/api/users/[id]/followers.ts index 2917802..761e1a3 100644 --- a/src/pages/api/users/[id]/followers.ts +++ b/src/pages/api/users/[id]/followers.ts @@ -1,59 +1,18 @@ -import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import ServerError from '@/config/util/ServerError'; -import DBClient from '@/prisma/DBClient'; -import findUserById from '@/services/User/findUserById'; -import getUsersFollowingUser from '@/services/UserFollows/getUsersFollowingUser'; +import { getUserFollowers } from '@/controllers/users/profile'; +import { GetUserFollowInfoRequest } from '@/controllers/users/profile/types'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; -interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest { - query: { id: string; page_size: string; page_num: string }; -} - const router = createRouter< GetUserFollowInfoRequest, NextApiResponse> >(); -const getFollowingInfo = async ( - req: GetUserFollowInfoRequest, - res: NextApiResponse>, -) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, page_num, page_size } = req.query; - - const user = await findUserById(id); - if (!user) { - throw new ServerError('User not found', 404); - } - - const pageNum = parseInt(page_num, 10); - const pageSize = parseInt(page_size, 10); - - const following = await getUsersFollowingUser({ - userId: id, - pageNum, - pageSize, - }); - const followingCount = await DBClient.instance.userFollow.count({ - where: { following: { id } }, - }); - - res.setHeader('X-Total-Count', followingCount); - - res.json({ - message: 'Retrieved users that are followed by queried user', - payload: following, - success: true, - statusCode: 200, - }); -}; - router.get( validateRequest({ querySchema: z.object({ @@ -62,7 +21,7 @@ router.get( page_num: z.string().regex(/^\d+$/), }), }), - getFollowingInfo, + getUserFollowers, ); const handler = router.handler(NextConnectOptions); diff --git a/src/pages/api/users/[id]/following.ts b/src/pages/api/users/[id]/following.ts index 1c3fa61..9e99111 100644 --- a/src/pages/api/users/[id]/following.ts +++ b/src/pages/api/users/[id]/following.ts @@ -1,10 +1,7 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import ServerError from '@/config/util/ServerError'; -import DBClient from '@/prisma/DBClient'; -import findUserById from '@/services/User/findUserById'; -import getUsersFollowedByUser from '@/services/UserFollows/getUsersFollowedByUser'; +import { getUsersFollowed } from '@/controllers/users/profile'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; @@ -20,40 +17,6 @@ const router = createRouter< NextApiResponse> >(); -const getFollowingInfo = async ( - req: GetUserFollowInfoRequest, - res: NextApiResponse>, -) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, page_num, page_size } = req.query; - - const user = await findUserById(id); - if (!user) { - throw new ServerError('User not found', 404); - } - - const pageNum = parseInt(page_num, 10); - const pageSize = parseInt(page_size, 10); - - const following = await getUsersFollowedByUser({ - userId: id, - pageNum, - pageSize, - }); - const followingCount = await DBClient.instance.userFollow.count({ - where: { follower: { id } }, - }); - - res.setHeader('X-Total-Count', followingCount); - - res.json({ - message: 'Retrieved users that are followed by queried user', - payload: following, - success: true, - statusCode: 200, - }); -}; - router.get( validateRequest({ querySchema: z.object({ @@ -62,7 +25,7 @@ router.get( page_num: z.string().regex(/^\d+$/), }), }), - getFollowingInfo, + getUsersFollowed, ); const handler = router.handler(NextConnectOptions); diff --git a/src/pages/api/users/[id]/index.ts b/src/pages/api/users/[id]/index.ts index d0da141..afa2179 100644 --- a/src/pages/api/users/[id]/index.ts +++ b/src/pages/api/users/[id]/index.ts @@ -1,113 +1,16 @@ -import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; 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'; -import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas'; +import { editUserInfo, deleteAccount } from '@/controllers/users/auth'; +import { checkIfUserCanEditUser } from '@/controllers/users/profile'; +import { EditUserRequest } from '@/controllers/users/profile/types'; +import EditUserSchema from '@/services/User/schema/EditUserSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; -import { NextHandler, createRouter } from 'next-connect'; +import { createRouter } from 'next-connect'; import { z } from 'zod'; -const EditUserSchema = BaseCreateUserSchema.pick({ - username: true, - email: true, - firstName: true, - lastName: true, -}); - -interface UserRouteRequest extends UserExtendedNextApiRequest { - query: { id: string }; -} - -interface EditUserRequest extends UserRouteRequest { - body: z.infer; -} - -const checkIfUserCanEditUser = async ( - req: EditUserRequest, - res: NextApiResponse, - next: NextHandler, -) => { - const authenticatedUser = req.user!; - - const userToUpdate = await findUserById(req.query.id); - if (!userToUpdate) { - throw new ServerError('User not found', 404); - } - - if (authenticatedUser.id !== userToUpdate.id) { - throw new ServerError('You are not permitted to modify this user', 403); - } - - return next(); -}; - -const editUser = async ( - req: EditUserRequest, - res: NextApiResponse>, -) => { - 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, - }, - }); - - res.json({ - message: 'User edited successfully', - payload: updatedUser, - success: true, - statusCode: 200, - }); -}; - -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> @@ -120,7 +23,7 @@ router.put( querySchema: z.object({ id: z.string().cuid() }), }), checkIfUserCanEditUser, - editUser, + editUserInfo, ); router.delete( @@ -129,7 +32,7 @@ router.delete( querySchema: z.object({ id: z.string().cuid() }), }), checkIfUserCanEditUser, - deleteUser, + deleteAccount, ); const handler = router.handler(NextConnectOptions); diff --git a/src/pages/api/users/[id]/is-followed.ts b/src/pages/api/users/[id]/is-followed.ts index ef6c368..a53ed42 100644 --- a/src/pages/api/users/[id]/is-followed.ts +++ b/src/pages/api/users/[id]/is-followed.ts @@ -1,62 +1,21 @@ -import { UserExtendedNextApiRequest } from '@/config/auth/types'; -import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; -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 findUserById from '@/services/User/findUserById'; - -import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; - import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; -interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest { - query: { id: string }; -} +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; + +import { checkIfUserIsFollowedBySessionUser } from '@/controllers/users/profile'; +import { GetUserFollowInfoRequest } from '@/controllers/users/profile/types'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; const router = createRouter< GetUserFollowInfoRequest, NextApiResponse> >(); -const checkIfUserIsFollowedBySessionUser = async ( - req: GetUserFollowInfoRequest, - res: NextApiResponse>, -) => { - const { id } = req.query; - - const user = await findUserById(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 }, - }); - - if (!userIsFollowedBySessionUser) { - res.status(200).json({ - message: 'User is not followed by the current user.', - success: true, - statusCode: 200, - payload: { isFollowed: false }, - }); - - return; - } - - res.status(200).json({ - message: 'User is followed by the current user.', - success: true, - statusCode: 200, - payload: { isFollowed: true }, - }); -}; - router.get( validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }), getCurrentUser, diff --git a/src/pages/api/users/[id]/posts/beers.ts b/src/pages/api/users/[id]/posts/beers.ts index 040e64b..a710d6b 100644 --- a/src/pages/api/users/[id]/posts/beers.ts +++ b/src/pages/api/users/[id]/posts/beers.ts @@ -1,48 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; +import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import DBClient from '@/prisma/DBClient'; -import getBeerPostsByPostedById from '@/services/BeerPost/getBeerPostsByPostedById'; import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; - -interface GetBeerPostsRequest extends NextApiRequest { - query: { - page_num: string; - page_size: string; - id: string; - }; -} - -const getBeerPostsByUserId = async ( - req: GetBeerPostsRequest, - res: NextApiResponse>, -) => { - const pageNum = parseInt(req.query.page_num, 10); - const pageSize = parseInt(req.query.page_size, 10); - - const { id } = req.query; - - const beerPosts = await getBeerPostsByPostedById({ pageNum, pageSize, postedById: id }); - - const beerPostCount = await DBClient.instance.beerPost.count({ - where: { postedBy: { id } }, - }); - - res.setHeader('X-Total-Count', beerPostCount); - - res.status(200).json({ - message: `Beer posts by user ${id} fetched successfully`, - statusCode: 200, - payload: beerPosts, - success: true, - }); -}; +import { GetPostsByUserIdRequest } from '@/controllers/posts/types'; +import { getBeerPostsByUserId } from '@/controllers/posts/beerPosts'; const router = createRouter< - GetBeerPostsRequest, + GetPostsByUserIdRequest, NextApiResponse> >(); diff --git a/src/pages/api/users/[id]/posts/breweries.ts b/src/pages/api/users/[id]/posts/breweries.ts index 1b8975f..dcc79b8 100644 --- a/src/pages/api/users/[id]/posts/breweries.ts +++ b/src/pages/api/users/[id]/posts/breweries.ts @@ -1,52 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; +import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import DBClient from '@/prisma/DBClient'; -import getAllBreweryPostsByPostedById from '@/services/BreweryPost/getAllBreweryPostsByPostedById'; import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; - -interface GetBreweryPostsRequest extends NextApiRequest { - query: { - page_num: string; - page_size: string; - id: string; - }; -} - -const getBreweryPostsByUserId = async ( - req: GetBreweryPostsRequest, - res: NextApiResponse>, -) => { - const pageNum = parseInt(req.query.page_num, 10); - const pageSize = parseInt(req.query.page_size, 10); - - const { id } = req.query; - - const breweryPosts = await getAllBreweryPostsByPostedById({ - pageNum, - pageSize, - postedById: id, - }); - - const breweryPostCount = await DBClient.instance.breweryPost.count({ - where: { postedBy: { id } }, - }); - - res.setHeader('X-Total-Count', breweryPostCount); - - res.status(200).json({ - message: `Brewery posts by user ${id} fetched successfully`, - statusCode: 200, - payload: breweryPosts, - success: true, - }); -}; +import { getBreweryPostsByUserId } from '@/controllers/posts/breweries'; +import { GetPostsByUserIdRequest } from '@/controllers/posts/types'; const router = createRouter< - GetBreweryPostsRequest, + GetPostsByUserIdRequest, NextApiResponse> >(); diff --git a/src/pages/api/users/[id]/profile/update-avatar.ts b/src/pages/api/users/[id]/profile/update-avatar.ts index fa25dd7..c08de09 100644 --- a/src/pages/api/users/[id]/profile/update-avatar.ts +++ b/src/pages/api/users/[id]/profile/update-avatar.ts @@ -33,7 +33,7 @@ const checkIfUserCanUpdateProfile = async ( await next(); }; -const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => { +const updateAvatar = async (req: UpdateProfileRequest, res: NextApiResponse) => { const { file, user } = req; const avatar: UpdateUserAvatarByIdParams['data']['avatar'] = { @@ -60,7 +60,7 @@ router.put( checkIfUserCanUpdateProfile, // @ts-expect-error singleUploadMiddleware, - updateProfile, + updateAvatar, ); const handler = router.handler(); diff --git a/src/pages/api/users/[id]/profile/update-bio.ts b/src/pages/api/users/[id]/profile/update-bio.ts index 1b0645c..65c0b68 100644 --- a/src/pages/api/users/[id]/profile/update-bio.ts +++ b/src/pages/api/users/[id]/profile/update-bio.ts @@ -1,75 +1,13 @@ -import { UserExtendedNextApiRequest } from '@/config/auth/types'; - 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 GetUserSchema from '@/services/User/schema/GetUserSchema'; +import { checkIfUserCanUpdateProfile, updateProfile } from '@/controllers/users/profile'; +import { UpdateProfileRequest } from '@/controllers/users/profile/types'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; -import { NextHandler, createRouter } from 'next-connect'; +import { createRouter } from 'next-connect'; import { z } from 'zod'; -interface UpdateProfileRequest extends UserExtendedNextApiRequest { - body: { bio: string }; -} - -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; -}; - -const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => { - const user = req.user!; - const { body } = req; - - await updateUserProfileById({ id: user!.id, data: { bio: body.bio } }); - - res.status(200).json({ - message: 'Profile updated successfully.', - statusCode: 200, - success: true, - }); -}; - -const checkIfUserCanUpdateProfile = async ( - req: UpdateProfileRequest, - res: NextApiResponse, - next: NextHandler, -) => { - const user = req.user!; - - if (user.id !== req.query.id) { - throw new ServerError('You can only update your own profile.', 403); - } - - await next(); -}; - const router = createRouter< UpdateProfileRequest, NextApiResponse> diff --git a/src/pages/api/users/check-email.ts b/src/pages/api/users/check-email.ts index 4d03c37..b48a3b7 100644 --- a/src/pages/api/users/check-email.ts +++ b/src/pages/api/users/check-email.ts @@ -1,38 +1,18 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { NextApiRequest, NextApiResponse } from 'next'; +import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import findUserByEmail from '@/services/User/findUserByEmail'; - -const CheckEmailRequestQuerySchema = z.object({ - email: z.string(), -}); - -interface CheckEmailRequestSchema extends NextApiRequest { - query: z.infer; -} +import { CheckEmailRequest } from '@/controllers/users/auth/types'; +import { checkEmail } from '@/controllers/users/auth'; const router = createRouter< - CheckEmailRequestSchema, + CheckEmailRequest, NextApiResponse> >(); -const checkEmail = async (req: NextApiRequest, res: NextApiResponse) => { - const { email: emailToCheck } = req.query; - - const email = await findUserByEmail(emailToCheck as string); - - res.json({ - success: true, - payload: { emailIsTaken: !!email }, - statusCode: 200, - message: 'Getting email availability.', - }); -}; - router.get( validateRequest({ querySchema: z.object({ email: z.string().email() }) }), checkEmail, diff --git a/src/pages/api/users/check-username.ts b/src/pages/api/users/check-username.ts index ebf4f60..5c2e79b 100644 --- a/src/pages/api/users/check-username.ts +++ b/src/pages/api/users/check-username.ts @@ -1,38 +1,19 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { NextApiRequest, NextApiResponse } from 'next'; +import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import findUserByUsername from '@/services/User/findUserByUsername'; -const CheckUsernameRequestQuerySchema = z.object({ - username: z.string(), -}); - -interface CheckUsernameRequestSchema extends NextApiRequest { - query: z.infer; -} +import { CheckUsernameRequest } from '@/controllers/users/auth/types'; +import { checkUsername } from '@/controllers/users/auth'; const router = createRouter< - CheckUsernameRequestSchema, + CheckUsernameRequest, NextApiResponse> >(); -const checkUsername = async (req: NextApiRequest, res: NextApiResponse) => { - const { username: usernameToCheck } = req.query; - - const user = await findUserByUsername(usernameToCheck as string); - - res.json({ - success: true, - payload: { usernameIsTaken: !!user }, - statusCode: 200, - message: 'Getting username availability.', - }); -}; - router.get( validateRequest({ querySchema: z.object({ username: z.string() }) }), checkUsername, diff --git a/src/pages/api/users/confirm.ts b/src/pages/api/users/confirm.ts index 165a41b..274cb0a 100644 --- a/src/pages/api/users/confirm.ts +++ b/src/pages/api/users/confirm.ts @@ -7,8 +7,8 @@ import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import { TokenValidationRequest } from '@/controllers/auth/types'; -import { confirmUser } from '@/controllers/auth'; +import { TokenValidationRequest } from '@/controllers/users/auth/types'; +import { confirmUser } from '@/controllers/users/auth'; import TokenValidationSchema from '@/services/User/schema/TokenValidationSchema'; const router = createRouter< diff --git a/src/pages/api/users/current.ts b/src/pages/api/users/current.ts index aff2a35..ba7fd7c 100644 --- a/src/pages/api/users/current.ts +++ b/src/pages/api/users/current.ts @@ -5,16 +5,7 @@ import { NextApiResponse } from 'next'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import { createRouter } from 'next-connect'; import { z } from 'zod'; - -const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResponse) => { - const { user } = req; - res.status(200).json({ - message: `Currently logged in as ${user!.username}`, - statusCode: 200, - success: true, - payload: user, - }); -}; +import { sendCurrentUser } from '@/controllers/users/auth'; const router = createRouter< UserExtendedNextApiRequest, diff --git a/src/pages/api/users/edit-password.ts b/src/pages/api/users/edit-password.ts index d62f21c..899b2f9 100644 --- a/src/pages/api/users/edit-password.ts +++ b/src/pages/api/users/edit-password.ts @@ -1,38 +1,14 @@ -import { hashPassword } from '@/config/auth/passwordFns'; -import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import DBClient from '@/prisma/DBClient'; +import { updatePassword } from '@/controllers/users/auth'; +import { UpdatePasswordRequest } from '@/controllers/users/auth/types'; import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; -interface UpdatePasswordRequest extends UserExtendedNextApiRequest { - body: z.infer; -} - -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 }, - }); - - res.json({ - message: 'Updated user password.', - statusCode: 200, - success: true, - }); -}; const router = createRouter< UpdatePasswordRequest, NextApiResponse> diff --git a/src/pages/api/users/forgot-password.ts b/src/pages/api/users/forgot-password.ts index e892acc..a1789aa 100644 --- a/src/pages/api/users/forgot-password.ts +++ b/src/pages/api/users/forgot-password.ts @@ -5,8 +5,8 @@ import { createRouter } from 'next-connect'; import { z } from 'zod'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import { resetPassword } from '@/controllers/auth'; -import { ResetPasswordRequest } from '@/controllers/auth/types'; +import { resetPassword } from '@/controllers/users/auth'; +import { ResetPasswordRequest } from '@/controllers/users/auth/types'; const router = createRouter< ResetPasswordRequest, diff --git a/src/pages/api/users/login.ts b/src/pages/api/users/login.ts index 5d9abbc..4648f44 100644 --- a/src/pages/api/users/login.ts +++ b/src/pages/api/users/login.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema'; import { UserExtendedNextApiRequest } from '@/config/auth/types'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; -import { authenticateUser, loginUser } from '@/controllers/auth'; +import { authenticateUser, loginUser } from '@/controllers/users/auth'; const router = createRouter< UserExtendedNextApiRequest, diff --git a/src/pages/api/users/logout.ts b/src/pages/api/users/logout.ts index ef198b8..0e23889 100644 --- a/src/pages/api/users/logout.ts +++ b/src/pages/api/users/logout.ts @@ -3,7 +3,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem import { NextApiRequest, NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { z } from 'zod'; -import { logoutUser } from '@/controllers/auth'; +import { logoutUser } from '@/controllers/users/auth'; const router = createRouter< NextApiRequest, diff --git a/src/pages/api/users/register.ts b/src/pages/api/users/register.ts index 0960df3..8ee065e 100644 --- a/src/pages/api/users/register.ts +++ b/src/pages/api/users/register.ts @@ -5,8 +5,8 @@ import { CreateUserValidationSchema } from '@/services/User/schema/CreateUserVal import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { registerUser } from '@/controllers/auth'; -import { RegisterUserRequest } from '@/controllers/auth/types'; +import { registerUser } from '@/controllers/users/auth'; +import { RegisterUserRequest } from '@/controllers/users/auth/types'; const router = createRouter< RegisterUserRequest, diff --git a/src/pages/api/users/resend-confirmation.ts b/src/pages/api/users/resend-confirmation.ts index d422c29..56c92ab 100644 --- a/src/pages/api/users/resend-confirmation.ts +++ b/src/pages/api/users/resend-confirmation.ts @@ -5,21 +5,7 @@ import { NextApiResponse } from 'next'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import { createRouter } from 'next-connect'; import { z } from 'zod'; -import sendConfirmationEmail from '@/services/User/sendConfirmationEmail'; - -const resendConfirmation = async ( - req: UserExtendedNextApiRequest, - res: NextApiResponse, -) => { - const user = req.user!; - - await sendConfirmationEmail(user); - res.status(200).json({ - message: `Resent the confirmation email for ${user.username}.`, - statusCode: 200, - success: true, - }); -}; +import { resendConfirmation } from '@/controllers/users/auth'; const router = createRouter< UserExtendedNextApiRequest, diff --git a/src/services/BeerPost/getBeerPostsByBeerStyleId.ts b/src/services/BeerPost/getBeerPostsByBeerStyleId.ts index 6fabe91..ccc089f 100644 --- a/src/services/BeerPost/getBeerPostsByBeerStyleId.ts +++ b/src/services/BeerPost/getBeerPostsByBeerStyleId.ts @@ -16,7 +16,7 @@ const getBeerPostsByBeerStyleId = async ({ const beers = await DBClient.instance.beerPost.findMany({ where: { styleId }, take: pageSize, - skip: pageNum * pageSize, + skip: (pageNum - 1) * pageSize, select: { id: true, name: true, diff --git a/src/services/BeerPost/getBeerPostsByBreweryId.ts b/src/services/BeerPost/getBeerPostsByBreweryId.ts index 1bbbc58..4c7946a 100644 --- a/src/services/BeerPost/getBeerPostsByBreweryId.ts +++ b/src/services/BeerPost/getBeerPostsByBreweryId.ts @@ -16,7 +16,7 @@ const getAllBeerPostsByBreweryId = async ({ const beers = await DBClient.instance.beerPost.findMany({ where: { breweryId }, take: pageSize, - skip: pageNum * pageSize, + skip: (pageNum - 1) * pageSize, select: { id: true, name: true, diff --git a/src/services/BeerPost/getBeerPostsByPostedById.ts b/src/services/BeerPost/getBeerPostsByPostedById.ts index 31ce792..1d6589d 100644 --- a/src/services/BeerPost/getBeerPostsByPostedById.ts +++ b/src/services/BeerPost/getBeerPostsByPostedById.ts @@ -16,7 +16,7 @@ const getBeerPostsByPostedById = async ({ const beers = await DBClient.instance.beerPost.findMany({ where: { postedBy: { id: postedById } }, take: pageSize, - skip: pageNum * pageSize, + skip: (pageNum - 1) * pageSize, select: { id: true, name: true, diff --git a/src/services/User/schema/EditUserSchema.ts b/src/services/User/schema/EditUserSchema.ts new file mode 100644 index 0000000..f9d4bad --- /dev/null +++ b/src/services/User/schema/EditUserSchema.ts @@ -0,0 +1,10 @@ +import { BaseCreateUserSchema } from './CreateUserValidationSchemas'; + +const EditUserSchema = BaseCreateUserSchema.pick({ + username: true, + email: true, + firstName: true, + lastName: true, +}); + +export default EditUserSchema; diff --git a/src/services/User/updateUserProfileById.ts b/src/services/User/updateUserProfileById.ts new file mode 100644 index 0000000..204aba9 --- /dev/null +++ b/src/services/User/updateUserProfileById.ts @@ -0,0 +1,33 @@ +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;