| @@ -27,6 +27,7 @@ yarn-error.log* | |||
| # local env files | |||
| .env*.local | |||
| .env | |||
| # vercel | |||
| .vercel | |||
| @@ -0,0 +1,76 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import Link from 'next/link'; | |||
| import React from 'react'; | |||
| import { LOGIN_PAGE } from '../../../constants/pages'; | |||
| import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema'; | |||
| const ForgotPasswordForm = () => { | |||
| const handleSubmit = (values) => { | |||
| console.log('Values', values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: '', | |||
| }, | |||
| validationSchema: forgotPasswordSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| Forgot password | |||
| </Typography> | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="email" | |||
| label="Email" | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| Send email | |||
| </Button> | |||
| <Grid container justifyContent="center"> | |||
| <Link href={LOGIN_PAGE}>Back</Link> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default ForgotPasswordForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockForgotPasswordFormProps = { | |||
| base, | |||
| }; | |||
| @@ -0,0 +1,20 @@ | |||
| import ForgotPasswordForm from './ForgotPasswordForm'; | |||
| import { mockForgotPasswordFormProps } from './ForgotPasswordForm.mock'; | |||
| const obj = { | |||
| title: 'forms/ForgotPasswordForm', | |||
| component: ForgotPasswordForm, | |||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | |||
| argTypes: {}, | |||
| }; //eslint-disable-line | |||
| export default obj; | |||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | |||
| const Template = (args) => <ForgotPasswordForm {...args} />; | |||
| export const Base = Template.bind({}); | |||
| // More on args: https://storybook.js.org/docs/react/writing-stories/args | |||
| Base.args = { | |||
| ...mockForgotPasswordFormProps.base, | |||
| }; | |||
| @@ -0,0 +1,139 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| IconButton, | |||
| InputAdornment, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { signIn } from 'next-auth/react'; | |||
| import Link from 'next/link'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useState } from 'react'; | |||
| import { | |||
| BASE_PAGE, | |||
| FORGOT_PASSWORD_PAGE, | |||
| REGISTER_PAGE, | |||
| } from '../../../constants/pages'; | |||
| import { loginSchema } from '../../../schemas/loginSchema'; | |||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | |||
| const LoginForm = () => { | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const router = useRouter(); | |||
| const [error, setError] = useState({ hasError: false, errorMessage: '' }); | |||
| const submitHandler = async (values) => { | |||
| const result = await signIn('credentials', { | |||
| redirect: false, | |||
| username: values.username, | |||
| password: values.password, | |||
| }); | |||
| if (!result.error) { | |||
| router.replace(BASE_PAGE); | |||
| } else { | |||
| setError({ hasError: true, errorMessage: result.error }); | |||
| } | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| username: '', | |||
| password: '', | |||
| }, | |||
| validationSchema: loginSchema, | |||
| onSubmit: submitHandler, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| Login | |||
| </Typography> | |||
| {error.hasError && <ErrorMessageComponent error={error.errorMessage} />} | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="username" | |||
| label="Username" | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| label="Password" | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| Login | |||
| </Button> | |||
| <Grid container> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'left' } }} | |||
| > | |||
| <Link href={FORGOT_PASSWORD_PAGE}>Forgot your password?</Link> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'right' } }} | |||
| > | |||
| <Link href={REGISTER_PAGE}>Dont have an account?</Link> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default LoginForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockLoginFormProps = { | |||
| base, | |||
| }; | |||
| @@ -0,0 +1,20 @@ | |||
| import LoginForm from './LoginForm'; | |||
| import { mockLoginFormProps } from './LoginForm.mock'; | |||
| const obj = { | |||
| title: 'forms/LoginForm', | |||
| component: LoginForm, | |||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | |||
| argTypes: {}, | |||
| }; //eslint-disable-line | |||
| export default obj; | |||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | |||
| const Template = (args) => <LoginForm {...args} />; | |||
| export const Base = Template.bind({}); | |||
| // More on args: https://storybook.js.org/docs/react/writing-stories/args | |||
| Base.args = { | |||
| ...mockLoginFormProps.base, | |||
| }; | |||
| @@ -0,0 +1,190 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| IconButton, | |||
| InputAdornment, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import Link from 'next/link'; | |||
| import { useState } from 'react'; | |||
| import { FORGOT_PASSWORD_PAGE, LOGIN_PAGE } from '../../../constants/pages'; | |||
| import { createUser } from '../../../requests/accountRequests'; | |||
| import { registerSchema } from '../../../schemas/registerSchema'; | |||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | |||
| const RegisterForm = () => { | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const [showConfirmPassword, setShowConfirmPassword] = useState(false); | |||
| const handleClickShowConfirmPassword = () => | |||
| setShowConfirmPassword(!showConfirmPassword); | |||
| const handleMouseDownConfirmPassword = () => | |||
| setShowConfirmPassword(!showConfirmPassword); | |||
| const [error, setError] = useState({ hasError: false, errorMessage: '' }); | |||
| const submitHandler = async (values) => { | |||
| try { | |||
| const result = await createUser( | |||
| values.fullName, | |||
| values.username, | |||
| values.email, | |||
| values.password | |||
| ); | |||
| console.log(result); | |||
| } catch (error) { | |||
| setError({ hasError: true, errorMessage: error.message }); | |||
| } | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| fullName: '', | |||
| username: '', | |||
| email: '', | |||
| password: '', | |||
| confirmPassword: '', | |||
| }, | |||
| validationSchema: registerSchema, | |||
| onSubmit: submitHandler, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 10, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| Register | |||
| </Typography> | |||
| {error.hasError && <ErrorMessageComponent error={error.errorMessage} />} | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="fullName" | |||
| label="Full name" | |||
| margin="normal" | |||
| value={formik.values.fullName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.fullName && Boolean(formik.errors.fullName)} | |||
| helperText={formik.touched.fullName && formik.errors.fullName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="username" | |||
| label="Username" | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="email" | |||
| label="Email" | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| label="Password" | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <TextField | |||
| name="confirmPassword" | |||
| label="Confirm password" | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.confirmPassword} | |||
| onChange={formik.handleChange} | |||
| error={ | |||
| formik.touched.confirmPassword && | |||
| Boolean(formik.errors.confirmPassword) | |||
| } | |||
| helperText={ | |||
| formik.touched.confirmPassword && formik.errors.confirmPassword | |||
| } | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowConfirmPassword} | |||
| onMouseDown={handleMouseDownConfirmPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| Register | |||
| </Button> | |||
| <Grid container> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'left' } }} | |||
| > | |||
| <Link href={FORGOT_PASSWORD_PAGE}>Forgot your password?</Link> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'right' } }} | |||
| > | |||
| <Link href={LOGIN_PAGE}>Already have an account?</Link> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default RegisterForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockRegisterFormProps = { | |||
| base, | |||
| }; | |||
| @@ -0,0 +1,20 @@ | |||
| import RegisterForm from './RegisterForm'; | |||
| import { mockRegisterFormProps } from './RegisterForm.mock'; | |||
| const obj = { | |||
| title: 'forms/RegisterForm', | |||
| component: RegisterForm, | |||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | |||
| argTypes: {}, | |||
| }; //eslint-disable-line | |||
| export default obj; | |||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | |||
| const Template = (args) => <RegisterForm {...args} />; | |||
| export const Base = Template.bind({}); | |||
| // More on args: https://storybook.js.org/docs/react/writing-stories/args | |||
| Base.args = { | |||
| ...mockRegisterFormProps.base, | |||
| }; | |||
| @@ -0,0 +1,15 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Typography } from '@mui/material'; | |||
| const ErrorMessageComponent = ({ error }) => ( | |||
| <Typography variant="body1" color="error" my={2}> | |||
| {error} | |||
| </Typography> | |||
| ); | |||
| ErrorMessageComponent.propTypes = { | |||
| error: PropTypes.string.isRequired, | |||
| }; | |||
| export default ErrorMessageComponent; | |||
| @@ -1,13 +1,14 @@ | |||
| import BaseTemplate from './BaseTemplate'; | |||
| import { mockBaseTemplateProps } from './BaseTemplate.mocks'; | |||
| export default { | |||
| const obj = { | |||
| title: 'templates/BaseTemplate', | |||
| component: BaseTemplate, | |||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | |||
| argTypes: {}, | |||
| }; | |||
| }; //eslint-disable-line | |||
| export default obj; | |||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | |||
| const Template = (args) => <BaseTemplate {...args} />; | |||
| @@ -0,0 +1,5 @@ | |||
| export const BASE_PAGE = "/"; | |||
| export const LOGIN_PAGE = "/auth"; | |||
| export const PROFILE_PAGE = "/profile"; | |||
| export const REGISTER_PAGE = "/auth/register"; | |||
| export const FORGOT_PASSWORD_PAGE = "/auth/forgot-password"; | |||
| @@ -13,12 +13,27 @@ | |||
| "build-storybook": "build-storybook" | |||
| }, | |||
| "dependencies": { | |||
| "@emotion/react": "^11.10.0", | |||
| "@emotion/styled": "^11.10.0", | |||
| "@mui/codemod": "^5.8.7", | |||
| "@mui/icons-material": "^5.8.4", | |||
| "@mui/material": "^5.9.2", | |||
| "bcryptjs": "^2.4.3", | |||
| "date-fns": "^2.29.1", | |||
| "formik": "^2.2.9", | |||
| "mongodb": "^4.8.1", | |||
| "next": "12.2.3", | |||
| "next-auth": "^4.10.2", | |||
| "prop-types": "^15.8.1", | |||
| "react": "18.2.0", | |||
| "react-dom": "18.2.0" | |||
| "react-dom": "18.2.0", | |||
| "sass": "^1.54.0", | |||
| "yup": "^0.32.11" | |||
| }, | |||
| "devDependencies": { | |||
| "@babel/core": "^7.18.9", | |||
| "@babel/preset-env": "^7.18.9", | |||
| "@babel/preset-react": "^7.18.6", | |||
| "@commitlint/cli": "^17.0.3", | |||
| "@commitlint/config-conventional": "^17.0.3", | |||
| "@storybook/addon-actions": "^6.5.9", | |||
| @@ -30,6 +45,7 @@ | |||
| "@storybook/react": "^6.5.9", | |||
| "@storybook/testing-library": "^0.0.13", | |||
| "babel-loader": "^8.2.5", | |||
| "babel-plugin-import": "^1.13.5", | |||
| "cross-env": "^7.0.3", | |||
| "eslint": "8.21.0", | |||
| "eslint-config-next": "12.2.3", | |||
| @@ -0,0 +1,42 @@ | |||
| import NextAuth from 'next-auth'; | |||
| import Credentials from 'next-auth/providers/credentials'; | |||
| import { connectToDatabase } from '../../../utils/helpers/dbHelpers'; | |||
| import { verifyPassword } from '../../../utils/helpers/hashPasswordHelpers'; | |||
| export default NextAuth({ | |||
| session: { | |||
| jwt: true, | |||
| }, | |||
| providers: [ | |||
| Credentials({ | |||
| async authorize(credentials) { | |||
| const client = await connectToDatabase(); | |||
| const usersCollection = client.db().collection('users'); | |||
| const user = await usersCollection.findOne({ | |||
| username: credentials.username, | |||
| }); | |||
| if (!user) { | |||
| client.close(); | |||
| throw new Error('No user found!'); | |||
| } | |||
| const isValid = await verifyPassword( | |||
| credentials.password, | |||
| user.password | |||
| ); | |||
| if (!isValid) { | |||
| client.close(); | |||
| throw new Error('Could not log you in!'); | |||
| } | |||
| client.close(); | |||
| return { name: user.fullName }; | |||
| }, | |||
| }), | |||
| ], | |||
| }); | |||
| @@ -0,0 +1,51 @@ | |||
| import { connectToDatabase } from '../../../utils/helpers/dbHelpers'; | |||
| import { hashPassword } from '../../../utils/helpers/hashPasswordHelpers'; | |||
| async function handler(req, res) { | |||
| if (req.method !== 'POST') { | |||
| return; | |||
| } | |||
| const { fullName, username, email, password } = req.body; | |||
| if ( | |||
| !fullName || | |||
| !username || | |||
| !email || | |||
| !email.includes('@') || | |||
| !password || | |||
| password.trim().length < 7 | |||
| ) { | |||
| res.status(422).json({ | |||
| message: 'Invalid input ', | |||
| }); | |||
| return; | |||
| } | |||
| const client = await connectToDatabase(); | |||
| const db = client.db(); | |||
| const existingUser = await db | |||
| .collection('users') | |||
| .findOne({ $or: [{ email: email }, { username: username }] }); | |||
| if (existingUser) { | |||
| res.status(422).json({ message: 'User exists already!' }); | |||
| client.close(); | |||
| return; | |||
| } | |||
| const hashedPassword = await hashPassword(password); | |||
| const result = await db.collection('users').insertOne({ | |||
| fullName: fullName, | |||
| username: username, | |||
| email: email, | |||
| password: hashedPassword, | |||
| }); | |||
| res.status(201).json({ message: 'Created user!', result: result }); | |||
| client.close(); | |||
| } | |||
| export default handler; | |||
| @@ -0,0 +1,21 @@ | |||
| import { getSession } from 'next-auth/react'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useEffect } from 'react'; | |||
| import ForgotPasswordForm from '../../../components/forms/forgot-password/ForgotPasswordForm'; | |||
| import { BASE_PAGE } from '../../../constants/pages'; | |||
| const ForgotPasswordPage = () => { | |||
| const router = useRouter(); | |||
| useEffect(() => { | |||
| getSession().then((session) => { | |||
| if (session) { | |||
| router.replace(BASE_PAGE); | |||
| } | |||
| }); | |||
| }, [router]); | |||
| return <ForgotPasswordForm />; | |||
| }; | |||
| export default ForgotPasswordPage; | |||
| @@ -0,0 +1,21 @@ | |||
| import { getSession } from 'next-auth/react'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useEffect } from 'react'; | |||
| import LoginForm from '../../components/forms/login/LoginForm'; | |||
| import { BASE_PAGE } from '../../constants/pages'; | |||
| const AuthPage = () => { | |||
| const router = useRouter(); | |||
| useEffect(() => { | |||
| getSession().then((session) => { | |||
| if (session) { | |||
| router.replace(BASE_PAGE); | |||
| } | |||
| }); | |||
| }, [router]); | |||
| return <LoginForm />; | |||
| }; | |||
| export default AuthPage; | |||
| @@ -0,0 +1,21 @@ | |||
| import { getSession } from 'next-auth/react'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useEffect } from 'react'; | |||
| import RegisterForm from '../../../components/forms/register/RegisterForm'; | |||
| import { BASE_PAGE } from '../../../constants/pages'; | |||
| const RegisterPage = () => { | |||
| const router = useRouter(); | |||
| useEffect(() => { | |||
| getSession().then((session) => { | |||
| if (session) { | |||
| router.replace(BASE_PAGE); | |||
| } | |||
| }); | |||
| }, [router]); | |||
| return <RegisterForm />; | |||
| }; | |||
| export default RegisterPage; | |||
| @@ -1,69 +1,15 @@ | |||
| import Head from 'next/head'; | |||
| import Image from 'next/image'; | |||
| import styles from '../styles/Home.module.css'; | |||
| import { signOut } from 'next-auth/react'; | |||
| export default function Home() { | |||
| const Home = () => { | |||
| function logoutHandler() { | |||
| signOut(); | |||
| } | |||
| return ( | |||
| <div className={styles.container}> | |||
| <Head> | |||
| <title>Create Next App</title> | |||
| <meta name="description" content="Generated by create next app" /> | |||
| <link rel="icon" href="/favicon.ico" /> | |||
| </Head> | |||
| <main className={styles.main}> | |||
| <h1 className={styles.title}> | |||
| Welcome to <a href="https://nextjs.org">Next.js!</a> | |||
| </h1> | |||
| <p className={styles.description}> | |||
| Get started by editing{' '} | |||
| <code className={styles.code}>pages/index.js</code> | |||
| </p> | |||
| <div className={styles.grid}> | |||
| <a href="https://nextjs.org/docs" className={styles.card}> | |||
| <h2>Documentation →</h2> | |||
| <p>Find in-depth information about Next.js features and API.</p> | |||
| </a> | |||
| <a href="https://nextjs.org/learn" className={styles.card}> | |||
| <h2>Learn →</h2> | |||
| <p>Learn about Next.js in an interactive course with quizzes!</p> | |||
| </a> | |||
| <a | |||
| href="https://github.com/vercel/next.js/tree/canary/examples" | |||
| className={styles.card} | |||
| > | |||
| <h2>Examples →</h2> | |||
| <p>Discover and deploy boilerplate example Next.js projects.</p> | |||
| </a> | |||
| <a | |||
| href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" | |||
| className={styles.card} | |||
| > | |||
| <h2>Deploy →</h2> | |||
| <p> | |||
| Instantly deploy your Next.js site to a public URL with Vercel. | |||
| </p> | |||
| </a> | |||
| </div> | |||
| </main> | |||
| <footer className={styles.footer}> | |||
| <a | |||
| href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| Powered by{' '} | |||
| <span className={styles.logo}> | |||
| <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} /> | |||
| </span> | |||
| </a> | |||
| </footer> | |||
| </div> | |||
| <> | |||
| <h1>Home</h1> | |||
| <button onClick={logoutHandler}>Logout</button> | |||
| </> | |||
| ); | |||
| } | |||
| }; | |||
| export default Home; | |||
| @@ -0,0 +1,19 @@ | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const createUser = async (fullName, username, email, password) => { | |||
| const response = await fetch(apiEndpoints.account.createUser, { | |||
| method: "POST", | |||
| body: JSON.stringify({ fullName, username, email, password }), | |||
| headers: { | |||
| "Content-Type": "application/json", | |||
| }, | |||
| }); | |||
| const data = await response.json(); | |||
| if (!response.ok) { | |||
| throw new Error(data.message || "Something went wrong!"); | |||
| } | |||
| return data; | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| export default { | |||
| account: { | |||
| createUser: "/api/auth/signup", | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| import * as Yup from "yup"; | |||
| export const forgotPasswordSchema = Yup.object().shape({ | |||
| email: Yup.string().required("Email is required").email(), | |||
| }); | |||
| @@ -0,0 +1,6 @@ | |||
| import * as Yup from "yup"; | |||
| export const loginSchema = Yup.object().shape({ | |||
| username: Yup.string().required("Username is required"), | |||
| password: Yup.string().required("Password is required"), | |||
| }); | |||
| @@ -0,0 +1,12 @@ | |||
| import * as Yup from "yup"; | |||
| export const registerSchema = Yup.object().shape({ | |||
| fullName: Yup.string().required("Full name is required"), | |||
| username: Yup.string().required("Username is required"), | |||
| email: Yup.string().email().required("Email is required"), | |||
| password: Yup.string().required("Password is required"), | |||
| confirmPassword: Yup.string().oneOf( | |||
| [Yup.ref("password"), null], | |||
| "Passwords must match" | |||
| ), | |||
| }); | |||
| @@ -1,129 +0,0 @@ | |||
| .container { | |||
| padding: 0 2rem; | |||
| } | |||
| .main { | |||
| min-height: 100vh; | |||
| padding: 4rem 0; | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: center; | |||
| } | |||
| .footer { | |||
| display: flex; | |||
| flex: 1; | |||
| padding: 2rem 0; | |||
| border-top: 1px solid #eaeaea; | |||
| justify-content: center; | |||
| align-items: center; | |||
| } | |||
| .footer a { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| flex-grow: 1; | |||
| } | |||
| .title a { | |||
| color: #0070f3; | |||
| text-decoration: none; | |||
| } | |||
| .title a:hover, | |||
| .title a:focus, | |||
| .title a:active { | |||
| text-decoration: underline; | |||
| } | |||
| .title { | |||
| margin: 0; | |||
| line-height: 1.15; | |||
| font-size: 4rem; | |||
| } | |||
| .title, | |||
| .description { | |||
| text-align: center; | |||
| } | |||
| .description { | |||
| margin: 4rem 0; | |||
| line-height: 1.5; | |||
| font-size: 1.5rem; | |||
| } | |||
| .code { | |||
| background: #fafafa; | |||
| border-radius: 5px; | |||
| padding: 0.75rem; | |||
| font-size: 1.1rem; | |||
| font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, | |||
| Bitstream Vera Sans Mono, Courier New, monospace; | |||
| } | |||
| .grid { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| flex-wrap: wrap; | |||
| max-width: 800px; | |||
| } | |||
| .card { | |||
| margin: 1rem; | |||
| padding: 1.5rem; | |||
| text-align: left; | |||
| color: inherit; | |||
| text-decoration: none; | |||
| border: 1px solid #eaeaea; | |||
| border-radius: 10px; | |||
| transition: color 0.15s ease, border-color 0.15s ease; | |||
| max-width: 300px; | |||
| } | |||
| .card:hover, | |||
| .card:focus, | |||
| .card:active { | |||
| color: #0070f3; | |||
| border-color: #0070f3; | |||
| } | |||
| .card h2 { | |||
| margin: 0 0 1rem 0; | |||
| font-size: 1.5rem; | |||
| } | |||
| .card p { | |||
| margin: 0; | |||
| font-size: 1.25rem; | |||
| line-height: 1.5; | |||
| } | |||
| .logo { | |||
| height: 1em; | |||
| margin-left: 0.5rem; | |||
| } | |||
| @media (max-width: 600px) { | |||
| .grid { | |||
| width: 100%; | |||
| flex-direction: column; | |||
| } | |||
| } | |||
| @media (prefers-color-scheme: dark) { | |||
| .card, | |||
| .footer { | |||
| border-color: #222; | |||
| } | |||
| .code { | |||
| background: #111; | |||
| } | |||
| .logo img { | |||
| filter: invert(1); | |||
| } | |||
| } | |||
| @@ -1,26 +1,18 @@ | |||
| html, | |||
| body { | |||
| padding: 0; | |||
| margin: 0; | |||
| font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, | |||
| Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; | |||
| } | |||
| a { | |||
| color: inherit; | |||
| text-decoration: none; | |||
| } | |||
| @import url('https://fonts.googleapis.com/css2?family=Lato:wght@700&family=Open+Sans:wght@400;700&display=swap'); | |||
| * { | |||
| box-sizing: border-box; | |||
| } | |||
| @media (prefers-color-scheme: dark) { | |||
| html { | |||
| color-scheme: dark; | |||
| } | |||
| body { | |||
| color: white; | |||
| background: black; | |||
| } | |||
| body { | |||
| font-family: 'Open Sans', 'Lato', sans-serif; | |||
| } | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6 { | |||
| font-family: 'Lato', sans-serif; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| import { MongoClient } from "mongodb"; | |||
| export async function connectToDatabase() { | |||
| const client = await MongoClient.connect(process.env.MONGODB_AUTH); | |||
| return client; | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| import { hash, compare } from "bcryptjs"; | |||
| export async function hashPassword(password) { | |||
| const hashedPassword = await hash(password, 12); | |||
| return hashedPassword; | |||
| } | |||
| export async function verifyPassword(password, hashedPassword) { | |||
| const isValid = await compare(password, hashedPassword); | |||
| return isValid; | |||
| } | |||