Chapter 4, Authentication complete.

This commit is contained in:
Colin Diem
2022-04-21 22:03:30 -04:00
parent 11919b6b5f
commit a8777abc2f
15 changed files with 812 additions and 49 deletions

View File

@@ -13,6 +13,7 @@
]
},
"dependencies": {
"@redwoodjs/auth": "1.0.0",
"@redwoodjs/forms": "1.0.0",
"@redwoodjs/router": "1.0.0",
"@redwoodjs/web": "1.0.0",

View File

@@ -1,3 +1,5 @@
import { AuthProvider } from '@redwoodjs/auth'
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
@@ -10,9 +12,11 @@ import './index.css'
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
<AuthProvider type="dbAuth">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)

View File

@@ -7,26 +7,32 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
import { Router, Route, Set } from '@redwoodjs/router'
import { Private, Router, Route, Set } from '@redwoodjs/router'
import PostsLayout from 'src/layouts/PostsLayout'
import BlogLayout from './layouts/BlogLayout/BlogLayout'
const Routes = () => {
return (
<Router>
<Set wrap={PostsLayout}>
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/posts" page={PostPostsPage} name="posts" />
</Set>
<Route path="/login" page={LoginPage} name="login" />
<Route path="/signup" page={SignupPage} name="signup" />
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
<Private unauthenticated="home">
<Set wrap={PostsLayout}>
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
</Private>
<Set wrap={BlogLayout}>
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
<Route path="/contact" page={ContactPage} name="contact" />
</Set>
<Route notfound page={NotFoundPage} />
<Route notfound page={NotFoundPage} />
</Router>
)
}

View File

@@ -1,26 +1,45 @@
import { Link, routes } from '@redwoodjs/router';
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
const BlogLayout = ({ children }) => {
return <>
<header>
<h1>
<Link to={routes.home()}>Colin's Redwood Blog</Link>
</h1>
<nav>
<ul>
<li>
<Link to={routes.home()}>Home</Link>
</li>
<li>
<Link to={routes.about()}>About</Link>
</li>
<li>
<Link to={routes.contact()}>Contact Us</Link>
</li>
</ul>
</nav>
</header>
<main>{children}</main></>
const { isAuthenticated, currentUser, logOut } = useAuth()
return (
<>
<header>
<div className="flex-between">
<h1>
<Link to={routes.home()}>Colin's Redwood Blog</Link>
</h1>
{isAuthenticated ? (
<div>
<span>Logged in as {currentUser.email}</span>{' '}
<button type="button" onClick={logOut}>
Logout
</button>
</div>
) : (
<Link to={routes.login()}>Login</Link>
)}
</div>
<nav>
<ul>
<li>
<Link to={routes.home()}>Home</Link>
</li>
<li>
<Link to={routes.about()}>About</Link>
</li>
<li>
<Link to={routes.contact()}>Contact Us</Link>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
</>
)
}
export default BlogLayout

View File

@@ -0,0 +1,89 @@
import { useEffect, useRef } from 'react'
import { useAuth } from '@redwoodjs/auth'
import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
const ForgotPasswordPage = () => {
const { isAuthenticated, forgotPassword } = useAuth()
useEffect(() => {
if (isAuthenticated) {
navigate(routes.home())
}
}, [isAuthenticated])
const usernameRef = useRef()
useEffect(() => {
usernameRef.current.focus()
}, [])
const onSubmit = async (data) => {
const response = await forgotPassword(data.username)
if (response.error) {
toast.error(response.error)
} else {
// The function `forgotPassword.handler` in api/src/functions/auth.js has
// been invoked, let the user know how to get the link to reset their
// password (sent in email, perhaps?)
toast.success(
'A link to reset your password was sent to ' + response.email
)
navigate(routes.login())
}
}
return (
<>
<MetaTags title="Forgot Password" />
<main className="rw-main">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Forgot Password
</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<div className="text-left">
<Label
name="username"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Username
</Label>
<TextField
name="username"
className="rw-input"
errorClassName="rw-input rw-input-error"
ref={usernameRef}
validation={{
required: true,
}}
/>
<FieldError name="username" className="rw-field-error" />
</div>
<div className="rw-button-group">
<Submit className="rw-button rw-button-blue">Submit</Submit>
</div>
</Form>
</div>
</div>
</div>
</div>
</main>
</>
)
}
export default ForgotPasswordPage

View File

