Update user auth services

This commit is contained in:
Aaron William Po
2023-12-17 13:39:50 -05:00
parent 70a168df92
commit bffa28b93d
31 changed files with 700 additions and 552 deletions

View File

@@ -3,4 +3,3 @@ Disallow: /api/
Disallow: /login/
Disallow: /register/
Disallow: /users/
Disallow: /account/

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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);

View File

@@ -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 <T extends UserExtendedNextApiR
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 } });
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.',

View File

@@ -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<ForgotEmailProps> = ({ name, url }) => {
const ResetPasswordEmail: FC<ResetPasswordEmailProps> = ({ name, url }) => {
return (
<Tailwind>
<Container className="mx-auto">
@@ -36,4 +36,4 @@ const ForgotEmail: FC<ForgotEmailProps> = ({ name, url }) => {
);
};
export default ForgotEmail;
export default ResetPasswordEmail;

View File

@@ -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<WelcomeEmail> = ({ name, url }) => (
const WelcomeEmail: FC<WelcomeEmailProps> = ({ name, url }) => (
<Tailwind>
<Container className="flex h-full w-full flex-col items-center justify-center">
<Section>
@@ -43,4 +43,4 @@ const Welcome: FC<WelcomeEmail> = ({ name, url }) => (
</Tailwind>
);
export default Welcome;
export default WelcomeEmail;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<typeof GetUserSchema>;
@@ -39,7 +40,7 @@ export default UserInfoPage;
export const getServerSideProps = withPageAuthRequired<UserInfoPageProps>(
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 };

View File

@@ -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: {} };
}

View File

@@ -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<typeof GetUserSchema> = 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;

View File

@@ -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<typeof CreateUserValidationSchema>) => {
const hash = await hashPassword(password);
const user: z.infer<typeof GetUserSchema> = 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;

View File

@@ -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<typeof GetUserSchema> | 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;

View File

@@ -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;

View File

@@ -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<typeof GetUserSchema> | 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;

View File

@@ -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<typeof PublicUserSchema> | 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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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<typeof GetUserSchema>;
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<unknown, string>;
const html = render(component);
const text = render(component, { plainText: true });
await sendEmail({ address, subject, text, html });
};
export default sendConfirmationEmail;

View File

@@ -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;

View File

@@ -0,0 +1,47 @@
import { z } from 'zod';
import GetUserSchema from '../schema/GetUserSchema';
import { CreateUserValidationSchema } from '../schema/CreateUserValidationSchemas';
type User = z.infer<typeof GetUserSchema>;
type AuthUser = { username: string; hash: string; id: string };
export type CreateNewUser = (
args: z.infer<typeof CreateUserValidationSchema>,
) => Promise<User>;
export type DeleteUserById = (args: { userId: string }) => Promise<AuthUser | null>;
export type FindUserById = (args: { userId: string }) => Promise<User | null>;
export type FindUserByUsername = (args: { username: string }) => Promise<AuthUser | null>;
export type FindUserByEmail = (args: { email: string }) => Promise<User | null>;
export type UpdateUserPassword = (args: {
userId: string;
password: string;
}) => Promise<AuthUser | null>;
export type SendConfirmationEmail = (args: {
userId: string;
username: string;
email: string;
}) => Promise<void>;
export type SendResetPasswordEmail = (args: {
userId: string;
username: string;
email: string;
}) => Promise<void>;
export type UpdateUserToBeConfirmedById = (args: { userId: string }) => Promise<User>;
export type UpdateUserById = (args: {
userId: string;
data: {
email: string;
firstName: string;
lastName: string;
username: string;
};
}) => Promise<User>;

View File

@@ -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<typeof GetUserSchema> = 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;

View File

@@ -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<typeof GetUserSchema> = 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;

View File

@@ -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<z.infer<typeof FollowInfoSchema>[]> => {
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;

View File

@@ -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<z.infer<typeof FollowInfoSchema>[]> => {
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;

View File

@@ -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;
};

View File

@@ -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<typeof FollowInfoSchema>;
type User = z.infer<typeof GetUserSchema>;
export type UserFollowService = (args: {
followerId: string;
followingId: string;
}) => Promise<UserFollow | null>;
export type UpdateUserProfileById = (args: {
userId: string;
data: { bio: string };
}) => Promise<User>;
export type CheckIfUserIsFollowedBySessionUser = (args: {
followerId: string;
followingId: string;
}) => Promise<boolean>;
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<User>;