Restructure account api routes, fix update profile

This commit is contained in:
Aaron William Po
2023-12-02 22:42:07 -05:00
parent 0d23b02957
commit cf89dc92df
15 changed files with 366 additions and 103 deletions

View File

@@ -0,0 +1,118 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import { singleUploadMiddleware } from '@/config/multer/uploadMiddleware';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { NextHandler, createRouter } from 'next-connect';
import { z } from 'zod';
interface UpdateProfileRequest extends UserExtendedNextApiRequest {
file: Express.Multer.File;
body: {
bio: string;
};
}
interface UpdateUserProfileByIdParams {
id: string;
data: {
avatar: {
alt: string;
path: string;
caption: string;
};
};
}
const updateUserAvatarById = async ({ id, data }: UpdateUserProfileByIdParams) => {
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;
};
const checkIfUserCanUpdateProfile = async (
req: UpdateProfileRequest,
res: NextApiResponse,
next: NextHandler,
) => {
const user = req.user!;
if (user.id !== req.query.id) {
throw new ServerError('You can only update your own profile.', 403);
}
await next();
};
const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => {
const { file, user } = req;
await updateUserAvatarById({
id: user!.id,
data: {
avatar: { alt: file.originalname, path: file.path, caption: '' },
},
});
res.status(200).json({
message: 'User avatar updated successfully.',
statusCode: 200,
success: true,
});
};
const router = createRouter<
UpdateProfileRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
getCurrentUser,
checkIfUserCanUpdateProfile,
// @ts-expect-error
singleUploadMiddleware,
updateProfile,
);
const handler = router.handler();
export default handler;
export const config = { api: { bodyParser: false } };

View File

@@ -0,0 +1,89 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { NextHandler, createRouter } from 'next-connect';
import { z } from 'zod';
interface UpdateProfileRequest extends UserExtendedNextApiRequest {
body: {
bio: string;
};
}
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;
};
const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => {
const user = req.user!;
const { body } = req;
await updateUserProfileById({ id: user!.id, data: { bio: body.bio } });
res.status(200).json({
message: 'Profile updated successfully.',
statusCode: 200,
success: true,
});
};
const checkIfUserCanUpdateProfile = async (
req: UpdateProfileRequest,
res: NextApiResponse,
next: NextHandler,
) => {
const user = req.user!;
if (user.id !== req.query.id) {
throw new ServerError('You can only update your own profile.', 403);
}
await next();
};
const router = createRouter<
UpdateProfileRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
getCurrentUser,
checkIfUserCanUpdateProfile,
validateRequest({ bodySchema: z.object({ bio: z.string().max(1000) }) }),
updateProfile,
);
const handler = router.handler();
export default handler;

View File

