mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-16 02:24:44 +00:00
Move next js project to archive (#207)
This commit is contained in:
308
archive/next-js-web-app/src/services/users/auth/index.ts
Normal file
308
archive/next-js-web-app/src/services/users/auth/index.ts
Normal 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;
|
||||
};
|
||||
@@ -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'],
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseCreateUserSchema } from './CreateUserValidationSchemas';
|
||||
|
||||
const EditUserSchema = BaseCreateUserSchema.pick({
|
||||
username: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
});
|
||||
|
||||
export default EditUserSchema;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import z from 'zod';
|
||||
|
||||
const TokenValidationSchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export default TokenValidationSchema;
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user