| # local env files | # local env files | ||||
| .env*.local | .env*.local | ||||
| .env | |||||
| # vercel | # vercel | ||||
| .vercel | .vercel |
| 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; |
| const base = {}; | |||||
| export const mockForgotPasswordFormProps = { | |||||
| base, | |||||
| }; |
| 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, | |||||
| }; |
| 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; |
| const base = {}; | |||||
| export const mockLoginFormProps = { | |||||
| base, | |||||
| }; |
| 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, | |||||
| }; |
| 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; |
| const base = {}; | |||||
| export const mockRegisterFormProps = { | |||||
| base, | |||||
| }; |
| 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, | |||||
| }; |
| 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; |
| import BaseTemplate from './BaseTemplate'; | import BaseTemplate from './BaseTemplate'; | ||||
| import { mockBaseTemplateProps } from './BaseTemplate.mocks'; | import { mockBaseTemplateProps } from './BaseTemplate.mocks'; | ||||
| export default { | |||||
| const obj = { | |||||
| title: 'templates/BaseTemplate', | title: 'templates/BaseTemplate', | ||||
| component: BaseTemplate, | component: BaseTemplate, | ||||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | ||||
| argTypes: {}, | argTypes: {}, | ||||
| }; | |||||
| }; //eslint-disable-line | |||||
| export default obj; | |||||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | ||||
| const Template = (args) => <BaseTemplate {...args} />; | const Template = (args) => <BaseTemplate {...args} />; | ||||
| 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"; |
| "build-storybook": "build-storybook" | "build-storybook": "build-storybook" | ||||
| }, | }, | ||||
| "dependencies": { | "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": "12.2.3", | ||||
| "next-auth": "^4.10.2", | |||||
| "prop-types": "^15.8.1", | |||||
| "react": "18.2.0", | "react": "18.2.0", | ||||
| "react-dom": "18.2.0" | |||||
| "react-dom": "18.2.0", | |||||
| "sass": "^1.54.0", | |||||
| "yup": "^0.32.11" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@babel/core": "^7.18.9", | "@babel/core": "^7.18.9", | ||||
| "@babel/preset-env": "^7.18.9", | |||||
| "@babel/preset-react": "^7.18.6", | |||||
| "@commitlint/cli": "^17.0.3", | "@commitlint/cli": "^17.0.3", | ||||
| "@commitlint/config-conventional": "^17.0.3", | "@commitlint/config-conventional": "^17.0.3", | ||||
| "@storybook/addon-actions": "^6.5.9", | "@storybook/addon-actions": "^6.5.9", | ||||
| "@storybook/react": "^6.5.9", | "@storybook/react": "^6.5.9", | ||||
| "@storybook/testing-library": "^0.0.13", | "@storybook/testing-library": "^0.0.13", | ||||
| "babel-loader": "^8.2.5", | "babel-loader": "^8.2.5", | ||||
| "babel-plugin-import": "^1.13.5", | |||||
| "cross-env": "^7.0.3", | "cross-env": "^7.0.3", | ||||
| "eslint": "8.21.0", | "eslint": "8.21.0", | ||||
| "eslint-config-next": "12.2.3", | "eslint-config-next": "12.2.3", |
| 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 }; | |||||
| }, | |||||
| }), | |||||
| ], | |||||
| }); |
| 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; |
| 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; |
| 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; |
| 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; |
| 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 ( | 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; |
| 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; | |||||
| }; |
| export default { | |||||
| account: { | |||||
| createUser: "/api/auth/signup", | |||||
| }, | |||||
| }; |
| import * as Yup from "yup"; | |||||
| export const forgotPasswordSchema = Yup.object().shape({ | |||||
| email: Yup.string().required("Email is required").email(), | |||||
| }); |
| 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"), | |||||
| }); |
| 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" | |||||
| ), | |||||
| }); |
| .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); | |||||
| } | |||||
| } |
| 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; | 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; | |||||
| } | } |
| import { MongoClient } from "mongodb"; | |||||
| export async function connectToDatabase() { | |||||
| const client = await MongoClient.connect(process.env.MONGODB_AUTH); | |||||
| return client; | |||||
| } |
| 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; | |||||
| } |