From 6954342045f0d969a37131ee4cba5edfeb571612 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Tue, 14 Jan 2025 15:13:51 +0100 Subject: [PATCH] fix: cannot login with email on PWA --- components/login.js | 1 + lib/validate.js | 4 +++ pages/api/auth/[...nextauth].js | 37 +++++++++++++++++++++++---- pages/email.js | 44 ++++++++++++++++++++++++++++++++- pages/settings/index.js | 3 ++- 5 files changed, 82 insertions(+), 7 deletions(-) diff --git a/components/login.js b/components/login.js index 3f43f8a0d..ed7968e6d 100644 --- a/components/login.js +++ b/components/login.js @@ -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 }) }} > diff --git a/lib/validate.js b/lib/validate.js index 12bce77a8..7f06078ab 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -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') }) diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index f2dcfde39..7ef1568ca 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -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' @@ -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 }) ] @@ -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({ @@ -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) { @@ -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 @@ -439,13 +447,32 @@ const html = ({ url, site, email }) => { + + +
- login as ${escapedEmail} + login with ${escapedEmail}
+ + + + +
+ using the app? copy the magic code +
+ ${token} +
+
+ + + +
+ on browser? click the button below +
login
diff --git a/pages/email.js b/pages/email.js index c3bba919c..a3bd104d4 100644 --- a/pages/email.js +++ b/pages/email.js @@ -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 (
@@ -14,8 +40,24 @@ export default function Email () {

Check your email

-

A sign in link has been sent to your email address

+

A {isPWA ? 'magic code' : 'sign in link'} has been sent to {callback ? callback.email : 'your email address'}

+ {isPWA && pushCallback(token)} />}
) } + +export const MagicCodeForm = ({ onSubmit }) => { + return ( +
{ onSubmit(token) }} + > + + verify + + ) +} diff --git a/pages/settings/index.js b/pages/settings/index.js index 4f1e912d9..5ad2f813c 100644 --- a/pages/settings/index.js +++ b/pages/settings/index.js @@ -858,7 +858,7 @@ function AuthMethods ({ methods, apiKeyEnabled }) { ) - :
+ :
} else if (provider === 'lightning') { return (