account linking
This commit is contained in:
parent
dd4be45ae8
commit
1df49e03d9
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.useIgnoreFiles": true
|
||||
}
|
|
@ -25,6 +25,23 @@ export function topClause (within) {
|
|||
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 {
|
||||
Query: {
|
||||
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() } })
|
||||
},
|
||||
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 }) => {
|
||||
return await models.user.findUnique({ where: { name } })
|
||||
},
|
||||
|
@ -152,10 +176,54 @@ export default {
|
|||
await createMentions(item, models)
|
||||
|
||||
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: {
|
||||
authMethods,
|
||||
nitems: async (user, args, { models }) => {
|
||||
return await models.item.count({ where: { userId: user.id, parentId: null } })
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
|
|||
export default gql`
|
||||
extend type Query {
|
||||
me: User
|
||||
settings: User
|
||||
user(name: String!): User
|
||||
users: [User!]
|
||||
nameAvailable(name: String!): Boolean!
|
||||
|
@ -33,6 +34,15 @@ export default gql`
|
|||
setPhoto(photoId: ID!): Int!
|
||||
upsertBio(bio: String!): User!
|
||||
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 {
|
||||
|
@ -61,5 +71,6 @@ export default gql`
|
|||
noteInvites: Boolean!
|
||||
noteJobIndicator: Boolean!
|
||||
lastCheckedJobs: String
|
||||
authMethods: AuthMethods!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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} />
|
||||
}
|
|
@ -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 { Form, Input, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Alert from 'react-bootstrap/Alert'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import { useRouter } from 'next/router'
|
||||
import LnQR, { LnQRSkeleton } from '../components/lnqr'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import { LightningAuth } from './lightning-auth'
|
||||
|
||||
export const EmailSchema = Yup.object({
|
||||
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 }) {
|
||||
const errors = {
|
||||
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>
|
||||
<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>
|
||||
<EmailLoginForm callbackUrl={callbackUrl} />
|
||||
</>)}
|
||||
</div>
|
||||
</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>
|
||||
<Modal.Body>
|
||||
{children}
|
||||
{show && children}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
<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 =
|
||||
gql`
|
||||
query nameAvailable($name: String!) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1153,7 +1153,7 @@
|
|||
"any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
|
@ -2082,7 +2082,7 @@
|
|||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
|
@ -3558,11 +3558,6 @@
|
|||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
@ -3771,9 +3766,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"futoin-hkdf": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.2.tgz",
|
||||
"integrity": "sha512-2BggwLEJOTfXzKq4Tl2bIT37p0IqqKkblH4e0cMp2sXTdmwg/ADBKMxvxaEytYYcgdxgng8+acsi3WgMVUl6CQ=="
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.1.tgz",
|
||||
"integrity": "sha512-g5d0Qp7ks55hYmYmfqn4Nz18XH49lcCR+vvIvHT92xXnsJaGZmY1EtWQWilJ6BQp57heCIXM/rRo+AFep8hGgg=="
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
|
@ -3946,21 +3941,6 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
|
||||
|
@ -4811,22 +4791,22 @@
|
|||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"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": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"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": {
|
||||
"version": "4.0.6",
|
||||
|
@ -4836,7 +4816,7 @@
|
|||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"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": {
|
||||
"version": "4.1.2",
|
||||
|
@ -4853,7 +4833,7 @@
|
|||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
|
@ -6086,9 +6066,9 @@
|
|||
}
|
||||
},
|
||||
"next-auth": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-3.29.0.tgz",
|
||||
"integrity": "sha512-B//4QTv/1Of0D+roZ82URmI6L2JSbkKgeaKI7Mdrioq8lAzp9ff8NdmouvZL/7zwrPe2cUyM6MLYlasfuI3ZIQ==",
|
||||
"version": "3.29.3",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-3.29.3.tgz",
|
||||
"integrity": "sha512-OoG5y8oFV7MWF2VVs20AfdF41ndoXtPBFIlLfCHbrvFWHfPGsjnyAnhDxyJZX91Taknd4MD3zrCGOlBJKrLU7A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@next-auth/prisma-legacy-adapter": "0.1.2",
|
||||
|
@ -6302,9 +6282,9 @@
|
|||
"integrity": "sha1-VWD8abweqQ46THhB8jPSAHU3hQM="
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "6.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.5.tgz",
|
||||
"integrity": "sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A=="
|
||||
"version": "6.7.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz",
|
||||
"integrity": "sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg=="
|
||||
},
|
||||
"nofilter": {
|
||||
"version": "3.0.3",
|
||||
|
@ -6355,7 +6335,7 @@
|
|||
"oauth": {
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -6615,11 +6595,6 @@
|
|||
"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": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
|
||||
|
@ -7097,14 +7072,14 @@
|
|||
}
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.5.14",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.14.tgz",
|
||||
"integrity": "sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ=="
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.7.3.tgz",
|
||||
"integrity": "sha512-giqJXP8VbtA1tyGa3f1n9wiN7PrHtONrDyE3T+ifjr/tTkg+2N4d/6sjC9WyJKv8wM7rOYDveqy5ZoFmYlwo4w=="
|
||||
},
|
||||
"preact-render-to-string": {
|
||||
"version": "5.1.19",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.1.19.tgz",
|
||||
"integrity": "sha512-bj8sn/oytIKO6RtOGSS/1+5CrQyRSC99eLUnEVbqUa6MzJX5dYh7wu9bmT0d6lm/Vea21k9KhCQwvr2sYN3rrQ==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.0.tgz",
|
||||
"integrity": "sha512-+RGwSW78Cl+NsZRUbFW1MGB++didsfqRk+IyRVTaqy+3OjtpKK/6HgBtfszUX0YXMfo41k2iaQSseAHGKEwrbg==",
|
||||
"requires": {
|
||||
"pretty-format": "^3.8.0"
|
||||
}
|
||||
|
@ -7123,7 +7098,7 @@
|
|||
"pretty-format": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U="
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
||||
},
|
||||
"prisma": {
|
||||
"version": "2.25.0",
|
||||
|
@ -7800,7 +7775,7 @@
|
|||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"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": {
|
||||
"version": "5.7.1",
|
||||
|
@ -9148,9 +9123,9 @@
|
|||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||
},
|
||||
"typeorm": {
|
||||
"version": "0.2.37",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.37.tgz",
|
||||
"integrity": "sha512-7rkW0yCgFC24I5T0f3S/twmLSuccPh1SQmxET/oDWn2sSDVzbyWdnItSdKy27CdJGTlKHYtUVeOcMYw5LRsXVw==",
|
||||
"version": "0.2.45",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz",
|
||||
"integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==",
|
||||
"requires": {
|
||||
"@sqltools/formatter": "^1.2.2",
|
||||
"app-root-path": "^3.0.0",
|
||||
|
@ -9165,8 +9140,8 @@
|
|||
"reflect-metadata": "^0.1.13",
|
||||
"sha.js": "^2.4.11",
|
||||
"tslib": "^2.1.0",
|
||||
"uuid": "^8.3.2",
|
||||
"xml2js": "^0.4.23",
|
||||
"yargonaut": "^1.1.4",
|
||||
"yargs": "^17.0.1",
|
||||
"zen-observable-ts": "^1.0.0"
|
||||
},
|
||||
|
@ -9237,18 +9212,23 @@
|
|||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz",
|
||||
"integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==",
|
||||
"version": "17.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
|
||||
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
|
||||
"requires": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"string-width": "^4.2.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"ln-service": "^52.8.0",
|
||||
"mdast-util-find-and-replace": "^1.1.1",
|
||||
"next": "^11.1.2",
|
||||
"next-auth": "^3.13.3",
|
||||
"next-auth": "^3.29.3",
|
||||
"next-plausible": "^2.1.3",
|
||||
"next-seo": "^4.24.0",
|
||||
"nextjs-progressbar": "^0.0.13",
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import Adapters from 'next-auth/adapters'
|
||||
import { PrismaLegacyAdapter } from '../../../lib/prisma-adapter'
|
||||
import prisma from '../../../api/models'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
|
||||
|
@ -19,7 +20,10 @@ const options = {
|
|||
async jwt (token, user, account, profile, isNewUser) {
|
||||
// Add additional session params
|
||||
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
|
||||
|
@ -44,7 +48,7 @@ const options = {
|
|||
},
|
||||
async session (session, token) {
|
||||
// we need to add additional session params here
|
||||
session.user.id = token.id
|
||||
session.user.id = Number(token.id)
|
||||
return session
|
||||
}
|
||||
},
|
||||
|
@ -65,9 +69,18 @@ const options = {
|
|||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||
if (lnauth.pubkey === pubkey) {
|
||||
let user = await prisma.user.findUnique({ where: { pubkey } })
|
||||
const session = await getSession({ req })
|
||||
if (!user) {
|
||||
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), pubkey } })
|
||||
// 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 } })
|
||||
}
|
||||
} else if (session && session.user?.id !== user.id) {
|
||||
throw new Error('account not linked')
|
||||
}
|
||||
|
||||
await prisma.lnAuth.delete({ where: { k1 } })
|
||||
return user
|
||||
}
|
||||
|
@ -108,7 +121,7 @@ const options = {
|
|||
}
|
||||
})
|
||||
],
|
||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||
adapter: PrismaLegacyAdapter({ prisma }),
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
session: { jwt: true },
|
||||
jwt: {
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
import { Checkbox, Form, Input, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { Alert, Button, InputGroup } from 'react-bootstrap'
|
||||
import { useMe } from '../components/me'
|
||||
import { Alert, Button, InputGroup, Modal } from 'react-bootstrap'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import { useState } from 'react'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
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({
|
||||
tipDefault: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole')
|
||||
})
|
||||
|
||||
export default function Settings () {
|
||||
const me = useMe()
|
||||
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
|
||||
|
||||
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 [setSettings] = useMutation(
|
||||
gql`
|
||||
|
@ -29,75 +39,260 @@ export default function Settings () {
|
|||
}`
|
||||
)
|
||||
|
||||
const { data } = useQuery(SETTINGS)
|
||||
if (data) {
|
||||
({ settings } = data)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutCenter>
|
||||
<h2 className='mb-5 text-left'>settings</h2>
|
||||
<Form
|
||||
initial={{
|
||||
tipDefault: me?.tipDefault || 21,
|
||||
noteItemSats: me?.noteItemSats,
|
||||
noteEarning: me?.noteEarning,
|
||||
noteAllDescendants: me?.noteAllDescendants,
|
||||
noteMentions: me?.noteMentions,
|
||||
noteDeposits: me?.noteDeposits,
|
||||
noteInvites: me?.noteInvites,
|
||||
noteJobIndicator: me?.noteJobIndicator
|
||||
}}
|
||||
schema={SettingsSchema}
|
||||
onSubmit={async ({ tipDefault, ...values }) => {
|
||||
await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } })
|
||||
setSuccess('settings saved')
|
||||
}}
|
||||
>
|
||||
{success && <Alert variant='info' onClose={() => setSuccess(undefined)} dismissible>{success}</Alert>}
|
||||
<Input
|
||||
label='tip default'
|
||||
name='tipDefault'
|
||||
required
|
||||
autoFocus
|
||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||
/>
|
||||
<div className='form-label'>notify me when ...</div>
|
||||
<Checkbox
|
||||
label='I stack sats from posts and comments'
|
||||
name='noteItemSats'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='I get a daily airdrop'
|
||||
name='noteEarning'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='someone replies to someone who replied to me'
|
||||
name='noteAllDescendants'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='my invite links are redeemed'
|
||||
name='noteInvites'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='sats are deposited in my account'
|
||||
name='noteDeposits'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='someone mentions me'
|
||||
name='noteMentions'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='there is a new job'
|
||||
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'>
|
||||
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
||||
<div className='py-3 w-100'>
|
||||
<h2 className='mb-2 text-left'>settings</h2>
|
||||
<Form
|
||||
initial={{
|
||||
tipDefault: settings?.tipDefault || 21,
|
||||
noteItemSats: settings?.noteItemSats,
|
||||
noteEarning: settings?.noteEarning,
|
||||
noteAllDescendants: settings?.noteAllDescendants,
|
||||
noteMentions: settings?.noteMentions,
|
||||
noteDeposits: settings?.noteDeposits,
|
||||
noteInvites: settings?.noteInvites,
|
||||
noteJobIndicator: settings?.noteJobIndicator
|
||||
}}
|
||||
schema={SettingsSchema}
|
||||
onSubmit={async ({ tipDefault, ...values }) => {
|
||||
await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } })
|
||||
setSuccess('settings saved')
|
||||
}}
|
||||
>
|
||||
{success && <Alert variant='info' onClose={() => setSuccess(undefined)} dismissible>{success}</Alert>}
|
||||
<Input
|
||||
label='tip default'
|
||||
name='tipDefault'
|
||||
required
|
||||
autoFocus
|
||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||
/>
|
||||
<div className='form-label'>notify me when ...</div>
|
||||
<Checkbox
|
||||
label='I stack sats from posts and comments'
|
||||
name='noteItemSats'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='I get a daily airdrop'
|
||||
name='noteEarning'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='someone replies to someone who replied to me'
|
||||
name='noteAllDescendants'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='my invite links are redeemed'
|
||||
name='noteInvites'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='sats are deposited in my account'
|
||||
name='noteDeposits'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='someone mentions me'
|
||||
name='noteMentions'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='there is a new job'
|
||||
name='noteJobIndicator'
|
||||
/>
|
||||
<div className='d-flex'>
|
||||
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
||||
</div>
|
||||
</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>
|
||||
</Form>
|
||||
</div>
|
||||
</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…
Reference in New Issue