diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..972944bd
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "search.useIgnoreFiles": true
+}
\ No newline at end of file
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 595ee8d6..10602964 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -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 } })
},
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 32fc324f..1214eb57 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -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!
}
`
diff --git a/components/lightning-auth.js b/components/lightning-auth.js
new file mode 100644
index 00000000..734e6ce6
--- /dev/null
+++ b/components/lightning-auth.js
@@ -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 (
+ <>
+
+ Does my wallet support lnurl-auth?
+
+
+ >
+ )
+}
+
+export function LightningAuth ({ callbackUrl }) {
+ // query for challenge
+ const [createAuth, { data, error }] = useMutation(gql`
+ mutation createAuth {
+ createAuth {
+ k1
+ encodedUrl
+ }
+ }`)
+
+ useEffect(createAuth, [])
+
+ if (error) return
error
+
+ if (!data) {
+ return
+ }
+
+ return
+}
diff --git a/components/login-button.js b/components/login-button.js
new file mode 100644
index 00000000..88a77cc4
--- /dev/null
+++ b/components/login-button.js
@@ -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 (
+
+ )
+}
diff --git a/components/login.js b/components/login.js
index 6244c817..cbae1f64 100644
--- a/components/login.js
+++ b/components/login.js
@@ -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 (
+
+ )
+}
+
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 }) {
)
})}
or
-
+
>)}
)
}
-
-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 (
- <>
-
- Does my wallet support lnurl-auth?
-
-
- >
- )
-}
-
-export function LightningAuth ({ callbackUrl }) {
- // query for challenge
- const [createAuth, { data, error }] = useMutation(gql`
- mutation createAuth {
- createAuth {
- k1
- encodedUrl
- }
- }`)
-
- useEffect(createAuth, [])
-
- if (error) return error
-
- if (!data) {
- return
- }
-
- return
-}
diff --git a/components/modal-button.js b/components/modal-button.js
index 9b71c77b..cb06457d 100644
--- a/components/modal-button.js
+++ b/components/modal-button.js
@@ -12,7 +12,7 @@ export default function ModalButton ({ children, clicker }) {
>
setShow(false)}>X
- {children}
+ {show && children}
setShow(true)}>{clicker}
diff --git a/fragments/users.js b/fragments/users.js
index 1b9fcb61..e0301264 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -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!) {
diff --git a/lib/prisma-adapter.js b/lib/prisma-adapter.js
new file mode 100644
index 00000000..442533f3
--- /dev/null
+++ b/lib/prisma-adapter.js
@@ -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)
+ }
+ })
+ }
+
+ }
+ }
+
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index e0f1df6f..9e6a558e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index de7c6083..53ae7bb5 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
index d5ae4509..e104714a 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].js
@@ -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: {
diff --git a/pages/settings.js b/pages/settings.js
index 831c57d6..7105beae 100644
--- a/pages/settings.js
+++ b/pages/settings.js
@@ -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 (
- settings
-