Refactor: begin reorganizing services dir.

- Renamed files and directories to reflect the new structure
- Moved comment-related services to the 'comments' directory
- Moved image-related services to the 'images' directory
- Moved like-related services to the 'likes' directory
- Moved post-related services to the 'posts' directory
- Moved user-related services to the 'users' directory
This commit is contained in:
Aaron William Po
2023-12-10 14:11:49 -05:00
parent 830e9dc845
commit fd641c36ab
163 changed files with 177 additions and 177 deletions

View File

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,37 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,13 @@
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,84 @@
import validateEmailRequest from '@/requests/User/validateEmailRequest';
import validateUsernameRequest from '@/requests/validateUsernameRequest';
import sub from 'date-fns/sub';
import { z } from 'zod';
const MINIMUM_DATE_OF_BIRTH = sub(new Date(), { years: 19 });
const NAME_REGEX =
/^[a-zA-ZàáâäãåąčćęèéêëėįìíîïłńòóôöõøùúûüųūÿýżźçčšžÀÁÂÄÃÅĄĆČĖĘÈÉÊËÌÍÎÏĮŁŃÒÓÔÖÕØÙÚÛÜŲŪŸÝŻŹÑßÇŒÆČŠŽðæ ,.'-]+$/u;
export const BaseCreateUserSchema = z.object({
password: z
.string()
.min(8, { message: 'Password must be at least 8 characters.' })
.refine((password) => /[A-Z]/.test(password), {
message: 'Password must contain at least one uppercase letter.',
})
.refine((password) => /[0-9]/.test(password), {
message: 'Password must contain at least one number.',
})
.refine((password) => /[^a-zA-Z0-9]/.test(password), {
message: 'Password must contain at least one special character.',
}),
confirmPassword: z.string(),
firstName: z
.string()
.min(1, { message: 'First name must not be empty.' })
.max(20, { message: 'First name must be less than 20 characters.' })
.refine((firstName) => NAME_REGEX.test(firstName), {
message: 'First name must only contain letters or hyphens.',
}),
lastName: z
.string()
.min(1, { message: 'Last name must not be empty.' })
.max(20, { message: 'Last name must be less than 20 characters.' })
.refine((lastName) => NAME_REGEX.test(lastName), {
message: 'Last name must only contain letters.',
}),
dateOfBirth: z
.string()
.refine((val) => !Number.isNaN(Date.parse(val)), {
message: 'Date is invalid.',
})
.refine((dateOfBirth) => new Date(dateOfBirth) <= MINIMUM_DATE_OF_BIRTH, {
message: 'You must be at least 19 years old to register.',
}),
username: z
.string()
.min(1, { message: 'Username must not be empty.' })
.max(20, { message: 'Username must be less than 20 characters.' }),
email: z.string().email({ message: 'Email must be a valid email address.' }),
});
export const CreateUserValidationSchema = BaseCreateUserSchema.refine(
(data) => data.password === data.confirmPassword,
{ message: 'Passwords do not match.', path: ['confirmPassword'] },
);
export const CreateUserValidationSchemaWithUsernameAndEmailCheck =
BaseCreateUserSchema.extend({
email: z
.string()
.email({ message: 'Email must be a valid email address.' })
.refine(async (email) => validateEmailRequest(email), {
message: 'Email is already taken.',
}),
username: z
.string()
.min(1, { message: 'Username must not be empty.' })
.max(20, { message: 'Username must be less than 20 characters.' })
.refine(async (username) => validateUsernameRequest(username), {
message: 'Username is already taken.',
}),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
});
export const UpdatePasswordSchema = BaseCreateUserSchema.pick({
password: true,
confirmPassword: true,
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
});

View File

@@ -0,0 +1,10 @@
import { BaseCreateUserSchema } from './CreateUserValidationSchemas';
const EditUserSchema = BaseCreateUserSchema.pick({
username: true,
email: true,
firstName: true,
lastName: true,
});
export default EditUserSchema;

View File

@@ -0,0 +1,19 @@
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
import { z } from 'zod';
const GetUserSchema = z.object({
id: z.string().cuid(),
username: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date().nullable(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.coerce.date(),
accountIsVerified: z.boolean(),
role: z.enum(['USER', 'ADMIN']),
bio: z.string().nullable(),
userAvatar: ImageQueryValidationSchema.nullable(),
});
export default GetUserSchema;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
const LoginValidationSchema = z.object({
username: z
.string({
required_error: 'Username is required.',
invalid_type_error: 'Username must be a string.',
})
.min(1, { message: 'Username is required.' }),
password: z
.string({
required_error: 'Password is required.',
invalid_type_error: 'Password must be a string.',
})
.min(1, { message: 'Password is required.' }),
});
export default LoginValidationSchema;

View File

@@ -0,0 +1,11 @@
import GetUserSchema from '@/services/users/User/schema/GetUserSchema';
const PublicUserSchema = GetUserSchema.pick({
id: true,
name: true,
createdAt: true,
username: true,
role: true,
});
export default PublicUserSchema;

View File

@@ -0,0 +1,7 @@
import z from 'zod';
const TokenValidationSchema = z.object({
token: z.string(),
});
export default TokenValidationSchema;

View File

@@ -0,0 +1,29 @@
import { z } from 'zod';
const UpdateProfileSchema = z.object({
bio: z.string(),
userAvatar: z
.instanceof(typeof FileList !== 'undefined' ? FileList : Object)
.refine((fileList) => fileList instanceof FileList, {
message: 'You must submit this form in a web browser.',
})
.refine((fileList) => [...(fileList as FileList)].length <= 1, {
message: 'You must upload only one or zero files.',
})
.refine(
(fileList) =>
[...(fileList as FileList)]
.map((file) => file.type)
.every((fileType) => fileType.startsWith('image/')),
{ message: 'You must upload only images.' },
)
.refine(
(fileList) =>
[...(fileList as FileList)]
.map((file) => file.size)
.every((fileSize) => fileSize < 15 * 1024 * 1024),
{ message: 'You must upload images smaller than 15MB.' },
),
});
export default UpdateProfileSchema;

View File

@@ -0,0 +1,29 @@
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

@@ -0,0 +1,30 @@
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,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<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

@@ -0,0 +1,37 @@
import GetUserSchema from '@/services/users/User/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

@@ -0,0 +1,55 @@
import DBClient from '@/prisma/DBClient';
import GetUserSchema from '@/services/users/User/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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,27 @@
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,9 @@
import GetUserSchema from '@/services/users/User/schema/GetUserSchema';
const FollowInfoSchema = GetUserSchema.pick({
userAvatar: true,
id: true,
username: true,
});
export default FollowInfoSchema;