Move next js project to archive (#207)

This commit is contained in:
Aaron Po
2026-04-20 02:30:25 -04:00
committed by GitHub
parent 92ec16ce93
commit d47e3ed7f0
347 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
/* 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 createNewUserService: 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 deleteUserService: 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 findUserByUsernameService: 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 findUserByEmailService: 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 findUserByIdService: 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 sendConfirmationEmailService: 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 sendResetPasswordEmailService: 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 confirmUserService: UpdateUserToBeConfirmedById = async ({ userId }) => {
return DBClient.instance.user.update({
where: { id: userId },
data: { accountIsVerified: true, updatedAt: new Date() },
select: userSelect,
});
};
export const updateUserPasswordService: 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 updateUserService: 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 findUserByEmailService({ email: data.email });
if (emailIsTaken) {
throw new ServerError('Email is already taken', 400);
}
await sendConfirmationEmailService({
userId,
username: data.username,
email: data.email,
});
}
if (updatedFields.username) {
const usernameIsTaken = await findUserByUsernameService({ 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

@@ -0,0 +1,84 @@
import { validateEmailRequest } from '@/requests/users/auth';
import validateUsernameRequest from '@/requests/users/profile/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,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,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>;