account linking
This commit is contained in:
parent
dd4be45ae8
commit
1df49e03d9
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"search.useIgnoreFiles": true
|
||||||
|
}
|
@ -25,6 +25,23 @@ export function topClause (within) {
|
|||||||
return interval
|
return interval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function authMethods (user, args, { models, me }) {
|
||||||
|
const accounts = await models.account.findMany({
|
||||||
|
where: {
|
||||||
|
userId: me.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const oauth = accounts.map(a => a.providerId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
lightning: !!user.pubkey,
|
||||||
|
email: user.emailVerified && user.email,
|
||||||
|
twitter: oauth.indexOf('twitter') >= 0,
|
||||||
|
github: oauth.indexOf('github') >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
me: async (parent, args, { models, me }) => {
|
me: async (parent, args, { models, me }) => {
|
||||||
@ -34,6 +51,13 @@ export default {
|
|||||||
|
|
||||||
return await models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } })
|
return await models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } })
|
||||||
},
|
},
|
||||||
|
settings: async (parent, args, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
},
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
return await models.user.findUnique({ where: { name } })
|
||||||
},
|
},
|
||||||
@ -152,10 +176,54 @@ export default {
|
|||||||
await createMentions(item, models)
|
await createMentions(item, models)
|
||||||
|
|
||||||
return await models.user.findUnique({ where: { id: me.id } })
|
return await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
},
|
||||||
|
unlinkAuth: async (parent, { authType }, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === 'twitter' || authType === 'github') {
|
||||||
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
const account = await models.account.findFirst({ where: { userId: me.id, providerId: authType } })
|
||||||
|
if (!account) {
|
||||||
|
throw new UserInputError('no such account')
|
||||||
|
}
|
||||||
|
await models.account.delete({ where: { id: account.id } })
|
||||||
|
return await authMethods(user, undefined, { models, me })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === 'lightning') {
|
||||||
|
const user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
|
||||||
|
return await authMethods(user, undefined, { models, me })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === 'email') {
|
||||||
|
const user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
|
||||||
|
return await authMethods(user, undefined, { models, me })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UserInputError('no such account')
|
||||||
|
},
|
||||||
|
linkUnverifiedEmail: async (parent, { email }, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await models.user.update({ where: { id: me.id }, data: { email } })
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'P2002') {
|
||||||
|
throw new UserInputError('email taken')
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
User: {
|
User: {
|
||||||
|
authMethods,
|
||||||
nitems: async (user, args, { models }) => {
|
nitems: async (user, args, { models }) => {
|
||||||
return await models.item.count({ where: { userId: user.id, parentId: null } })
|
return await models.item.count({ where: { userId: user.id, parentId: null } })
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
|
|||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
me: User
|
me: User
|
||||||
|
settings: User
|
||||||
user(name: String!): User
|
user(name: String!): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
nameAvailable(name: String!): Boolean!
|
nameAvailable(name: String!): Boolean!
|
||||||
@ -33,6 +34,15 @@ export default gql`
|
|||||||
setPhoto(photoId: ID!): Int!
|
setPhoto(photoId: ID!): Int!
|
||||||
upsertBio(bio: String!): User!
|
upsertBio(bio: String!): User!
|
||||||
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
||||||
|
unlinkAuth(authType: String!): AuthMethods!
|
||||||
|
linkUnverifiedEmail(email: String!): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthMethods {
|
||||||
|
lightning: Boolean!
|
||||||
|
email: String
|
||||||
|
twitter: Boolean!
|
||||||
|
github: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
@ -61,5 +71,6 @@ export default gql`
|
|||||||
noteInvites: Boolean!
|
noteInvites: Boolean!
|
||||||
noteJobIndicator: Boolean!
|
noteJobIndicator: Boolean!
|
||||||
lastCheckedJobs: String
|
lastCheckedJobs: String
|
||||||
|
authMethods: AuthMethods!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
50
components/lightning-auth.js
Normal file
50
components/lightning-auth.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||||
|
import { signIn } from 'next-auth/client'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import LnQR, { LnQRSkeleton } from './lnqr'
|
||||||
|
|
||||||
|
function LnQRAuth ({ k1, encodedUrl, callbackUrl }) {
|
||||||
|
const query = gql`
|
||||||
|
{
|
||||||
|
lnAuth(k1: "${k1}") {
|
||||||
|
pubkey
|
||||||
|
k1
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||||
|
|
||||||
|
if (data && data.lnAuth.pubkey) {
|
||||||
|
signIn('credentials', { ...data.lnAuth, callbackUrl })
|
||||||
|
}
|
||||||
|
|
||||||
|
// output pubkey and k1
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<small className='mb-2'>
|
||||||
|
<a className='text-muted text-underline' href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer' style={{ textDecoration: 'underline' }}>Does my wallet support lnurl-auth?</a>
|
||||||
|
</small>
|
||||||
|
<LnQR value={encodedUrl} status='waiting for you' />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LightningAuth ({ callbackUrl }) {
|
||||||
|
// query for challenge
|
||||||
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
|
mutation createAuth {
|
||||||
|
createAuth {
|
||||||
|
k1
|
||||||
|
encodedUrl
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
useEffect(createAuth, [])
|
||||||
|
|
||||||
|
if (error) return <div>error</div>
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <LnQRSkeleton status='generating' />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LnQRAuth {...data.createAuth} callbackUrl={callbackUrl} />
|
||||||
|
}
|
33
components/login-button.js
Normal file
33
components/login-button.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import GithubIcon from '../svgs/github-fill.svg'
|
||||||
|
import TwitterIcon from '../svgs/twitter-fill.svg'
|
||||||
|
import LightningIcon from '../svgs/bolt.svg'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
export default function LoginButton ({ text, type, className, onClick }) {
|
||||||
|
let Icon, variant
|
||||||
|
switch (type) {
|
||||||
|
case 'twitter':
|
||||||
|
Icon = TwitterIcon
|
||||||
|
variant = 'twitter'
|
||||||
|
break
|
||||||
|
case 'github':
|
||||||
|
Icon = GithubIcon
|
||||||
|
variant = 'dark'
|
||||||
|
break
|
||||||
|
case 'lightning':
|
||||||
|
Icon = LightningIcon
|
||||||
|
variant = 'primary'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button className={className} variant={variant} onClick={onClick}>
|
||||||
|
<Icon
|
||||||
|
width={20}
|
||||||
|
height={20} className='mr-2'
|
||||||
|
/>
|
||||||
|
{text} {name}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
@ -6,17 +6,39 @@ import TwitterIcon from '../svgs/twitter-fill.svg'
|
|||||||
import LightningIcon from '../svgs/bolt.svg'
|
import LightningIcon from '../svgs/bolt.svg'
|
||||||
import { Form, Input, SubmitButton } from '../components/form'
|
import { Form, Input, SubmitButton } from '../components/form'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Alert from 'react-bootstrap/Alert'
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import LayoutCenter from '../components/layout-center'
|
import LayoutCenter from '../components/layout-center'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import LnQR, { LnQRSkeleton } from '../components/lnqr'
|
import { LightningAuth } from './lightning-auth'
|
||||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
|
||||||
|
|
||||||
export const EmailSchema = Yup.object({
|
export const EmailSchema = Yup.object({
|
||||||
email: Yup.string().email('email is no good').required('required')
|
email: Yup.string().email('email is no good').required('required')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function EmailLoginForm ({ callbackUrl }) {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
email: ''
|
||||||
|
}}
|
||||||
|
schema={EmailSchema}
|
||||||
|
onSubmit={async ({ email }) => {
|
||||||
|
signIn('email', { email, callbackUrl })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label='Email'
|
||||||
|
name='email'
|
||||||
|
placeholder='email@example.com'
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<SubmitButton variant='secondary' className={styles.providerButton}>Login with Email</SubmitButton>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Login ({ providers, callbackUrl, error, Header }) {
|
export default function Login ({ providers, callbackUrl, error, Header }) {
|
||||||
const errors = {
|
const errors = {
|
||||||
Signin: 'Try signing with a different account.',
|
Signin: 'Try signing with a different account.',
|
||||||
@ -84,72 +106,9 @@ export default function Login ({ providers, callbackUrl, error, Header }) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className='mt-2 text-center text-muted font-weight-bold'>or</div>
|
<div className='mt-2 text-center text-muted font-weight-bold'>or</div>
|
||||||
<Form
|
<EmailLoginForm callbackUrl={callbackUrl} />
|
||||||
initial={{
|
|
||||||
email: ''
|
|
||||||
}}
|
|
||||||
schema={EmailSchema}
|
|
||||||
onSubmit={async ({ email }) => {
|
|
||||||
signIn('email', { email, callbackUrl })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label='Email'
|
|
||||||
name='email'
|
|
||||||
placeholder='email@example.com'
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<SubmitButton variant='secondary' className={styles.providerButton}>Login with Email</SubmitButton>
|
|
||||||
</Form>
|
|
||||||
</>)}
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
</LayoutCenter>
|
</LayoutCenter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LnQRAuth ({ k1, encodedUrl, callbackUrl }) {
|
|
||||||
const query = gql`
|
|
||||||
{
|
|
||||||
lnAuth(k1: "${k1}") {
|
|
||||||
pubkey
|
|
||||||
k1
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
const { data } = useQuery(query, { pollInterval: 1000 })
|
|
||||||
|
|
||||||
if (data && data.lnAuth.pubkey) {
|
|
||||||
signIn('credentials', { ...data.lnAuth, callbackUrl })
|
|
||||||
}
|
|
||||||
|
|
||||||
// output pubkey and k1
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<small className='mb-2'>
|
|
||||||
<a className='text-muted text-underline' href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer' style={{ textDecoration: 'underline' }}>Does my wallet support lnurl-auth?</a>
|
|
||||||
</small>
|
|
||||||
<LnQR value={encodedUrl} status='waiting for you' />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LightningAuth ({ callbackUrl }) {
|
|
||||||
// query for challenge
|
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
|
||||||
mutation createAuth {
|
|
||||||
createAuth {
|
|
||||||
k1
|
|
||||||
encodedUrl
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
useEffect(createAuth, [])
|
|
||||||
|
|
||||||
if (error) return <div>error</div>
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <LnQRSkeleton status='generating' />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <LnQRAuth {...data.createAuth} callbackUrl={callbackUrl} />
|
|
||||||
}
|
|
||||||
|
@ -12,7 +12,7 @@ export default function ModalButton ({ children, clicker }) {
|
|||||||
>
|
>
|
||||||
<div className='modal-close' onClick={() => setShow(false)}>X</div>
|
<div className='modal-close' onClick={() => setShow(false)}>X</div>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{children}
|
{show && children}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className='pointer' onClick={() => setShow(true)}>{clicker}</div>
|
<div className='pointer' onClick={() => setShow(true)}>{clicker}</div>
|
||||||
|
@ -52,6 +52,26 @@ export const ME_SSR = gql`
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
export const SETTINGS = gql`
|
||||||
|
{
|
||||||
|
settings {
|
||||||
|
tipDefault
|
||||||
|
noteItemSats
|
||||||
|
noteEarning
|
||||||
|
noteAllDescendants
|
||||||
|
noteMentions
|
||||||
|
noteDeposits
|
||||||
|
noteInvites
|
||||||
|
noteJobIndicator
|
||||||
|
authMethods {
|
||||||
|
lightning
|
||||||
|
email
|
||||||
|
twitter
|
||||||
|
github
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
export const NAME_QUERY =
|
export const NAME_QUERY =
|
||||||
gql`
|
gql`
|
||||||
query nameAvailable($name: String!) {
|
query nameAvailable($name: String!) {
|
||||||
|
296
lib/prisma-adapter.js
Normal file
296
lib/prisma-adapter.js
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
exports.getCompoundId = getCompoundId
|
||||||
|
exports.Adapter = exports.PrismaLegacyAdapter = PrismaLegacyAdapter
|
||||||
|
|
||||||
|
const _crypto = require('crypto')
|
||||||
|
|
||||||
|
function getCompoundId (a, b) {
|
||||||
|
return (0, _crypto.createHash)('sha256').update(`${a}:${b}`).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrismaLegacyAdapter (config) {
|
||||||
|
const {
|
||||||
|
prisma,
|
||||||
|
modelMapping = {
|
||||||
|
User: 'user',
|
||||||
|
Account: 'account',
|
||||||
|
Session: 'session',
|
||||||
|
VerificationRequest: 'verificationRequest'
|
||||||
|
}
|
||||||
|
} = config
|
||||||
|
const {
|
||||||
|
User,
|
||||||
|
Account,
|
||||||
|
Session,
|
||||||
|
VerificationRequest
|
||||||
|
} = modelMapping
|
||||||
|
return {
|
||||||
|
async getAdapter ({
|
||||||
|
session: {
|
||||||
|
maxAge,
|
||||||
|
updateAge
|
||||||
|
},
|
||||||
|
secret,
|
||||||
|
...appOptions
|
||||||
|
}) {
|
||||||
|
const sessionMaxAge = maxAge * 1000
|
||||||
|
const sessionUpdateAge = updateAge * 1000
|
||||||
|
|
||||||
|
const hashToken = token => (0, _crypto.createHash)('sha256').update(`${token}${secret}`).digest('hex')
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: 'PRISMA_LEGACY',
|
||||||
|
|
||||||
|
createUser (profile) {
|
||||||
|
let _profile$emailVerifie
|
||||||
|
|
||||||
|
return prisma[User].create({
|
||||||
|
data: {
|
||||||
|
name: profile.name,
|
||||||
|
email: profile.email,
|
||||||
|
image: profile.image,
|
||||||
|
emailVerified: (_profile$emailVerifie = profile.emailVerified) === null || _profile$emailVerifie === void 0 ? void 0 : _profile$emailVerifie.toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getUser (id) {
|
||||||
|
return prisma[User].findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserByEmail (email) {
|
||||||
|
if (email) {
|
||||||
|
return prisma[User].findUnique({
|
||||||
|
where: {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserByProviderAccountId (providerId, providerAccountId) {
|
||||||
|
const account = await prisma[Account].findUnique({
|
||||||
|
where: {
|
||||||
|
compoundId: getCompoundId(providerId, providerAccountId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
return prisma[User].findUnique({
|
||||||
|
where: {
|
||||||
|
id: account.userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUser (user) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
image,
|
||||||
|
emailVerified
|
||||||
|
} = user
|
||||||
|
return prisma[User].update({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
image,
|
||||||
|
emailVerified: emailVerified === null || emailVerified === void 0 ? void 0 : emailVerified.toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUser (userId) {
|
||||||
|
return prisma[User].delete({
|
||||||
|
where: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||||
|
return prisma[Account].create({
|
||||||
|
data: {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
compoundId: getCompoundId(providerId, providerAccountId),
|
||||||
|
providerAccountId: `${providerAccountId}`,
|
||||||
|
providerId,
|
||||||
|
providerType,
|
||||||
|
accessTokenExpires,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
unlinkAccount (_, providerId, providerAccountId) {
|
||||||
|
return prisma[Account].delete({
|
||||||
|
where: {
|
||||||
|
compoundId: getCompoundId(providerId, providerAccountId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createSession (user) {
|
||||||
|
let expires = null
|
||||||
|
|
||||||
|
if (sessionMaxAge) {
|
||||||
|
const dateExpires = new Date()
|
||||||
|
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||||
|
expires = dateExpires.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma[Session].create({
|
||||||
|
data: {
|
||||||
|
expires,
|
||||||
|
userId: user.id,
|
||||||
|
sessionToken: (0, _crypto.randomBytes)(32).toString('hex'),
|
||||||
|
accessToken: (0, _crypto.randomBytes)(32).toString('hex')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSession (sessionToken) {
|
||||||
|
const session = await prisma[Session].findUnique({
|
||||||
|
where: {
|
||||||
|
sessionToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (session !== null && session !== void 0 && session.expires && new Date() > session.expires) {
|
||||||
|
await prisma[Session].delete({
|
||||||
|
where: {
|
||||||
|
sessionToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSession (session, force) {
|
||||||
|
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||||
|
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||||
|
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||||
|
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||||
|
|
||||||
|
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||||
|
const newExpiryDate = new Date()
|
||||||
|
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||||
|
session.expires = newExpiryDate
|
||||||
|
} else if (!force) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!force) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
expires
|
||||||
|
} = session
|
||||||
|
return prisma[Session].update({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
expires: expires.toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSession (sessionToken) {
|
||||||
|
return prisma[Session].delete({
|
||||||
|
where: {
|
||||||
|
sessionToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createVerificationRequest (identifier, url, token, _, provider) {
|
||||||
|
const {
|
||||||
|
sendVerificationRequest,
|
||||||
|
maxAge
|
||||||
|
} = provider
|
||||||
|
let expires = null
|
||||||
|
|
||||||
|
if (maxAge) {
|
||||||
|
const dateExpires = new Date()
|
||||||
|
dateExpires.setTime(dateExpires.getTime() + maxAge * 1000)
|
||||||
|
expires = dateExpires.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationRequest = await prisma[VerificationRequest].create({
|
||||||
|
data: {
|
||||||
|
identifier,
|
||||||
|
token: hashToken(token),
|
||||||
|
expires
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await sendVerificationRequest({
|
||||||
|
identifier,
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
baseUrl: appOptions.baseUrl,
|
||||||
|
provider
|
||||||
|
})
|
||||||
|
return verificationRequest
|
||||||
|
},
|
||||||
|
|
||||||
|
async getVerificationRequest (identifier, token) {
|
||||||
|
const hashedToken = hashToken(token)
|
||||||
|
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||||
|
where: {
|
||||||
|
identifier,
|
||||||
|
token: hashedToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||||
|
await prisma[VerificationRequest].deleteMany({
|
||||||
|
where: {
|
||||||
|
identifier,
|
||||||
|
token: hashedToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationRequest
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteVerificationRequest (identifier, token) {
|
||||||
|
await prisma[VerificationRequest].deleteMany({
|
||||||
|
where: {
|
||||||
|
identifier,
|
||||||
|
token: hashToken(token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
147
package-lock.json
generated
147
package-lock.json
generated
@ -1153,7 +1153,7 @@
|
|||||||
"any-promise": {
|
"any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
|
||||||
},
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
@ -2082,7 +2082,7 @@
|
|||||||
"buffer-equal-constant-time": {
|
"buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||||
},
|
},
|
||||||
"buffer-writer": {
|
"buffer-writer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3558,11 +3558,6 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"figlet": {
|
|
||||||
"version": "1.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz",
|
|
||||||
"integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ=="
|
|
||||||
},
|
|
||||||
"file-entry-cache": {
|
"file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@ -3771,9 +3766,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"futoin-hkdf": {
|
"futoin-hkdf": {
|
||||||
"version": "1.4.2",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.1.tgz",
|
||||||
"integrity": "sha512-2BggwLEJOTfXzKq4Tl2bIT37p0IqqKkblH4e0cMp2sXTdmwg/ADBKMxvxaEytYYcgdxgng8+acsi3WgMVUl6CQ=="
|
"integrity": "sha512-g5d0Qp7ks55hYmYmfqn4Nz18XH49lcCR+vvIvHT92xXnsJaGZmY1EtWQWilJ6BQp57heCIXM/rRo+AFep8hGgg=="
|
||||||
},
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
@ -3946,21 +3941,6 @@
|
|||||||
"function-bind": "^1.1.1"
|
"function-bind": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"has-ansi": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
|
|
||||||
"requires": {
|
|
||||||
"ansi-regex": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
|
||||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"has-bigints": {
|
"has-bigints": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
|
||||||
@ -4811,22 +4791,22 @@
|
|||||||
"lodash.includes": {
|
"lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||||
},
|
},
|
||||||
"lodash.isboolean": {
|
"lodash.isboolean": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||||
},
|
},
|
||||||
"lodash.isinteger": {
|
"lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||||
},
|
},
|
||||||
"lodash.isnumber": {
|
"lodash.isnumber": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||||
},
|
},
|
||||||
"lodash.isplainobject": {
|
"lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
@ -4836,7 +4816,7 @@
|
|||||||
"lodash.isstring": {
|
"lodash.isstring": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||||
},
|
},
|
||||||
"lodash.memoize": {
|
"lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
@ -4853,7 +4833,7 @@
|
|||||||
"lodash.once": {
|
"lodash.once": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||||
},
|
},
|
||||||
"lodash.sortby": {
|
"lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
@ -6086,9 +6066,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"next-auth": {
|
"next-auth": {
|
||||||
"version": "3.29.0",
|
"version": "3.29.3",
|
||||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-3.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-3.29.3.tgz",
|
||||||
"integrity": "sha512-B//4QTv/1Of0D+roZ82URmI6L2JSbkKgeaKI7Mdrioq8lAzp9ff8NdmouvZL/7zwrPe2cUyM6MLYlasfuI3ZIQ==",
|
"integrity": "sha512-OoG5y8oFV7MWF2VVs20AfdF41ndoXtPBFIlLfCHbrvFWHfPGsjnyAnhDxyJZX91Taknd4MD3zrCGOlBJKrLU7A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.14.0",
|
"@babel/runtime": "^7.14.0",
|
||||||
"@next-auth/prisma-legacy-adapter": "0.1.2",
|
"@next-auth/prisma-legacy-adapter": "0.1.2",
|
||||||
@ -6302,9 +6282,9 @@
|
|||||||
"integrity": "sha1-VWD8abweqQ46THhB8jPSAHU3hQM="
|
"integrity": "sha1-VWD8abweqQ46THhB8jPSAHU3hQM="
|
||||||
},
|
},
|
||||||
"nodemailer": {
|
"nodemailer": {
|
||||||
"version": "6.6.5",
|
"version": "6.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz",
|
||||||
"integrity": "sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A=="
|
"integrity": "sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg=="
|
||||||
},
|
},
|
||||||
"nofilter": {
|
"nofilter": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6355,7 +6335,7 @@
|
|||||||
"oauth": {
|
"oauth": {
|
||||||
"version": "0.9.15",
|
"version": "0.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@ -6615,11 +6595,6 @@
|
|||||||
"callsites": "^3.0.0"
|
"callsites": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parent-require": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc="
|
|
||||||
},
|
|
||||||
"parse-asn1": {
|
"parse-asn1": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
|
||||||
@ -7097,14 +7072,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preact": {
|
"preact": {
|
||||||
"version": "10.5.14",
|
"version": "10.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.7.3.tgz",
|
||||||
"integrity": "sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ=="
|
"integrity": "sha512-giqJXP8VbtA1tyGa3f1n9wiN7PrHtONrDyE3T+ifjr/tTkg+2N4d/6sjC9WyJKv8wM7rOYDveqy5ZoFmYlwo4w=="
|
||||||
},
|
},
|
||||||
"preact-render-to-string": {
|
"preact-render-to-string": {
|
||||||
"version": "5.1.19",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.1.19.tgz",
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.0.tgz",
|
||||||
"integrity": "sha512-bj8sn/oytIKO6RtOGSS/1+5CrQyRSC99eLUnEVbqUa6MzJX5dYh7wu9bmT0d6lm/Vea21k9KhCQwvr2sYN3rrQ==",
|
"integrity": "sha512-+RGwSW78Cl+NsZRUbFW1MGB++didsfqRk+IyRVTaqy+3OjtpKK/6HgBtfszUX0YXMfo41k2iaQSseAHGKEwrbg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"pretty-format": "^3.8.0"
|
"pretty-format": "^3.8.0"
|
||||||
}
|
}
|
||||||
@ -7123,7 +7098,7 @@
|
|||||||
"pretty-format": {
|
"pretty-format": {
|
||||||
"version": "3.8.0",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
"integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U="
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"version": "2.25.0",
|
"version": "2.25.0",
|
||||||
@ -7800,7 +7775,7 @@
|
|||||||
"resolve-from": {
|
"resolve-from": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
"integrity": "sha512-qpFcKaXsq8+oRoLilkwyc7zHGF5i9Q2/25NIgLQQ/+VVv9rU4qvr6nXVAw1DsnXJyQkZsR4Ytfbtg5ehfcUssQ=="
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
@ -9148,9 +9123,9 @@
|
|||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
"typeorm": {
|
"typeorm": {
|
||||||
"version": "0.2.37",
|
"version": "0.2.45",
|
||||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.37.tgz",
|
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz",
|
||||||
"integrity": "sha512-7rkW0yCgFC24I5T0f3S/twmLSuccPh1SQmxET/oDWn2sSDVzbyWdnItSdKy27CdJGTlKHYtUVeOcMYw5LRsXVw==",
|
"integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sqltools/formatter": "^1.2.2",
|
"@sqltools/formatter": "^1.2.2",
|
||||||
"app-root-path": "^3.0.0",
|
"app-root-path": "^3.0.0",
|
||||||
@ -9165,8 +9140,8 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sha.js": "^2.4.11",
|
"sha.js": "^2.4.11",
|
||||||
"tslib": "^2.1.0",
|
"tslib": "^2.1.0",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"xml2js": "^0.4.23",
|
"xml2js": "^0.4.23",
|
||||||
"yargonaut": "^1.1.4",
|
|
||||||
"yargs": "^17.0.1",
|
"yargs": "^17.0.1",
|
||||||
"zen-observable-ts": "^1.0.0"
|
"zen-observable-ts": "^1.0.0"
|
||||||
},
|
},
|
||||||
@ -9237,18 +9212,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "17.2.1",
|
"version": "17.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
|
||||||
"integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==",
|
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"cliui": "^7.0.2",
|
"cliui": "^7.0.2",
|
||||||
"escalade": "^3.1.1",
|
"escalade": "^3.1.1",
|
||||||
"get-caller-file": "^2.0.5",
|
"get-caller-file": "^2.0.5",
|
||||||
"require-directory": "^2.1.1",
|
"require-directory": "^2.1.1",
|
||||||
"string-width": "^4.2.0",
|
"string-width": "^4.2.3",
|
||||||
"y18n": "^5.0.5",
|
"y18n": "^5.0.5",
|
||||||
"yargs-parser": "^20.2.2"
|
"yargs-parser": "^21.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"yargs-parser": {
|
||||||
|
"version": "21.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
|
||||||
|
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -9779,53 +9759,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
"yargonaut": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==",
|
|
||||||
"requires": {
|
|
||||||
"chalk": "^1.1.1",
|
|
||||||
"figlet": "^1.1.1",
|
|
||||||
"parent-require": "^1.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
|
||||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
|
||||||
},
|
|
||||||
"ansi-styles": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
|
||||||
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
|
|
||||||
},
|
|
||||||
"chalk": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
|
||||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
|
||||||
"requires": {
|
|
||||||
"ansi-styles": "^2.2.1",
|
|
||||||
"escape-string-regexp": "^1.0.2",
|
|
||||||
"has-ansi": "^2.0.0",
|
|
||||||
"strip-ansi": "^3.0.0",
|
|
||||||
"supports-color": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-ansi": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
|
||||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
|
||||||
"requires": {
|
|
||||||
"ansi-regex": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"supports-color": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"ln-service": "^52.8.0",
|
"ln-service": "^52.8.0",
|
||||||
"mdast-util-find-and-replace": "^1.1.1",
|
"mdast-util-find-and-replace": "^1.1.1",
|
||||||
"next": "^11.1.2",
|
"next": "^11.1.2",
|
||||||
"next-auth": "^3.13.3",
|
"next-auth": "^3.29.3",
|
||||||
"next-plausible": "^2.1.3",
|
"next-plausible": "^2.1.3",
|
||||||
"next-seo": "^4.24.0",
|
"next-seo": "^4.24.0",
|
||||||
"nextjs-progressbar": "^0.0.13",
|
"nextjs-progressbar": "^0.0.13",
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import NextAuth from 'next-auth'
|
import NextAuth from 'next-auth'
|
||||||
import Providers from 'next-auth/providers'
|
import Providers from 'next-auth/providers'
|
||||||
import Adapters from 'next-auth/adapters'
|
import { PrismaLegacyAdapter } from '../../../lib/prisma-adapter'
|
||||||
import prisma from '../../../api/models'
|
import prisma from '../../../api/models'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
|
import { getSession } from 'next-auth/client'
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
export default (req, res) => NextAuth(req, res, options)
|
||||||
|
|
||||||
@ -19,7 +20,10 @@ const options = {
|
|||||||
async jwt (token, user, account, profile, isNewUser) {
|
async jwt (token, user, account, profile, isNewUser) {
|
||||||
// Add additional session params
|
// Add additional session params
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
token.id = user.id
|
token.id = Number(user.id)
|
||||||
|
// HACK next-auth needs this to do account linking with jwts
|
||||||
|
// see: https://github.com/nextauthjs/next-auth/issues/625
|
||||||
|
token.user = { id: Number(user.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign them up for the newsletter
|
// sign them up for the newsletter
|
||||||
@ -44,7 +48,7 @@ const options = {
|
|||||||
},
|
},
|
||||||
async session (session, token) {
|
async session (session, token) {
|
||||||
// we need to add additional session params here
|
// we need to add additional session params here
|
||||||
session.user.id = token.id
|
session.user.id = Number(token.id)
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -65,9 +69,18 @@ const options = {
|
|||||||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||||
if (lnauth.pubkey === pubkey) {
|
if (lnauth.pubkey === pubkey) {
|
||||||
let user = await prisma.user.findUnique({ where: { pubkey } })
|
let user = await prisma.user.findUnique({ where: { pubkey } })
|
||||||
|
const session = await getSession({ req })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
// if we are logged in, update rather than create
|
||||||
|
if (session?.user) {
|
||||||
|
user = await prisma.user.update({ where: { id: session.user.id }, data: { pubkey } })
|
||||||
|
} else {
|
||||||
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), pubkey } })
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), pubkey } })
|
||||||
}
|
}
|
||||||
|
} else if (session && session.user?.id !== user.id) {
|
||||||
|
throw new Error('account not linked')
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.lnAuth.delete({ where: { k1 } })
|
await prisma.lnAuth.delete({ where: { k1 } })
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
@ -108,7 +121,7 @@ const options = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
adapter: PrismaLegacyAdapter({ prisma }),
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
session: { jwt: true },
|
session: { jwt: true },
|
||||||
jwt: {
|
jwt: {
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
import { Checkbox, Form, Input, SubmitButton } from '../components/form'
|
import { Checkbox, Form, Input, SubmitButton } from '../components/form'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { Alert, Button, InputGroup } from 'react-bootstrap'
|
import { Alert, Button, InputGroup, Modal } from 'react-bootstrap'
|
||||||
import { useMe } from '../components/me'
|
|
||||||
import LayoutCenter from '../components/layout-center'
|
import LayoutCenter from '../components/layout-center'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
|
import LoginButton from '../components/login-button'
|
||||||
|
import { signIn } from 'next-auth/client'
|
||||||
|
import ModalButton from '../components/modal-button'
|
||||||
|
import { LightningAuth } from '../components/lightning-auth'
|
||||||
|
import { SETTINGS } from '../fragments/users'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps()
|
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||||
|
|
||||||
export const SettingsSchema = Yup.object({
|
export const SettingsSchema = Yup.object({
|
||||||
tipDefault: Yup.number().typeError('must be a number').required('required')
|
tipDefault: Yup.number().typeError('must be a number').required('required')
|
||||||
.positive('must be positive').integer('must be whole')
|
.positive('must be positive').integer('must be whole')
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function Settings () {
|
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
|
||||||
const me = useMe()
|
|
||||||
|
export const WarningSchema = Yup.object({
|
||||||
|
warning: Yup.string().matches(warningMessage, 'does not match').required('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function Settings ({ data: { settings } }) {
|
||||||
const [success, setSuccess] = useState()
|
const [success, setSuccess] = useState()
|
||||||
const [setSettings] = useMutation(
|
const [setSettings] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -29,19 +39,25 @@ export default function Settings () {
|
|||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { data } = useQuery(SETTINGS)
|
||||||
|
if (data) {
|
||||||
|
({ settings } = data)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutCenter>
|
<LayoutCenter>
|
||||||
<h2 className='mb-5 text-left'>settings</h2>
|
<div className='py-3 w-100'>
|
||||||
|
<h2 className='mb-2 text-left'>settings</h2>
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
tipDefault: me?.tipDefault || 21,
|
tipDefault: settings?.tipDefault || 21,
|
||||||
noteItemSats: me?.noteItemSats,
|
noteItemSats: settings?.noteItemSats,
|
||||||
noteEarning: me?.noteEarning,
|
noteEarning: settings?.noteEarning,
|
||||||
noteAllDescendants: me?.noteAllDescendants,
|
noteAllDescendants: settings?.noteAllDescendants,
|
||||||
noteMentions: me?.noteMentions,
|
noteMentions: settings?.noteMentions,
|
||||||
noteDeposits: me?.noteDeposits,
|
noteDeposits: settings?.noteDeposits,
|
||||||
noteInvites: me?.noteInvites,
|
noteInvites: settings?.noteInvites,
|
||||||
noteJobIndicator: me?.noteJobIndicator
|
noteJobIndicator: settings?.noteJobIndicator
|
||||||
}}
|
}}
|
||||||
schema={SettingsSchema}
|
schema={SettingsSchema}
|
||||||
onSubmit={async ({ tipDefault, ...values }) => {
|
onSubmit={async ({ tipDefault, ...values }) => {
|
||||||
@ -92,12 +108,191 @@ export default function Settings () {
|
|||||||
label='there is a new job'
|
label='there is a new job'
|
||||||
name='noteJobIndicator'
|
name='noteJobIndicator'
|
||||||
/>
|
/>
|
||||||
<div className='form-label'>saturday newsletter</div>
|
|
||||||
<Button href='https://mail.stacker.news/subscription/form' target='_blank'>(re)subscribe</Button>
|
|
||||||
<div className='d-flex'>
|
<div className='d-flex'>
|
||||||
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<div className='text-left w-100'>
|
||||||
|
<div className='form-label'>saturday newsletter</div>
|
||||||
|
<Button href='https://mail.stacker.news/subscription/form' target='_blank'>(re)subscribe</Button>
|
||||||
|
{settings?.authMethods && <AuthMethods methods={settings.authMethods} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</LayoutCenter>
|
</LayoutCenter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AuthMethods ({ methods }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const [unlinkAuth] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation unlinkAuth($authType: String!) {
|
||||||
|
unlinkAuth(authType: $authType) {
|
||||||
|
lightning
|
||||||
|
email
|
||||||
|
twitter
|
||||||
|
github
|
||||||
|
}
|
||||||
|
}`, {
|
||||||
|
update (cache, { data: { unlinkAuth } }) {
|
||||||
|
cache.modify({
|
||||||
|
id: 'ROOT_QUERY',
|
||||||
|
fields: {
|
||||||
|
settings (existing) {
|
||||||
|
return { ...existing, authMethods: { ...unlinkAuth } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const [obstacle, setObstacle] = useState()
|
||||||
|
|
||||||
|
const unlink = async type => {
|
||||||
|
// if there's only one auth method left
|
||||||
|
let links = 0
|
||||||
|
links += methods.lightning ? 1 : 0
|
||||||
|
links += methods.email ? 1 : 0
|
||||||
|
links += methods.twitter ? 1 : 0
|
||||||
|
links += methods.github ? 1 : 0
|
||||||
|
|
||||||
|
if (links === 1) {
|
||||||
|
setObstacle(type)
|
||||||
|
} else {
|
||||||
|
await unlinkAuth({ variables: { authType: type } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
show={obstacle}
|
||||||
|
onHide={() => setObstacle(null)}
|
||||||
|
>
|
||||||
|
<div className='modal-close' onClick={() => setObstacle(null)}>X</div>
|
||||||
|
<Modal.Body>
|
||||||
|
You are removing your last auth method. It is recommended you link another auth method before removing
|
||||||
|
your last auth method. If you'd like to proceed anyway, type the following below
|
||||||
|
<div className='text-danger font-weight-bold my-2'>
|
||||||
|
If I logout, even accidentally, I will never be able to access my account again
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
className='mt-3'
|
||||||
|
initial={{
|
||||||
|
warning: ''
|
||||||
|
}}
|
||||||
|
schema={WarningSchema}
|
||||||
|
onSubmit={async () => {
|
||||||
|
await unlinkAuth({ variables: { authType: obstacle } })
|
||||||
|
router.push('/settings')
|
||||||
|
setObstacle(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name='warning'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<SubmitButton className='d-flex ml-auto' variant='danger'>do it</SubmitButton>
|
||||||
|
</Form>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
<div className='form-label mt-3'>auth methods</div>
|
||||||
|
{methods.lightning
|
||||||
|
? <LoginButton
|
||||||
|
className='d-block' type='lightning' text='Unlink' onClick={
|
||||||
|
async () => {
|
||||||
|
await unlink('lightning')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: (
|
||||||
|
<ModalButton clicker={<LoginButton className='d-block' type='lightning' text='Link' />}>
|
||||||
|
<div className='d-flex flex-column align-items-center'>
|
||||||
|
<LightningAuth />
|
||||||
|
</div>
|
||||||
|
</ModalButton>)}
|
||||||
|
<LoginButton
|
||||||
|
className='d-block mt-2' type='twitter' text={methods.twitter ? 'Unlink' : 'Link'} onClick={
|
||||||
|
async () => {
|
||||||
|
if (methods.twitter) {
|
||||||
|
await unlink('twitter')
|
||||||
|
} else {
|
||||||
|
signIn('twitter')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<LoginButton
|
||||||
|
className='d-block mt-2' type='github' text={methods.github ? 'Unlink' : 'Link'} onClick={
|
||||||
|
async () => {
|
||||||
|
if (methods.github) {
|
||||||
|
await unlink('github')
|
||||||
|
} else {
|
||||||
|
signIn('github')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{methods.email
|
||||||
|
? (
|
||||||
|
<div className='mt-2 d-flex align-items-center'>
|
||||||
|
<Input
|
||||||
|
name='email'
|
||||||
|
placeholder={methods.email}
|
||||||
|
groupClassName='mb-0'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='ml-2' variant='secondary' onClick={
|
||||||
|
async () => {
|
||||||
|
await unlink('email')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>Unlink Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <div className='mt-2'><EmailLinkForm /></div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailSchema = Yup.object({
|
||||||
|
email: Yup.string().email('email is no good').required('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
export function EmailLinkForm ({ callbackUrl }) {
|
||||||
|
const [linkUnverifiedEmail] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation linkUnverifiedEmail($email: String!) {
|
||||||
|
linkUnverifiedEmail(email: $email)
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
email: ''
|
||||||
|
}}
|
||||||
|
schema={EmailSchema}
|
||||||
|
onSubmit={async ({ email }) => {
|
||||||
|
// add email to user's account
|
||||||
|
// then call signIn
|
||||||
|
const { data } = await linkUnverifiedEmail({ variables: { email } })
|
||||||
|
if (data.linkUnverifiedEmail) {
|
||||||
|
signIn('email', { email, callbackUrl })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<Input
|
||||||
|
name='email'
|
||||||
|
placeholder='email@example.com'
|
||||||
|
required
|
||||||
|
groupClassName='mb-0'
|
||||||
|
/>
|
||||||
|
<SubmitButton className='ml-2' variant='secondary'>Link Email</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user