stacker.news/pages/email.js
soxa be7c702602
Login with magic code (#1818)
* fix: cannot login with email on PWA

* adjust other email templates

* restore manual url on new user email

* no padding on button section

* cleanup

* generate 6-digit bechh32 token

* token needs to be fed as lower case; validator case insensitive

* delete token if user has failed 3 times

* proposal: context-independent error page

* include expiration time on email page message

* add expiration time to emails

* independent checkPWA function

* restore token deletion if successful auth

* final cleanup: remove unused function

* compact useVerificationToken

* email.js: magic code for non-PWA users

* adjust email templates

* MultiInput component; magic code via MultiInput

* hotfix: revert length testing; larger width for inputs

* manual bech32 token generation; no upperCase

* reverting to string concatenation

* layout tweaks, fix error placement

* pastable inputs

* small nit fixes

* less ambiguous error path

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-03 18:41:01 -06:00

71 lines
2.6 KiB
JavaScript

import Image from 'react-bootstrap/Image'
import { StaticLayout } from '@/components/layout'
import { getGetServerSideProps } from '@/api/ssrApollo'
import { useRouter } from 'next/router'
import { useState, useEffect, useCallback } from 'react'
import { Form, SubmitButton, MultiInput } from '@/components/form'
import { emailTokenSchema } from '@/lib/validate'
// force SSR to include CSP nonces
export const getServerSideProps = getGetServerSideProps({ query: null })
export default function Email () {
const router = useRouter()
const [callback, setCallback] = useState(null) // callback.email, callback.callbackUrl
useEffect(() => {
setCallback(JSON.parse(window.sessionStorage.getItem('callback')))
}, [])
// build and push the final callback URL
const pushCallback = useCallback((token) => {
const params = new URLSearchParams()
if (callback.callbackUrl) params.set('callbackUrl', callback.callbackUrl)
params.set('token', token)
params.set('email', callback.email)
const url = `/api/auth/callback/email?${params.toString()}`
router.push(url)
}, [callback, router])
return (
<StaticLayout>
<div className='p-4 text-center'>
<video width='640' height='302' loop autoPlay muted preload='auto' playsInline style={{ maxWidth: '100%' }}>
<source src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/cowboy-saloon.mp4`} type='video/mp4' />
<Image className='rounded-1 shadow-sm' width='640' height='302' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/cowboy-saloon.gif`} fluid />
</video>
<h2 className='pt-4'>Check your email</h2>
<h4 className='text-muted pt-2 pb-4'>a magic code has been sent to {callback ? callback.email : 'your email address'}</h4>
<MagicCodeForm onSubmit={(token) => pushCallback(token)} disabled={!callback} />
</div>
</StaticLayout>
)
}
export const MagicCodeForm = ({ onSubmit, disabled }) => {
return (
<Form
initial={{
token: ''
}}
schema={emailTokenSchema}
onSubmit={(values) => {
onSubmit(values.token.toLowerCase()) // token is displayed in uppercase but we need to check it in lowercase
}}
>
<MultiInput
length={6}
charLength={1}
name='token'
required
autoFocus
groupClassName='d-flex flex-column justify-content-center gap-2'
inputType='text'
hideError // hide error message on every input, allow custom error message
disabled={disabled} // disable the form if no callback is provided
/>
<SubmitButton variant='primary' className='px-4' disabled={disabled}>login</SubmitButton>
</Form>
)
}