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

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