login: Create enter-login-code page

This commit is contained in:
David Ernst
2021-08-07 15:10:45 -07:00
parent 0af4fa0e82
commit b4d5462643
6 changed files with 127 additions and 74 deletions

View File

@@ -0,0 +1 @@
export { EnterCodePage as default } from '../src/admin/LoginPage/EnterCodePage'

View File

@@ -0,0 +1,108 @@
import { TextField } from '@material-ui/core'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { GlobalCSS } from 'src/GlobalCSS'
import { OnClickButton } from 'src/landing-page/Button'
import { api } from '../../api-helper'
import { Head } from '../../Head'
import { checkLoginCode } from '../auth'
import { Headerbar } from './Headerbar'
export const breakpoint = 500
export const EnterCodePage = () => {
const router = useRouter()
const { email } = router.query
const [error, setError] = useState('')
const [loginCode, setLoginCode] = useState('')
if (typeof email !== 'string') return <p>Missing email</p>
return (
<main>
<Head title="Admin Login" />
<Headerbar hideLogin />
<section>
<p>An email with login information is being sent to:</p>
<h4>{email}</h4>
<div>
<TextField
autoFocus
label="Code in Email"
placeholder="123456"
size="small"
style={{ backgroundColor: '#fff' }}
value={loginCode}
variant="outlined"
onChange={({ target }) => {
setError('')
const next = target.value
// Allow up to 6 numbers only
if (/^\d{0,6}$/.test(next)) return setLoginCode(next)
setError('Login codes are 6 digit numbers')
}}
/>
<OnClickButton
style={{ margin: 0, marginLeft: 10, padding: '8px 20px' }}
onClick={() => {
checkLoginCode({
code: loginCode,
email,
onExpired: () => {
setError('This login link has expired.\nSending you another...')
setLoginCode('')
api('admin-login', { email })
},
onInvalid: () => setError('Incorrect code'),
router,
})
}}
>
Submit
</OnClickButton>
</div>
<div className="error" style={{ opacity: error ? '' : 0 }}>
&nbsp; {error}
</div>
</section>
<style jsx>{`
section {
text-align: center;
margin-top: 3rem;
}
p {
margin: 0;
}
h4 {
margin: 2rem 0;
}
.error {
color: red;
opacity: 0.75;
font-size: 12px;
font-weight: 700;
border: 1px solid #f00a;
padding: 5px;
border-radius: 5px;
margin-top: 1rem;
display: inline-block;
white-space: pre-wrap;
}
`}</style>
<style global jsx>{`
body {
background: #f9fafb;
}
`}</style>
<GlobalCSS />
</main>
)
}

View File

@@ -1,14 +1,16 @@
import { LoginInput } from './LoginInput'
import { breakpoint } from './LoginPage'
export const Headerbar = () => {
export const Headerbar = ({ hideLogin }: { hideLogin?: boolean }) => {
return (
<header>
<div>
<h2>Secure Internet Voting</h2>
<section>
<LoginInput />
</section>
{!hideLogin && (
<section>
<LoginInput />
</section>
)}
</div>
<style jsx>{`
header {

View File

@@ -1,5 +1,6 @@
import { TextField } from '@material-ui/core'
import { validate as validateEmail } from 'email-validator'
import { useRouter } from 'next/router'
import { ChangeEventHandler, KeyboardEventHandler, useRef, useState } from 'react'
import { api } from 'src/api-helper'
import { OnClickButton } from 'src/landing-page/Button'
@@ -13,6 +14,7 @@ type SharedInputProps = {
}
export const LoginInput = ({ mobile }: { mobile?: boolean }) => {
const router = useRouter()
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [pending, setPending] = useState(false)
@@ -63,7 +65,7 @@ export const LoginInput = ({ mobile }: { mobile?: boolean }) => {
} else if (response.status === 404) {
setError('Not an approved account. \nCheck for typos, or Create Account below.')
} else {
alert('TODO: Switch to Enter Code page')
router.push(`./enter-login-code?email=${email}`)
}
}}
>

View File

@@ -1,11 +1,7 @@
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { GlobalCSS } from 'src/GlobalCSS'
import { OnClickButton } from 'src/landing-page/Button'
import { api } from '../../api-helper'
import { Head } from '../../Head'
import { checkLoginCode } from '../auth'
import { AboutSection } from './AboutSection'
import { CreateAccount } from './CreateAccount'
import { Headerbar } from './Headerbar'
@@ -14,17 +10,16 @@ import { MobileLogin } from './MobileLogin'
export const breakpoint = 500
export const LoginPage = () => {
const router = useRouter()
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [loginCode, setLoginCode] = useState('')
// const router = useRouter()
// const [email, setEmail] = useState('')
// const [error, setError] = useState('')
// Check if there's a redirect message in URL
useEffect(() => {
const { email, expired, invalid } = router.query
if (email) setEmail(email as string)
if (expired) setError('This login link has expired, click Sign In below to create another.')
if (invalid) setError('This login link appears invalid, click Sign In below to create another.')
// const { email, expired, invalid } = router.query
// if (email) setEmail(email as string)
// if (expired) setError('This login link has expired, click Sign In below to create another.')
// if (invalid) setError('This login link appears invalid, click Sign In below to create another.')
}, [])
return (
@@ -39,57 +34,6 @@ export const LoginPage = () => {
<CreateAccount />
</div>
</div>
<div style={{ display: 'none' }}>
{status == 'sent' && (
<>
<div className="text-center">
<p className="text-sm">An email with login information is being sent to:</p>
<p className="font-semibold">{email}</p>
</div>
<div>
<label className="mt-8 text-xs font-semibold">Code:</label>
<div className="flex">
<input
className="relative flex-grow block px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md appearance-none focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10"
placeholder="123456"
type="text"
value={loginCode}
onChange={({ target }) => {
setError('')
const next = target.value
// Allow up to 6 numbers only
if (/^\d{0,6}$/.test(next)) return setLoginCode(next)
setError('Login codes are 6 digit numbers')
}}
/>
<OnClickButton
style={{ margin: 0, marginLeft: 20, padding: '10px 20px' }}
onClick={() => {
checkLoginCode({
clientSideRedirect: false,
code: loginCode,
email,
onExpired: () => {
setError("This login link has expired, we're sending you another.")
setLoginCode('')
api('admin-login', { email })
},
onInvalid: () => setError('Incorrect code'),
router,
})
}}
>
Submit
</OnClickButton>
</div>
<label className="error" style={{ opacity: error ? 1 : 0 }}>
&nbsp; {error}
</label>
</div>
</>
)}
</div>
<style jsx>{`
.container {
display: flex;

View File

@@ -22,14 +22,12 @@ async function logout() {
}
export async function checkLoginCode({
clientSideRedirect = true,
code,
email,
onExpired,
onInvalid,
router,
}: {
clientSideRedirect?: boolean
code: string
email: string
onExpired: () => void
@@ -45,9 +43,7 @@ export async function checkLoginCode({
mutate(jwt_api_path)
// Remove url parameters
// Need to do full reload when transitioning in from /login page's Code Input form, otherwise Tailwind's css screws up formatting.
clientSideRedirect ? await router.replace('/admin') : (window.location.href = '/admin')
return
return await router.replace('/admin')
}
// Expired session: redirects back to login page w/ custom error