mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
feat: implement change password
This commit is contained in:
@@ -4,6 +4,7 @@ const nextConfig = {
|
||||
images: {
|
||||
domains: ['picsum.photos', 'res.cloudinary.com'],
|
||||
},
|
||||
swcMinify: true,
|
||||
};
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
|
||||
77
package-lock.json
generated
77
package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"multer-storage-cloudinary": "^4.0.0",
|
||||
"next": "^13.3.4",
|
||||
"next-connect": "^1.0.0-next.3",
|
||||
"onchange": "^7.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.12.0",
|
||||
@@ -202,6 +203,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blakeembrey/deque": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz",
|
||||
"integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg=="
|
||||
},
|
||||
"node_modules/@blakeembrey/template": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz",
|
||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw=="
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -8288,6 +8299,28 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/onchange": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz",
|
||||
"integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==",
|
||||
"dependencies": {
|
||||
"@blakeembrey/deque": "^1.0.5",
|
||||
"@blakeembrey/template": "^1.0.0",
|
||||
"arg": "^4.1.3",
|
||||
"chokidar": "^3.3.1",
|
||||
"cross-spawn": "^7.0.1",
|
||||
"ignore": "^5.1.4",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"onchange": "dist/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/onchange/node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -10741,6 +10774,14 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-node-cli": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
||||
@@ -11669,6 +11710,16 @@
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
}
|
||||
},
|
||||
"@blakeembrey/deque": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz",
|
||||
"integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg=="
|
||||
},
|
||||
"@blakeembrey/template": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.1.0.tgz",
|
||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw=="
|
||||
},
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -17455,6 +17506,27 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"onchange": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz",
|
||||
"integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==",
|
||||
"requires": {
|
||||
"@blakeembrey/deque": "^1.0.5",
|
||||
"@blakeembrey/template": "^1.0.0",
|
||||
"arg": "^4.1.3",
|
||||
"chokidar": "^3.3.1",
|
||||
"cross-spawn": "^7.0.1",
|
||||
"ignore": "^5.1.4",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -19181,6 +19253,11 @@
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
|
||||
},
|
||||
"tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
|
||||
},
|
||||
"tree-node-cli": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "npx prettier . --write",
|
||||
"format-watch": "npx onchange \"**/*\" -- prettier --write --ignore-unknown {{changed}}",
|
||||
"seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -80,6 +81,7 @@
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-jsdoc": "^0.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"onchange": "^7.1.0",
|
||||
"prisma": "^4.13.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
|
||||
77
src/components/Account/Security.tsx
Normal file
77
src/components/Account/Security.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormTextInput from '../ui/forms/FormTextInput';
|
||||
|
||||
const Security: FunctionComponent = () => {
|
||||
const [editToggled, setEditToggled] = useState(false);
|
||||
const { register, handleSubmit, formState, reset } = useForm<
|
||||
z.infer<typeof UpdatePasswordSchema>
|
||||
>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
|
||||
await sendUpdatePasswordRequest(data)
|
||||
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8 w-full space-y-4">
|
||||
<div className="flex w-full items-center justify-between space-x-5">
|
||||
<div className="">
|
||||
<h1 className="text-lg font-bold">Change Your Password</h1>
|
||||
<p>Update your password to maintain the safety of your account.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={editToggled}
|
||||
onClick={() => setEditToggled((val) => !val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editToggled && (
|
||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="password">New Password</FormLabel>
|
||||
<FormError>{formState.errors.password?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
error={!!formState.errors.password}
|
||||
id="password"
|
||||
formValidationSchema={register('password')}
|
||||
/>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="password">Confirm Password</FormLabel>
|
||||
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
error={!!formState.errors.confirmPassword}
|
||||
id="password"
|
||||
formValidationSchema={register('confirmPassword')}
|
||||
/>
|
||||
|
||||
<button className="btn-primary btn mt-5" disabled={!editToggled} type="submit">
|
||||
Update
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Security;
|
||||
@@ -6,6 +6,7 @@ import Head from 'next/head';
|
||||
import AccountInfo from '@/components/Account/AccountInfo';
|
||||
import { useContext } from 'react';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import Security from '@/components/Account/Security';
|
||||
|
||||
const AccountPage: NextPage = () => {
|
||||
const { user } = useContext(UserContext);
|
||||
@@ -50,7 +51,10 @@ const AccountPage: NextPage = () => {
|
||||
<Tab.Panel>
|
||||
<AccountInfo />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Content 3</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Security />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Your posts!</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
|
||||
60
src/pages/api/users/edit-password.ts
Normal file
60
src/pages/api/users/edit-password.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { hashPassword } from '@/config/auth/passwordFns';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface UpdatePasswordRequest extends UserExtendedNextApiRequest {
|
||||
body: z.infer<typeof UpdatePasswordSchema>;
|
||||
}
|
||||
|
||||
const updatePassword = async (
|
||||
req: UpdatePasswordRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
const { password } = req.body;
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
const user = req.user!;
|
||||
const updatedUser: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
|
||||
data: { hash },
|
||||
where: { id: user.id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
dateOfBirth: true,
|
||||
accountIsVerified: true,
|
||||
},
|
||||
});
|
||||
res.json({
|
||||
message: 'Updated user password.',
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
payload: updatedUser,
|
||||
});
|
||||
};
|
||||
const router = createRouter<
|
||||
UpdatePasswordRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
router.put(
|
||||
validateRequest({ bodySchema: UpdatePasswordSchema }),
|
||||
getCurrentUser,
|
||||
updatePassword,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
export default handler;
|
||||
33
src/requests/User/sendUpdatePasswordRequest.ts
Normal file
33
src/requests/User/sendUpdatePasswordRequest.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
const sendUpdatePasswordRequest = async (data: z.infer<typeof UpdatePasswordSchema>) => {
|
||||
const response = await fetch('/api/users/edit-password', {
|
||||
body: JSON.stringify(data),
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const json = await response.json();
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('API response validation failed.');
|
||||
}
|
||||
|
||||
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('API payload validation failed.');
|
||||
}
|
||||
|
||||
return parsedPayload.data;
|
||||
};
|
||||
|
||||
export default sendUpdatePasswordRequest;
|
||||
@@ -31,6 +31,7 @@ const createNewUser = async ({
|
||||
dateOfBirth: true,
|
||||
createdAt: true,
|
||||
accountIsVerified: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -69,3 +69,11 @@ export const CreateUserValidationSchemaWithUsernameAndEmailCheck =
|
||||
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'],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ const GetUserSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date().optional(),
|
||||
updatedAt: z.coerce.date().nullable(),
|
||||
email: z.string().email(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user