Skip to content

Commit

Permalink
fix: cannot login with email on PWA
Browse files Browse the repository at this point in the history
  • Loading branch information
Soxasora committed Jan 14, 2025
1 parent c40ef5a commit 6954342
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 7 deletions.
1 change: 1 addition & 0 deletions components/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
}}
schema={emailSchema}
onSubmit={async ({ email }) => {
window.sessionStorage.setItem('callback', JSON.stringify({ email, callbackUrl }))
signIn('email', { email, callbackUrl, multiAuth })
}}
>
Expand Down
4 changes: 4 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ export const emailSchema = object({
email: string().email('email is no good').required('required')
})

export const emailTokenSchema = object({
token: string().required('required').trim().matches(/^[0-9]{6}$/, 'must be 6 digits')
})

export const urlSchema = object({
url: string().url().required('required')
})
Expand Down
37 changes: 32 additions & 5 deletions pages/api/auth/[...nextauth].js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHash } from 'node:crypto'
import { createHash, randomInt } from 'node:crypto'
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GitHubProvider from 'next-auth/providers/github'
Expand Down Expand Up @@ -272,6 +272,8 @@ const getProviders = res => [
EmailProvider({
server: process.env.LOGIN_EMAIL_SERVER,
from: process.env.LOGIN_EMAIL_FROM,
maxAge: 5 * 60, // expires in 5 minutes
generateVerificationToken: randomizeToken,
sendVerificationRequest
})
]
Expand Down Expand Up @@ -366,9 +368,14 @@ export default async (req, res) => {
await NextAuth(req, res, getAuthOptions(req, res))
}

function randomizeToken () {
return randomInt(100000, 1000000).toString()
}

async function sendVerificationRequest ({
identifier: email,
url,
token,
provider
}) {
let user = await prisma.user.findUnique({
Expand All @@ -391,14 +398,15 @@ async function sendVerificationRequest ({
const { server, from } = provider

const site = new URL(url).host
// const isPWA = new URL(url).searchParams.get('pwa') === 'true'

nodemailer.createTransport(server).sendMail(
{
to: email,
from,
subject: `login to ${site}`,
text: text({ url, site, email }),
html: user ? html({ url, site, email }) : newUserHtml({ url, site, email })
text: text({ url, token, site, email }),
html: user ? html({ url, token, site, email }) : newUserHtml({ url, token, site, email })
},
(error) => {
if (error) {
Expand All @@ -411,7 +419,7 @@ async function sendVerificationRequest ({
}

// Email HTML body
const html = ({ url, site, email }) => {
const html = ({ url, token, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
Expand Down Expand Up @@ -439,13 +447,32 @@ const html = ({ url, site, email }) => {
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
login as <strong>${escapedEmail}</strong>
login with <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
using the app? copy the magic code
</td>
<tr><td height="10px"></td></tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 36px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${token}</strong>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" style="padding: 10px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
on browser? click the button below
</td>
<tr><td height="10px"></td></tr>
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">login</a></td>
</tr>
</table>
Expand Down
44 changes: 43 additions & 1 deletion pages/email.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
import Image from 'react-bootstrap/Image'
import { StaticLayout } from '@/components/layout'
import { getGetServerSideProps } from '@/api/ssrApollo'
import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
import { Form, SubmitButton, PasswordInput } 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('') // callback.email, callback.callbackUrl
const [isPWA, setIsPWA] = useState(false)

// TODO: evaluate independent checkPWA function
const checkPWA = () => { // from pull-to-refresh.js
const androidPWA = window.matchMedia('(display-mode: standalone)').matches
const iosPWA = window.navigator.standalone === true
setIsPWA(androidPWA || iosPWA)
}

useEffect(() => {
checkPWA()
setCallback(JSON.parse(window.sessionStorage.getItem('callback')))
}, [])

// build and push the final callback URL
const pushCallback = (token) => {
const url = `/api/auth/callback/email?${callback.callbackUrl ? `callbackUrl=${callback.callbackUrl}` : ''}&token=${token}&email=${encodeURIComponent(callback.email)}`
router.push(url)
}

return (
<StaticLayout>
<div className='p-4 text-center'>
Expand All @@ -14,8 +40,24 @@ export default function Email () {
<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'>A sign in link has been sent to your email address</h4>
<h4 className='text-muted pt-2 pb-4'>A {isPWA ? 'magic code' : 'sign in link'} has been sent to {callback ? callback.email : 'your email address'}</h4>
{isPWA && <MagicCodeForm onSubmit={(token) => pushCallback(token)} />}
</div>
</StaticLayout>
)
}

export const MagicCodeForm = ({ onSubmit }) => {
return (
<Form
initial={{
token: ''
}}
schema={emailTokenSchema}
onSubmit={({ token }) => { onSubmit(token) }}
>
<PasswordInput name='token' required placeholder='input your 6-digit magic code' />
<SubmitButton variant='primary' className='px-4'>verify</SubmitButton>
</Form>
)
}
3 changes: 2 additions & 1 deletion pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ function AuthMethods ({ methods, apiKeyEnabled }) {
</Button>
</div>
)
: <div key={provider} className='mt-2'><EmailLinkForm /></div>
: <div key={provider} className='mt-2'><EmailLinkForm callbackUrl='/settings' /></div>
} else if (provider === 'lightning') {
return (
<QRLinkButton
Expand Down Expand Up @@ -910,6 +910,7 @@ export function EmailLinkForm ({ callbackUrl }) {
// then call signIn
const { data } = await linkUnverifiedEmail({ variables: { email } })
if (data.linkUnverifiedEmail) {
window.sessionStorage.setItem('callback', JSON.stringify({ email, callbackUrl }))
signIn('email', { email, callbackUrl })
}
}}
Expand Down

0 comments on commit 6954342

Please sign in to comment.