mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #43 from aaronpo97/password-and-security
Account security updates
This commit is contained in:
@@ -4,6 +4,7 @@ const nextConfig = {
|
|||||||
images: {
|
images: {
|
||||||
domains: ['picsum.photos', 'res.cloudinary.com'],
|
domains: ['picsum.photos', 'res.cloudinary.com'],
|
||||||
},
|
},
|
||||||
|
swcMinify: true,
|
||||||
};
|
};
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
|
|||||||
87
package-lock.json
generated
87
package-lock.json
generated
@@ -73,6 +73,7 @@
|
|||||||
"eslint-config-next": "^13.3.4",
|
"eslint-config-next": "^13.3.4",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"onchange": "^7.1.0",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
@@ -202,6 +203,18 @@
|
|||||||
"node": ">=6.9.0"
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -8288,6 +8301,30 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/onchange": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/onetime": {
|
"node_modules/onetime": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
@@ -10741,6 +10778,15 @@
|
|||||||
"node": "*"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tree-kill": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tree-node-cli": {
|
"node_modules/tree-node-cli": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
||||||
@@ -11669,6 +11715,18 @@
|
|||||||
"regenerator-runtime": "^0.13.11"
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@cspotcode/source-map-support": {
|
"@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -17455,6 +17513,29 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"onchange": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"onetime": {
|
"onetime": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
@@ -19181,6 +19262,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
|
"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==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"tree-node-cli": {
|
"tree-node-cli": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "npx prettier . --write",
|
"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"
|
"seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/ejs": "^3.1.2",
|
|
||||||
"@types/jsonwebtoken": "^9.0.2",
|
"@types/jsonwebtoken": "^9.0.2",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "^4.14.194",
|
||||||
"@types/mapbox__mapbox-sdk": "^0.13.4",
|
"@types/mapbox__mapbox-sdk": "^0.13.4",
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||||
|
"onchange": "^7.1.0",
|
||||||
"prisma": "^4.13.0",
|
"prisma": "^4.13.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"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;
|
||||||
@@ -67,8 +67,8 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
|
|||||||
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* If the comments are loading, show a loading component. Otherwise, show
|
* If the comments are loading, show a loading component. Otherwise, show the
|
||||||
* the comments.
|
* comments.
|
||||||
*/
|
*/
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<div className="card bg-base-300 pb-6">
|
<div className="card bg-base-300 pb-6">
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const BeerRecommendationsSection: FC<{
|
|||||||
|
|
||||||
const { ref: penultimateBeerPostRef } = useInView({
|
const { ref: penultimateBeerPostRef } = useInView({
|
||||||
/**
|
/**
|
||||||
* When the last beer post comes into view, call setSize from
|
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||||
* useBeerPostsByBrewery to load more beer posts.
|
* load more beer posts.
|
||||||
*/
|
*/
|
||||||
onChange: (visible) => {
|
onChange: (visible) => {
|
||||||
if (!visible || isAtEnd) return;
|
if (!visible || isAtEnd) return;
|
||||||
@@ -46,9 +46,8 @@ const BeerRecommendationsSection: FC<{
|
|||||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a ref to the second last beer post in the list.
|
* Attach a ref to the second last beer post in the list. When it comes
|
||||||
* When it comes into view, the component will call
|
* into view, the component will call setSize to load more beer posts.
|
||||||
* setSize to load more beer posts.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,8 +85,8 @@ const BeerRecommendationsSection: FC<{
|
|||||||
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* If there are more beer posts to load, show a loading component
|
* If there are more beer posts to load, show a loading component with a
|
||||||
* with a skeleton loader and a loading spinner.
|
* skeleton loader and a loading spinner.
|
||||||
*/
|
*/
|
||||||
!!isLoadingMore && !isAtEnd && (
|
!!isLoadingMore && !isAtEnd && (
|
||||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
|||||||
});
|
});
|
||||||
const { ref: penultimateBeerPostRef } = useInView({
|
const { ref: penultimateBeerPostRef } = useInView({
|
||||||
/**
|
/**
|
||||||
* When the last beer post comes into view, call setSize from
|
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||||
* useBeerPostsByBrewery to load more beer posts.
|
* load more beer posts.
|
||||||
*/
|
*/
|
||||||
onChange: (visible) => {
|
onChange: (visible) => {
|
||||||
if (!visible || isAtEnd) return;
|
if (!visible || isAtEnd) return;
|
||||||
@@ -60,9 +60,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
|||||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a ref to the second last beer post in the list.
|
* Attach a ref to the second last beer post in the list. When it comes
|
||||||
* When it comes into view, the component will call
|
* into view, the component will call setSize to load more beer posts.
|
||||||
* setSize to load more beer posts.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,8 +90,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
|||||||
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* If there are more beer posts to load, show a loading component
|
* If there are more beer posts to load, show a loading component with a
|
||||||
* with a skeleton loader and a loading spinner.
|
* skeleton loader and a loading spinner.
|
||||||
*/
|
*/
|
||||||
!!isLoadingMore && !isAtEnd && (
|
!!isLoadingMore && !isAtEnd && (
|
||||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const LoginForm = () => {
|
|||||||
await mutate!();
|
await mutate!();
|
||||||
toast.remove(loadingToast);
|
toast.remove(loadingToast);
|
||||||
toast.success('Logged in!');
|
toast.success('Logged in!');
|
||||||
await router.push(`/user/current`);
|
await router.push(`/users/current`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.remove(loadingToast);
|
toast.remove(loadingToast);
|
||||||
createErrorToast(error);
|
createErrorToast(error);
|
||||||
|
|||||||
@@ -68,9 +68,8 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
|||||||
const isLastComment = index === comments.length - 1;
|
const isLastComment = index === comments.length - 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a ref to the last comment in the list. When it comes
|
* Attach a ref to the last comment in the list. When it comes into view, the
|
||||||
* into view, the component will call setSize to load more
|
* component will call setSize to load more comments.
|
||||||
* comments.
|
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -89,17 +88,16 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
|||||||
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* If there are more comments to load, show a loading component
|
* If there are more comments to load, show a loading component with a
|
||||||
* with a skeleton loader and a loading spinner.
|
* skeleton loader and a loading spinner.
|
||||||
*/
|
*/
|
||||||
!!isLoadingMore && <LoadingComponent length={pageSize} />
|
!!isLoadingMore && <LoadingComponent length={pageSize} />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* If the user has scrolled to the end of the comments, show a
|
* If the user has scrolled to the end of the comments, show a button that
|
||||||
* button that will scroll them back to the top of the comments
|
* will scroll them back to the top of the comments section.
|
||||||
* section.
|
|
||||||
*/
|
*/
|
||||||
!!isAtEnd && (
|
!!isAtEnd && (
|
||||||
<div className="flex h-20 items-center justify-center text-center">
|
<div className="flex h-20 items-center justify-center text-center">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { NextApiRequest } from 'next';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const BasicUserInfoSchema = z.object({
|
export const BasicUserInfoSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,31 @@ import { BasicUserInfoSchema } from '@/config/auth/types';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { CONFIRMATION_TOKEN_SECRET } from '../env';
|
import { CONFIRMATION_TOKEN_SECRET } from '../env';
|
||||||
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
type User = z.infer<typeof BasicUserInfoSchema>;
|
export const generateConfirmationToken = (user: z.infer<typeof BasicUserInfoSchema>) => {
|
||||||
|
return jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '3m' });
|
||||||
export const generateConfirmationToken = (user: User) => {
|
|
||||||
const token = jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '30m' });
|
|
||||||
return token;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyConfirmationToken = (token: string) => {
|
export const verifyConfirmationToken = async (token: string) => {
|
||||||
const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET);
|
try {
|
||||||
|
const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET);
|
||||||
|
|
||||||
const parsed = BasicUserInfoSchema.safeParse(decoded);
|
const parsed = BasicUserInfoSchema.safeParse(decoded);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === 'jwt expired') {
|
||||||
|
throw new ServerError(
|
||||||
|
'Your confirmation token is expired. Please generate a new one.',
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ServerError('Something went wrong', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed.data;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ import { useState, useEffect } from 'react';
|
|||||||
*/
|
*/
|
||||||
const useMediaQuery = (query: `(${string})`) => {
|
const useMediaQuery = (query: `(${string})`) => {
|
||||||
/**
|
/**
|
||||||
* Initialize the matches state variable to false. This is updated whenever the
|
* Initialize the matches state variable to false. This is updated whenever the viewport
|
||||||
* viewport size changes (i.e. when the component is mounted and when the window is
|
* size changes (i.e. when the component is mounted and when the window is resized)
|
||||||
* resized)
|
|
||||||
*/
|
*/
|
||||||
const [matches, setMatches] = useState(false);
|
const [matches, setMatches] = useState(false);
|
||||||
|
|
||||||
@@ -35,8 +34,8 @@ const useMediaQuery = (query: `(${string})`) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a resize event listener to the window object, and update the `matches`
|
* Add a resize event listener to the window object, and update the `matches` state
|
||||||
* state variable whenever the viewport size changes.
|
* variable whenever the viewport size changes.
|
||||||
*/
|
*/
|
||||||
const listener = () => setMatches(media.matches);
|
const listener = () => setMatches(media.matches);
|
||||||
window.addEventListener('resize', listener);
|
window.addEventListener('resize', listener);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Head from 'next/head';
|
|||||||
import AccountInfo from '@/components/Account/AccountInfo';
|
import AccountInfo from '@/components/Account/AccountInfo';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import UserContext from '@/contexts/UserContext';
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import Security from '@/components/Account/Security';
|
||||||
|
|
||||||
const AccountPage: NextPage = () => {
|
const AccountPage: NextPage = () => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
@@ -50,7 +51,10 @@ const AccountPage: NextPage = () => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<AccountInfo />
|
<AccountInfo />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>Content 3</Tab.Panel>
|
<Tab.Panel>
|
||||||
|
<Security />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>Your posts!</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ const router = createRouter<
|
|||||||
|
|
||||||
router
|
router
|
||||||
.delete(
|
.delete(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfCommentOwner,
|
checkIfCommentOwner,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
)
|
)
|
||||||
.put(
|
.put(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
bodySchema: CreateCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const router = createRouter<
|
|||||||
router.post(
|
router.post(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
bodySchema: CreateCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
createComment,
|
createComment,
|
||||||
@@ -88,7 +88,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({
|
querySchema: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
page_size: z.coerce.number().int().positive(),
|
page_size: z.coerce.number().int().positive(),
|
||||||
page_num: z.coerce.number().int().positive(),
|
page_num: z.coerce.number().int().positive(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ const router = createRouter<
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
sendLikeRequest,
|
sendLikeRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
getLikeCount,
|
getLikeCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const router = createRouter<
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
checkIfLiked,
|
checkIfLiked,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const getBeerRecommendationsRequest = async (
|
|||||||
router.get(
|
router.get(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({
|
querySchema: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
page_num: z.string().regex(/^[0-9]+$/),
|
page_num: z.string().regex(/^[0-9]+$/),
|
||||||
page_size: z.string().regex(/^[0-9]+$/),
|
page_size: z.string().regex(/^[0-9]+$/),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const router = createRouter<
|
|||||||
router.post(
|
router.post(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
bodySchema: CreateCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
createComment,
|
createComment,
|
||||||
@@ -98,7 +98,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({
|
querySchema: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
page_size: z.coerce.number().int().positive(),
|
page_size: z.coerce.number().int().positive(),
|
||||||
page_num: z.coerce.number().int().positive(),
|
page_num: z.coerce.number().int().positive(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ const router = createRouter<
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
sendLikeRequest,
|
sendLikeRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
getLikeCount,
|
getLikeCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ router.get(
|
|||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({
|
querySchema: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
checkIfLiked,
|
checkIfLiked,
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ const router = createRouter<
|
|||||||
|
|
||||||
router
|
router
|
||||||
.delete(
|
.delete(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfCommentOwner,
|
checkIfCommentOwner,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
)
|
)
|
||||||
.put(
|
.put(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
bodySchema: CreateCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ router.put(
|
|||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({
|
validateRequest({
|
||||||
bodySchema: EditUserSchema,
|
bodySchema: EditUserSchema,
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
}),
|
}),
|
||||||
checkIfUserCanEditUser,
|
checkIfUserCanEditUser,
|
||||||
editUser,
|
editUser,
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ const confirmUser = async (req: ConfirmUserRequest, res: NextApiResponse) => {
|
|||||||
const { token } = req.query;
|
const { token } = req.query;
|
||||||
|
|
||||||
const user = req.user!;
|
const user = req.user!;
|
||||||
const { id } = verifyConfirmationToken(token);
|
const { id } = await verifyConfirmationToken(token);
|
||||||
|
|
||||||
|
if (user.accountIsVerified) {
|
||||||
|
throw new ServerError('Your account is already verified.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.id !== id) {
|
if (user.id !== id) {
|
||||||
throw new ServerError('Could not confirm user.', 401);
|
throw new ServerError('Could not confirm user.', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.accountIsVerified) {
|
|
||||||
throw new ServerError('User is already verified.', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUserToBeConfirmedById(id);
|
await updateUserToBeConfirmedById(id);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
|
|||||||
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;
|
||||||
32
src/pages/api/users/resend-confirmation.ts
Normal file
32
src/pages/api/users/resend-confirmation.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import sendConfirmationEmail from '@/services/User/sendConfirmationEmail';
|
||||||
|
|
||||||
|
const resendConfirmation = async (
|
||||||
|
req: UserExtendedNextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) => {
|
||||||
|
const user = req.user!;
|
||||||
|
|
||||||
|
await sendConfirmationEmail(user);
|
||||||
|
res.status(200).json({
|
||||||
|
message: `Resent the confirmation email for ${user.username}.`,
|
||||||
|
statusCode: 200,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
UserExtendedNextApiRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(getCurrentUser, resendConfirmation);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
5
src/pages/users/[id].tsx
Normal file
5
src/pages/users/[id].tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
const UserInfoPage: FC = () => null;
|
||||||
|
|
||||||
|
export default UserInfoPage;
|
||||||
125
src/pages/users/confirm.tsx
Normal file
125
src/pages/users/confirm.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { FC, useContext, useState } from 'react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const useSendConfirmUserRequest = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const token = router.query.token as string | undefined;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(`/api/users/confirm?token=${token}`, async (url) => {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token must be provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, error: error as unknown };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmUserPage: FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { error, data } = useSendConfirmUserRequest();
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const needsToLogin =
|
||||||
|
error instanceof Error && error.message === 'Unauthorized' && !user;
|
||||||
|
const tokenExpired = error instanceof Error && error.message === 'Unauthorized' && user;
|
||||||
|
const [confirmationResent, setConfirmationResent] = useState(false);
|
||||||
|
|
||||||
|
if (user?.accountIsVerified) {
|
||||||
|
router.push('/users/current');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
router.push('/users/current');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsToLogin) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Confirm User | The Biergarten App</title>
|
||||||
|
</Head>
|
||||||
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
|
<p className="text-center text-xl font-bold">
|
||||||
|
Please login to confirm your account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tokenExpired) {
|
||||||
|
const onClick = async () => {
|
||||||
|
const loadingToast = toast.loading('Resending your confirmation email.');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users/resend-confirmation', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Something went wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
toast.success('Sent a new confirmation email.');
|
||||||
|
|
||||||
|
setConfirmationResent(true);
|
||||||
|
} catch (err) {
|
||||||
|
createErrorToast(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Confirm User | The Biergarten App</title>
|
||||||
|
</Head>
|
||||||
|
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||||
|
{!confirmationResent ? (
|
||||||
|
<>
|
||||||
|
<p className="text-center text-2xl font-bold">
|
||||||
|
Your confirmation token is expired.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="btn-outline btn-sm btn normal-case"
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Click here to request a new token.
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-center text-2xl font-bold">
|
||||||
|
Resent your confirmation link.
|
||||||
|
</p>
|
||||||
|
<p className="font-xl text-center">Please check your email.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmUserPage;
|
||||||
@@ -5,6 +5,7 @@ import UserContext from '@/contexts/UserContext';
|
|||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
const ProtectedPage: NextPage = () => {
|
const ProtectedPage: NextPage = () => {
|
||||||
const { user, isLoading } = useContext(UserContext);
|
const { user, isLoading } = useContext(UserContext);
|
||||||
@@ -17,22 +18,27 @@ const ProtectedPage: NextPage = () => {
|
|||||||
|
|
||||||
const isDesktop = useMediaQuery('(min-width: 768px)');
|
const isDesktop = useMediaQuery('(min-width: 768px)');
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-3 bg-primary text-center">
|
<>
|
||||||
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
<Head>
|
||||||
{user && !isLoading && (
|
<title>Hello, {user?.firstName}! | The Biergarten App</title>
|
||||||
<>
|
</Head>
|
||||||
<h1 className="text-2xl font-bold lg:text-7xl">
|
<div className="flex h-full flex-col items-center justify-center space-y-3 bg-primary text-center">
|
||||||
Good {isMorning && 'morning'}
|
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
||||||
{isAfternoon && 'afternoon'}
|
{user && !isLoading && (
|
||||||
{isEvening && 'evening'}
|
<>
|
||||||
{`, ${user?.firstName}!`}
|
<h1 className="text-2xl font-bold lg:text-7xl">
|
||||||
</h1>
|
Good {isMorning && 'morning'}
|
||||||
<h2 className="text-xl font-bold lg:text-4xl">
|
{isAfternoon && 'afternoon'}
|
||||||
Welcome to the Biergarten App!
|
{isEvening && 'evening'}
|
||||||
</h2>
|
{`, ${user?.firstName}!`}
|
||||||
</>
|
</h1>
|
||||||
)}
|
<h2 className="text-xl font-bold lg:text-4xl">
|
||||||
</div>
|
Welcome to the Biergarten App!
|
||||||
|
</h2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
firstName String
|
firstName String
|
||||||
lastName String
|
lastName String
|
||||||
@@ -36,7 +36,7 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeerPost {
|
model BeerPost {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
ibu Float
|
ibu Float
|
||||||
abv Float
|
abv Float
|
||||||
@@ -55,7 +55,7 @@ model BeerPost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeerPostLike {
|
model BeerPostLike {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
||||||
beerPostId String
|
beerPostId String
|
||||||
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
||||||
@@ -65,7 +65,7 @@ model BeerPostLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BreweryPostLike {
|
model BreweryPostLike {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
||||||
breweryPostId String
|
breweryPostId String
|
||||||
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
||||||
@@ -75,7 +75,7 @@ model BreweryPostLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeerComment {
|
model BeerComment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
rating Int
|
rating Int
|
||||||
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
||||||
beerPostId String
|
beerPostId String
|
||||||
@@ -87,7 +87,7 @@ model BeerComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeerType {
|
model BeerType {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
@@ -97,7 +97,7 @@ model BeerType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Location {
|
model Location {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
city String
|
city String
|
||||||
stateOrProvince String?
|
stateOrProvince String?
|
||||||
country String?
|
country String?
|
||||||
@@ -109,7 +109,7 @@ model Location {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BreweryPost {
|
model BreweryPost {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
location Location @relation(fields: [locationId], references: [id])
|
location Location @relation(fields: [locationId], references: [id])
|
||||||
locationId String @unique
|
locationId String @unique
|
||||||
@@ -126,7 +126,7 @@ model BreweryPost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BreweryComment {
|
model BreweryComment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
rating Int
|
rating Int
|
||||||
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
||||||
breweryPostId String
|
breweryPostId String
|
||||||
@@ -138,7 +138,7 @@ model BreweryComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeerImage {
|
model BeerImage {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
||||||
beerPostId String
|
beerPostId String
|
||||||
path String
|
path String
|
||||||
@@ -151,7 +151,7 @@ model BeerImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BreweryImage {
|
model BreweryImage {
|
||||||
id String @id @default(uuid())
|
id String @id @default(cuid())
|
||||||
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
||||||
breweryPostId String
|
breweryPostId String
|
||||||
path String
|
path String
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
|
const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
|
||||||
beerPostId: z.string().uuid(),
|
beerPostId: z.string().cuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendCreateBeerCommentRequest = async ({
|
const sendCreateBeerCommentRequest = async ({
|
||||||
|
|||||||
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;
|
||||||
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
|||||||
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({
|
const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().cuid(),
|
||||||
beerPostId: z.string().uuid(),
|
beerPostId: z.string().cuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewBeerComment = async ({
|
const createNewBeerComment = async ({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import beerPostQueryResult from './schema/BeerPostQueryResult';
|
|||||||
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
||||||
|
|
||||||
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().cuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewBeerPost = async ({
|
const createNewBeerPost = async ({
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ const CreateBeerPostValidationSchema = z.object({
|
|||||||
required_error: 'Type id is required.',
|
required_error: 'Type id is required.',
|
||||||
invalid_type_error: 'Type id must be a string.',
|
invalid_type_error: 'Type id must be a string.',
|
||||||
})
|
})
|
||||||
.uuid({ message: 'Invalid type id.' }),
|
.cuid({ message: 'Invalid type id.' }),
|
||||||
breweryId: z
|
breweryId: z
|
||||||
.string({
|
.string({
|
||||||
required_error: 'Brewery id is required.',
|
required_error: 'Brewery id is required.',
|
||||||
invalid_type_error: 'Brewery id must be a string.',
|
invalid_type_error: 'Brewery id must be a string.',
|
||||||
})
|
})
|
||||||
.uuid({ message: 'Invalid brewery id.' }),
|
.cuid({ message: 'Invalid brewery id.' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default CreateBeerPostValidationSchema;
|
export default CreateBeerPostValidationSchema;
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import CreateBeerPostValidationSchema from './CreateBeerPostValidationSchema';
|
|||||||
const EditBeerPostValidationSchema = CreateBeerPostValidationSchema.omit({
|
const EditBeerPostValidationSchema = CreateBeerPostValidationSchema.omit({
|
||||||
breweryId: true,
|
breweryId: true,
|
||||||
typeId: true,
|
typeId: true,
|
||||||
}).extend({ id: z.string().uuid() });
|
}).extend({ id: z.string().cuid() });
|
||||||
|
|
||||||
export default EditBeerPostValidationSchema;
|
export default EditBeerPostValidationSchema;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
|||||||
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().cuid(),
|
||||||
breweryPostId: z.string().uuid(),
|
breweryPostId: z.string().cuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewBreweryComment = async ({
|
const createNewBreweryComment = async ({
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const createNewUser = async ({
|
|||||||
dateOfBirth: true,
|
dateOfBirth: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
accountIsVerified: true,
|
accountIsVerified: true,
|
||||||
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const findUserById = async (id: string) => {
|
|||||||
dateOfBirth: true,
|
dateOfBirth: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
accountIsVerified: true,
|
accountIsVerified: true,
|
||||||
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,3 +69,11 @@ export const CreateUserValidationSchemaWithUsernameAndEmailCheck =
|
|||||||
message: 'Passwords do not match.',
|
message: 'Passwords do not match.',
|
||||||
path: ['confirmPassword'],
|
path: ['confirmPassword'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UpdatePasswordSchema = BaseCreateUserSchema.pick({
|
||||||
|
password: true,
|
||||||
|
confirmPassword: true,
|
||||||
|
}).refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match.',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const GetUserSchema = z.object({
|
const GetUserSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.coerce.date().optional(),
|
updatedAt: z.coerce.date().nullable(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
|
|||||||
|
|
||||||
const subject = 'Confirm your email';
|
const subject = 'Confirm your email';
|
||||||
const name = username;
|
const name = username;
|
||||||
const url = `${BASE_URL}/api/users/confirm?token=${confirmationToken}`;
|
const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`;
|
||||||
const address = email;
|
const address = email;
|
||||||
|
|
||||||
const html = render(Welcome({ name, url, subject })!);
|
const html = render(Welcome({ name, url, subject })!);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const updateUserToBeConfirmedById = async (id: string) => {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
firstName: true,
|
firstName: true,
|
||||||
lastName: true,
|
lastName: true,
|
||||||
|
updatedAt: true,
|
||||||
dateOfBirth: true,
|
dateOfBirth: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const CommentQueryResult = z.object({
|
const CommentQueryResult = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
content: z.string().min(1).max(500),
|
content: z.string().min(1).max(500),
|
||||||
rating: z.number().int().min(1).max(5),
|
rating: z.number().int().min(1).max(5),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
postedBy: z.object({
|
postedBy: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().cuid(),
|
||||||
username: z.string().min(1).max(50),
|
username: z.string().min(1).max(50),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user