@@ -0,0 +1,129 @@
import { Link, navigate, routes } from '@redwoodjs/router'
import { useRef } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { useAuth } from '@redwoodjs/auth'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useEffect } from 'react'
const LoginPage = () => {
const { isAuthenticated, logIn } = useAuth()
useEffect(() => {
if (isAuthenticated) {
navigate(routes.home())
}
}, [isAuthenticated])
const usernameRef = useRef()
useEffect(() => {
usernameRef.current.focus()
}, [])
const onSubmit = async (data) => {
const response = await logIn({ ...data })
if (response.message) {
toast(response.message)
} else if (response.error) {
toast.error(response.error)
} else {
toast.success('Welcome back!')
}
}
return (
<>
<MetaTags title="Login" />
<main className="rw-main">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">Login</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<Label
name="username"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Username
</Label>
<TextField
name="username"
className="rw-input"
errorClassName="rw-input rw-input-error"
ref={usernameRef}
validation={{
required: {
value: true,
message: 'Username is required',
},
}}
/>
<FieldError name="username" className="rw-field-error" />
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Password
</Label>
<PasswordField
name="password"
className="rw-input"
errorClassName="rw-input rw-input-error"
autoComplete="current-password"
validation={{
required: {
value: true,
message: 'Password is required',
},
}}
/>
<div className="rw-forgot-link">
<Link
to={routes.forgotPassword()}
className="rw-forgot-link"
>
Forgot Password?
</Link>
</div>
<FieldError name="password" className="rw-field-error" />
<div className="rw-button-group">
<Submit className="rw-button rw-button-blue">Login</Submit>
</div>
</Form>
</div>
</div>
</div>
<div className="rw-login-link">
<span>Don&apos;t have an account?</span>{' '}
<Link to={routes.signup()} className="rw-link">
Sign up!
</Link>
</div>
</div>
</main>
</>
)
}
export default LoginPage

View File

@@ -0,0 +1,119 @@
import { useEffect, useRef, useState } from 'react'
import { useAuth } from '@redwoodjs/auth'
import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import {
Form,
Label,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
const ResetPasswordPage = ({ resetToken }) => {
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
useAuth()
const [enabled, setEnabled] = useState(true)
useEffect(() => {
if (isAuthenticated) {
navigate(routes.home())
}
}, [isAuthenticated])
useEffect(() => {
const validateToken = async () => {
const response = await validateResetToken(resetToken)
if (response.error) {
setEnabled(false)
toast.error(response.error)
} else {
setEnabled(true)
}
}
validateToken()
}, [])
const passwordRef = useRef()
useEffect(() => {
passwordRef.current.focus()
}, [])
const onSubmit = async (data) => {
const response = await resetPassword({
resetToken,
password: data.password,
})
if (response.error) {
toast.error(response.error)
} else {
toast.success('Password changed!')
await reauthenticate()
navigate(routes.login())
}
}
return (
<>
<MetaTags title="Reset Password" />
<main className="rw-main">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Reset Password
</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<div className="text-left">
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
New Password
</Label>
<PasswordField
name="password"
autoComplete="new-password"
className="rw-input"
errorClassName="rw-input rw-input-error"
disabled={!enabled}
ref={passwordRef}
validation={{
required: {
value: true,
message: 'Password is required',
},
}}
/>
<FieldError name="password" className="rw-field-error" />
</div>
<div className="rw-button-group">
<Submit
className="rw-button rw-button-blue"
disabled={!enabled}
>
Submit
</Submit>
</div>
</Form>
</div>
</div>
</div>
</div>
</main>
</>
)
}
export default ResetPasswordPage

View File

@@ -0,0 +1,124 @@
import { Link, navigate, routes } from '@redwoodjs/router'
import { useRef } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
FieldError,
Submit,
} from '@redwoodjs/forms'
import { useAuth } from '@redwoodjs/auth'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useEffect } from 'react'
const SignupPage = () => {
const { isAuthenticated, signUp } = useAuth()
useEffect(() => {
if (isAuthenticated) {
navigate(routes.home())
}
}, [isAuthenticated])
// focus on email box on page load
const usernameRef = useRef()
useEffect(() => {
usernameRef.current.focus()
}, [])
const onSubmit = async (data) => {
const response = await signUp({ ...data })
if (response.message) {
toast(response.message)
} else if (response.error) {
toast.error(response.error)
} else {
// user is signed in automatically
toast.success('Welcome!')
}
}
return (
<>
<MetaTags title="Signup" />
<main className="rw-main">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">Signup</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<Label
name="username"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Username
</Label>
<TextField
name="username"
className="rw-input"
errorClassName="rw-input rw-input-error"
ref={usernameRef}
validation={{
required: {
value: true,
message: 'Username is required',
},
}}
/>
<FieldError name="username" className="rw-field-error" />
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Password
</Label>
<PasswordField
name="password"
className="rw-input"
errorClassName="rw-input rw-input-error"
autoComplete="current-password"
validation={{
required: {
value: true,
message: 'Password is required',
},
}}
/>
<FieldError name="password" className="rw-field-error" />
<div className="rw-button-group">
<Submit className="rw-button rw-button-blue">
Sign Up
</Submit>
</div>
</Form>
</div>
</div>
</div>
<div className="rw-login-link">
<span>Already have an account?</span>{' '}
<Link to={routes.login()} className="rw-link">
Log in!
</Link>
</div>
</div>
</main>
</>
)
}
export default SignupPage