@@ -1,23 +1,30 @@
import withPageAuthRequired from '@/util/withPageAuthRequired';
import { GetServerSideProps, NextPage } from 'next';
import { z } from 'zod';
import { useContext, useEffect } from 'react';
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { zodResolver } from '@hookform/resolvers/zod';
import { SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import toast from 'react-hot-toast';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import createErrorToast from '@/util/createErrorToast';
import UserAvatar from '@/components/Account/UserAvatar';
import { useContext, useEffect } from 'react';
import UserContext from '@/contexts/UserContext';
import UserAvatar from '@/components/Account/UserAvatar';
import UpdateProfileForm from '@/components/Account/UpdateProfileForm';
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
import Head from 'next/head';
import UpdateProfileSchema from '../../services/User/schema/UpdateProfileSchema';
import sendUpdateProfileRequest from '../../requests/Account/sendUpdateProfileRequest';
import UpdateProfileForm from '../../components/Account/UpdateProfileForm';
import UpdateProfileSchema from '@/services/User/schema/UpdateProfileSchema';
import sendUpdateUserAvatarRequest from '@/requests/Account/sendUpdateUserAvatarRequest';
import sendUpdateUserProfileRequest from '@/requests/Account/sendUpdateUserProfileRequest.ts';
import Spinner from '@/components/ui/Spinner';
const ProfilePage: NextPage = () => {
const { user, mutate: mutateUser } = useContext(UserContext);
@@ -28,31 +35,43 @@ const ProfilePage: NextPage = () => {
setValue,
formState: { errors, isSubmitting },
watch,
reset,
} = useForm<z.infer<typeof UpdateProfileSchema>>({
resolver: zodResolver(UpdateProfileSchema),
defaultValues: {
bio: user?.bio ?? '',
},
});
useEffect(() => {
if (!user || !user.bio) return;
if (!user || !user.bio) {
return;
}
setValue('bio', user.bio);
}, [user, setValue]);
const router = useRouter();
const onSubmit: SubmitHandler<z.infer<typeof UpdateProfileSchema>> = async (data) => {
try {
await sendUpdateProfileRequest(data);
const loadingToast = toast.loading('Updating profile...');
await new Promise((resolve) => {
setTimeout(resolve, 1000);
if (!user) {
throw new Error('User is not logged in.');
}
if (data.userAvatar instanceof FileList && data.userAvatar.length === 1) {
await sendUpdateUserAvatarRequest({
userId: user.id,
file: data.userAvatar[0],
});
}
await sendUpdateUserProfileRequest({
userId: user.id,
bio: data.bio,
});
toast.remove(loadingToast);
reset();
mutateUser!();
toast.success('Profile updated!');
await mutateUser!();
await router.push(`/users/${user.id}`);
toast.success('Profile updated.');
} catch (error) {
createErrorToast(error);
}
@@ -69,9 +88,11 @@ const ProfilePage: NextPage = () => {
const watchedInput = watch('userAvatar');
if (
!(watchedInput instanceof FileList) ||
watchedInput.length !== 1 ||
!watchedInput[0].type.startsWith('image/')
!(
watchedInput instanceof FileList &&
watchedInput.length === 1 &&
watchedInput[0].type.startsWith('image/')
)
) {
return '';
}
@@ -87,13 +108,13 @@ const ProfilePage: NextPage = () => {
<title>The Biergarten App || Update Your Profile</title>
<meta name="description" content="Update your user profile." />
</Head>
<div className="mt-20 flex flex-col items-center justify-center">
{user && (
<div className="my-10 flex flex-col items-center justify-center">
{user ? (
<div className="w-10/12 lg:w-7/12">
<div className="card">
<div className="card-body">
<div className="my-10 flex flex-col items-center justify-center">
<div className="my-5 h-52">
<div className="my-2 h-40 xl:my-5 xl:h-52">
<UserAvatar
user={{
...user,
@@ -113,17 +134,21 @@ const ProfilePage: NextPage = () => {
/>
</div>
<div className="text-center">
<h2 className="text-3xl font-bold">{user.username}</h2>
<h2 className="my-1 text-2xl font-bold xl:text-3xl">
{user.username}
</h2>
<div className="flex space-x-3 text-lg font-bold">
<div className="flex space-x-3 font-bold xl:text-lg">
<span>{followingCount} Following</span>
<span>{followerCount} Followers</span>
</div>
</div>
<div>
<p className="text-lg">
{watch('bio') || (
<p className="hyphens-auto text-center xl:text-lg">
{watch('bio') ? (
<span className="hyphens-manual">{watch('bio')}</span>
) : (
<span className="italic">Your bio will appear here.</span>
)}
</p>
@@ -141,6 +166,10 @@ const ProfilePage: NextPage = () => {
</div>
</div>
</div>
) : (
<div className="flex h-96 items-center justify-center">
<Spinner />
</div>
)}
</div>
</>

View File

@@ -11,6 +11,7 @@ import DeleteAccount from '@/components/Account/DeleteAccount';
import accountPageReducer from '@/reducers/accountPageReducer';
import UserAvatar from '@/components/Account/UserAvatar';
import UserPosts from '@/components/Account/UserPosts';
import UpdateProfileLink from '@/components/Account/UpdateProfileLink';
const AccountPage: NextPage = () => {
const { user } = useContext(UserContext);
@@ -57,6 +58,7 @@ const AccountPage: NextPage = () => {
</Tab.List>
<Tab.Panels>
<Tab.Panel className="h-full space-y-5">
<UpdateProfileLink />
<AccountInfo pageState={pageState} dispatch={dispatch} />
<Security pageState={pageState} dispatch={dispatch} />
<DeleteAccount pageState={pageState} dispatch={dispatch} />