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 (
+
+ )
+}
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 (
|