Skip to content
This repository was archived by the owner on Jul 17, 2022. It is now read-only.

Commit

Permalink
feat(frontend): hook up API
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Sep 5, 2021
1 parent b1845e1 commit 81b60b5
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 20 deletions.
4 changes: 4 additions & 0 deletions frontend/src/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PrivateRoute from './components/PrivateRoute'
import { UserProfileProvider } from './components/UserProfileContext'
import { useAuth } from './hooks/useAuth'
import AdminPage from './pages/AdminPage'
import ConfirmEmailWithTokenPage from './pages/ConfirmEmailWithTokenPage'
import ApolloDemoPage from './pages/demo/ApolloDemo'
import GroupCreatePage from './pages/groups/GroupCreatePage'
import GroupEditPage from './pages/groups/GroupEditPage'
Expand Down Expand Up @@ -43,6 +44,9 @@ const AppRoot = () => {
<Route path={ROUTES.HOME} exact>
{isAuthenticated ? <HomePage /> : <PublicHomePage />}
</Route>
<Route path={ROUTES.CONFIRM_EMAIL_WITH_TOKEN} exact>
<ConfirmEmailWithTokenPage />
</Route>
<PrivateRoute path={ROUTES.ADMIN_ROOT} exact>
<AdminPage />
</PrivateRoute>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/UserProfileContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ const UserProfileProvider: FunctionComponent = ({ children }) => {
// We fetch the token again in case the client-side cookie has expired but
// the remote session hasn't
if (!tokenWasFetched) {
fetch(`${SERVER_URL}/me`)
fetch(`${SERVER_URL}/me`, {
credentials: 'include',
})
.then((response) => response.json())
.catch(() => {
// The user is not logged in
Expand Down
65 changes: 63 additions & 2 deletions frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,92 @@ import { useState } from 'react'

const SERVER_URL = process.env.REACT_APP_SERVER_URL

const headers = {
'content-type': 'application/json; charset=utf-8',
}

export const tokenRegex = /^[0-9]{6}$/
export const emailRegEx = /.+@.+\..+/
export const passwordRegEx =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/

export const useAuth = () => {
const [isLoading, setIsLoading] = useState(false)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isRegistered, setIsRegistered] = useState(false)
const [isConfirmed, setIsConfirmed] = useState(false)
return {
isLoading,
isAuthenticated,
isRegistered,
isConfirmed,
logout: () => {
// Delete cookies (since the auth cookie is httpOnly we cannot access
// it using JavaScript, e.g. cookie.delete() will not work).
// Therefore we ask the server to send us an invalid cookie.
fetch(`${SERVER_URL}/me/cookie`, { method: 'DELETE' }).then(() => {
fetch(`${SERVER_URL}/me/cookie`, {
method: 'DELETE',
credentials: 'include',
}).then(() => {
setIsAuthenticated(false)
// Reload the page (no need to handle logout in the app)
document.location.reload()
})
},
login: ({ email, password }: { email: string; password: string }) => {
setIsLoading(true)
fetch(`${SERVER_URL}/me/login`, {
fetch(`${SERVER_URL}/login`, {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({ email, password }),
})
.then(() => {
setIsAuthenticated(true)
setIsLoading(false)
console.log('authenticated')
})
.catch((err) => {
console.error(err)
setIsLoading(false)
})
},
register: ({
name,
email,
password,
}: {
name: string
email: string
password: string
}) => {
setIsLoading(true)
fetch(`${SERVER_URL}/register`, {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({ email, password, name }),
})
.then(() => {
setIsLoading(false)
setIsRegistered(true)
})
.catch((err) => {
console.error(err)
setIsLoading(false)
})
},
confirm: ({ email, token }: { email: string; token: string }) => {
setIsLoading(true)
fetch(`${SERVER_URL}/register/confirm`, {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({ email, token }),
})
.then(() => {
setIsLoading(false)
setIsConfirmed(true)
})
.catch((err) => {
console.error(err)
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/pages/ConfirmEmailWithTokenPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FunctionComponent, useState } from 'react'
import { Redirect } from 'react-router-dom'
import DistributeAidWordmark from '../components/branding/DistributeAidWordmark'
import TextField from '../components/forms/TextField'
import { emailRegEx, tokenRegex, useAuth } from '../hooks/useAuth'

const ConfirmEmailWithTokenPage: FunctionComponent = () => {
const { confirm, isConfirmed } = useAuth()
const [email, setEmail] = useState('')
const [token, setToken] = useState('')

const formValid = emailRegEx.test(email) && tokenRegex.test(token)

return (
<main className="flex h-screen justify-center bg-navy-900 p-4">
<div className="max-w-md w-full mt-20">
<div className="p-4 text-center">
<DistributeAidWordmark className="block mx-auto mb-6" height="100" />
</div>
<div className="bg-white rounded p-6">
<h1 className="text-2xl mb-4 text-center">Shipment Tracker</h1>
<p className="mb-6">
Welcome to Distribute Aid's shipment tracker! Please log in to
continue.
</p>
<form>
<TextField
label="Your email"
type="email"
name="email"
autoComplete="email"
value={email}
onChange={({ target: { value } }) => setEmail(value)}
/>
<TextField
label="Your verification token"
type="text"
name="token"
value={token}
pattern="^[0-9]{6}"
onChange={({ target: { value } }) => setToken(value)}
/>
<button
className="bg-navy-800 text-white text-lg px-4 py-2 rounded-sm w-full hover:bg-opacity-90"
type="button"
onClick={() => {
confirm({ email, token })
}}
disabled={!formValid}
>
Verify
</button>
</form>
{isConfirmed && (
<Redirect
to={{
pathname: '/',
state: { email },
}}
/>
)}
</div>
</div>
</main>
)
}

export default ConfirmEmailWithTokenPage
49 changes: 32 additions & 17 deletions frontend/src/pages/PublicHome.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { FunctionComponent, useState } from 'react'
import { Redirect } from 'react-router-dom'
import DistributeAidWordmark from '../components/branding/DistributeAidWordmark'
import TextField from '../components/forms/TextField'
import { useAuth } from '../hooks/useAuth'

const SERVER_URL = process.env.REACT_APP_SERVER_URL

const emailRegEx = /.+@.+\..+/
const passwordRegEx =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/
import { emailRegEx, passwordRegEx, useAuth } from '../hooks/useAuth'

const PublicHomePage: FunctionComponent = () => {
const { login } = useAuth()
const { login, register, isRegistered } = useAuth()
const [showRegisterForm, setShowRegisterForm] = useState(false)
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [password2, setPassword2] = useState('')

const loginFormValid = emailRegEx.test(email) && passwordRegEx.test(password)

const registerFormValid = loginFormValid && password === password2
const registerFormValid =
loginFormValid && password === password2 && name.trim().length > 0

const showLoginForm = !showRegisterForm
return (
Expand All @@ -39,13 +36,15 @@ const PublicHomePage: FunctionComponent = () => {
label="email"
type="email"
name="email"
autoComplete="email"
value={email}
onChange={({ target: { value } }) => setEmail(value)}
/>
<TextField
label="password"
type="password"
name="password"
autoComplete="password"
value={password}
onChange={({ target: { value } }) => setPassword(value)}
/>
Expand All @@ -66,27 +65,38 @@ const PublicHomePage: FunctionComponent = () => {
</button>
</form>
)}
{showRegisterForm && (
{showRegisterForm && !isRegistered && (
<form>
<TextField
label="email"
label="Your name"
type="text"
name="name"
autoComplete="name"
value={name}
onChange={({ target: { value } }) => setName(value)}
/>
<TextField
label="Your email"
type="email"
name="email"
autoComplete="email"
value={email}
onChange={({ target: { value } }) => setEmail(value)}
/>
<TextField
label="password"
label="Pick a good password"
type="password"
name="password"
autoComplete="new-password"
minLength={8}
value={password}
onChange={({ target: { value } }) => setPassword(value)}
/>
<TextField
label="password (repeat)"
label="Repeat your password"
type="password"
name="password2"
autoComplete="new-password"
minLength={8}
value={password2}
onChange={({ target: { value } }) => setPassword2(value)}
Expand All @@ -95,10 +105,7 @@ const PublicHomePage: FunctionComponent = () => {
className="bg-navy-800 text-white text-lg px-4 py-2 rounded-sm w-full hover:bg-opacity-90"
type="button"
onClick={() => {
fetch(`${SERVER_URL}/register`, {
method: 'POST',
body: JSON.stringify({ email, password }),
})
register({ name, email, password })
}}
disabled={!registerFormValid}
>
Expand All @@ -113,6 +120,14 @@ const PublicHomePage: FunctionComponent = () => {
</button>
</form>
)}
{isRegistered && (
<Redirect
to={{
pathname: '/register/confirm',
state: { email },
}}
/>
)}
</div>
</div>
</main>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ROUTES = {
SHIPMENT_EDIT: '/shipment/:shipmentId/edit',
KITCHEN_SINK: '/kitchen-sink',
FORM_DEMO: '/form-demo',
CONFIRM_EMAIL_WITH_TOKEN: '/register/confirm',
}

export function groupViewRoute(groupId: number | string) {
Expand Down

0 comments on commit 81b60b5

Please sign in to comment.