diff --git a/api/resolvers/index.js b/api/resolvers/index.js index 9bfe0ea4..484e2f7d 100644 --- a/api/resolvers/index.js +++ b/api/resolvers/index.js @@ -2,5 +2,6 @@ import user from './user' import message from './message' import item from './item' import wallet from './wallet' +import lnurl from './lnurl' -export default [user, item, message, wallet] +export default [user, item, message, wallet, lnurl] diff --git a/api/resolvers/item.js b/api/resolvers/item.js index b721c4b5..9690b77e 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -91,7 +91,6 @@ export default { if (!me) { throw new AuthenticationError('you must be logged in') } - const user = await models.user.findUnique({ where: { name: me.name } }) comments = await models.$queryRaw(` ${SELECT} From "Item" @@ -99,7 +98,7 @@ export default { AND "Item"."userId" <> $1 AND "Item".created_at <= $2 ORDER BY "Item".created_at DESC OFFSET $3 - LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset) + LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset) } return { cursor: comments.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, @@ -110,14 +109,13 @@ export default { if (!me) { throw new AuthenticationError('you must be logged in') } - const user = await models.user.findUnique({ where: { name: me.name } }) return await models.$queryRaw(` ${SELECT} From "Item" JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = $1 AND "Item"."userId" <> $1 - ORDER BY "Item".created_at DESC`, user.id) + ORDER BY "Item".created_at DESC`, me.id) }, item: async (parent, { id }, { models }) => { const [item] = await models.$queryRaw(` @@ -229,15 +227,13 @@ export default { meSats: async (item, args, { me, models }) => { if (!me) return 0 - const meFull = await models.user.findUnique({ where: { name: me.name } }) - const { sum: { sats } } = await models.vote.aggregate({ sum: { sats: true }, where: { itemId: item.id, - userId: meFull.id + userId: me.id } }) diff --git a/api/resolvers/user.js b/api/resolvers/user.js index eab54024..9a198482 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -3,7 +3,7 @@ import { AuthenticationError, UserInputError } from 'apollo-server-errors' export default { Query: { me: async (parent, args, { models, me }) => - me ? await models.user.findUnique({ where: { name: me.name } }) : null, + me ? await models.user.findUnique({ where: { id: me.id } }) : null, user: async (parent, { name }, { models }) => { return await models.user.findUnique({ where: { name } }) }, @@ -21,7 +21,7 @@ export default { throw new AuthenticationError('you must be logged in') } - const user = await models.user.findUnique({ where: { name: me.name } }) + const user = await models.user.findUnique({ where: { id: me.id } }) const [{ sum }] = await models.$queryRaw(` SELECT sum("Vote".sats) @@ -32,7 +32,7 @@ export default { AND "Vote".boost = false WHERE "Item"."userId" = $1`, user.id, user.checkedNotesAt) - await models.user.update({ where: { name: me.name }, data: { checkedNotesAt: new Date() } }) + await models.user.update({ where: { id: me.id }, data: { checkedNotesAt: new Date() } }) return sum || 0 } }, @@ -44,7 +44,7 @@ export default { } try { - await models.user.update({ where: { name: me.name }, data: { name } }) + await models.user.update({ where: { id: me.id }, data: { name } }) } catch (error) { if (error.code === 'P2002') { throw new UserInputError('name taken') diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index ff4e3e7d..7d89e6fd 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -18,7 +18,7 @@ export default { } }) - if (inv.user.name !== me.name) { + if (inv.user.id !== me.id) { throw new AuthenticationError('not ur invoice') } @@ -38,7 +38,7 @@ export default { } }) - if (wdrwl.user.name !== me.name) { + if (wdrwl.user.id !== me.id) { throw new AuthenticationError('not ur withdrawl') } @@ -77,7 +77,7 @@ export default { msatsRequested: amount * 1000, user: { connect: { - name: me.name + id: me.id } } } diff --git a/api/typeDefs/index.js b/api/typeDefs/index.js index 1bccc60a..025546b9 100644 --- a/api/typeDefs/index.js +++ b/api/typeDefs/index.js @@ -4,6 +4,7 @@ import user from './user' import message from './message' import item from './item' import wallet from './wallet' +import lnurl from './lnurl' const link = gql` type Query { @@ -19,4 +20,4 @@ const link = gql` } ` -export default [link, user, item, message, wallet] +export default [link, user, item, message, wallet, lnurl] diff --git a/components/invoice.js b/components/invoice.js index dba26121..51230dc7 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -1,10 +1,6 @@ -import QRCode from 'qrcode.react' -import { CopyInput, InputSkeleton } from './form' -import InvoiceStatus from './invoice-status' +import LnQR from './lnqr' export function Invoice ({ invoice }) { - const qrValue = 'lightning:' + invoice.bolt11.toUpperCase() - let variant = 'default' let status = 'waiting for you' if (invoice.confirmedAt) { @@ -18,27 +14,5 @@ export function Invoice ({ invoice }) { status = 'expired' } - return ( - <> -
- -
-
- -
- - - ) -} - -export function InvoiceSkeleton ({ status }) { - return ( - <> -
-
- -
- - - ) + return } diff --git a/components/upvote.js b/components/upvote.js index 8eda6dc0..053a7377 100644 --- a/components/upvote.js +++ b/components/upvote.js @@ -17,14 +17,14 @@ export default function UpVote ({ itemId, meSats, className }) { cache.modify({ id: `Item:${itemId}`, fields: { + meSats (existingMeSats = 0) { + return existingMeSats + vote + }, sats (existingSats = 0) { - return existingSats || vote + return meSats === 0 ? existingSats + vote : existingSats }, boost (existingBoost = 0) { return meSats >= 1 ? existingBoost + vote : existingBoost - }, - meSats (existingMeSats = 0) { - return existingMeSats + vote } } }) diff --git a/package.json b/package.json index 414d1b97..d049df97 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "apollo-server-micro": "^2.21.2", "async-retry": "^1.3.1", "babel-plugin-inline-react-svg": "^2.0.1", + "bech32": "^2.0.0", "bootstrap": "^4.6.0", "clipboard-copy": "^4.0.1", "formik": "^2.2.6", @@ -28,6 +29,7 @@ "react-bootstrap": "^1.5.2", "react-dom": "17.0.1", "sass": "^1.32.8", + "secp256k1": "^4.0.2", "swr": "^0.5.4", "yup": "^0.32.9" }, diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 0c12f4c4..7c464623 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -6,7 +6,65 @@ import prisma from '../../../api/models' export default (req, res) => NextAuth(req, res, options) const options = { + callbacks: { + /** + * @param {object} token Decrypted JSON Web Token + * @param {object} user User object (only available on sign in) + * @param {object} account Provider account (only available on sign in) + * @param {object} profile Provider profile (only available on sign in) + * @param {boolean} isNewUser True if new user (only available on sign in) + * @return {object} JSON Web Token that will be saved + */ + async jwt (token, user, account, profile, isNewUser) { + // Add additional session params + if (user?.id) { + token.id = user.id + } + // XXX We need to update the user name incase they update it ... kind of hacky + // better if we use user id everywhere an ignore the username ... + if (token?.id) { + const { name } = await prisma.user.findUnique({ where: { id: token.id } }) + token.name = name + } + return token + }, + async session (session, token) { + // we need to add additional session params here + session.user.id = token.id + session.user.name = token.name + return session + } + }, providers: [ + Providers.Credentials({ + // The name to display on the sign in form (e.g. 'Sign in with...') + name: 'Lightning', + // The credentials is used to generate a suitable form on the sign in page. + // You can specify whatever fields you are expecting to be submitted. + // e.g. domain, username, password, 2FA token, etc. + credentials: { + pubkey: { label: 'publickey', type: 'text' }, + k1: { label: 'k1', type: 'text' } + }, + async authorize (credentials, req) { + const { k1, pubkey } = credentials + try { + const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } }) + if (lnauth.pubkey === pubkey) { + let user = await prisma.user.findUnique({ where: { pubkey } }) + if (!user) { + user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), pubkey } }) + } + await prisma.lnAuth.delete({ where: { k1 } }) + return user + } + } catch (error) { + console.log(error) + } + + return null + } + }), Providers.GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, @@ -37,6 +95,10 @@ const options = { ], adapter: Adapters.Prisma.Adapter({ prisma }), secret: process.env.SECRET, + session: { jwt: true }, + jwt: { + signingKey: process.env.JWT_SIGNING_PRIVATE_KEY + }, pages: { signIn: '/login' } diff --git a/pages/invoices/[id].js b/pages/invoices/[id].js index c07e60c6..80daee96 100644 --- a/pages/invoices/[id].js +++ b/pages/invoices/[id].js @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client' import gql from 'graphql-tag' -import { Invoice, InvoiceSkeleton } from '../../components/invoice' +import { Invoice } from '../../components/invoice' +import { LnQRSkeleton } from '../../components/lnqr' import LayoutCenter from '../../components/layout-center' export async function getServerSideProps ({ params: { id } }) { @@ -34,7 +35,7 @@ function LoadInvoice ({ query }) { const { loading, error, data } = useQuery(query, { pollInterval: 1000 }) if (error) return
error
if (!data || loading) { - return + return } return diff --git a/pages/login.js b/pages/login.js index 6c81fb02..0207b341 100644 --- a/pages/login.js +++ b/pages/login.js @@ -3,11 +3,15 @@ import Button from 'react-bootstrap/Button' import styles from '../styles/login.module.css' import GithubIcon from '../svgs/github-fill.svg' import TwitterIcon from '../svgs/twitter-fill.svg' +import LightningIcon from '../svgs/lightning.svg' import { Input, SubmitButton, SyncForm } from '../components/form' import * as Yup from 'yup' -import { useState } from 'react' +import { useEffect, useState } from 'react' import Alert from 'react-bootstrap/Alert' import LayoutCenter from '../components/layout-center' +import router, { useRouter } from 'next/router' +import LnQR, { LnQRSkeleton } from '../components/lnqr' +import { gql, useMutation, useQuery } from '@apollo/client' export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) { const session = await getSession({ req }) @@ -33,7 +37,7 @@ export const EmailSchema = Yup.object({ email: Yup.string().email('email is no good').required('required').trim() }) -export default function login ({ providers, csrfToken, error }) { +export default function Login ({ providers, csrfToken, error }) { const errors = { Signin: 'Try signing with a different account.', OAuthSignin: 'Try signing with a different account.', @@ -43,58 +47,123 @@ export default function login ({ providers, csrfToken, error }) { Callback: 'Try signing with a different account.', OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.', EmailSignin: 'Check your email address.', - CredentialsSignin: 'Sign in failed. Check the details you provided are correct.', + CredentialsSignin: 'Lightning auth failed.', default: 'Unable to sign in.' } const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default)) + const router = useRouter() return (
{errorMessage && setErrorMessage(undefined)} dismissible>{errorMessage}} - {Object.values(providers).map(provider => { - if (provider.name === 'Email') { - return null - } - const [variant, Icon] = + {router.query.type === 'lightning' + ? + : ( + <> + + {Object.values(providers).map(provider => { + if (provider.name === 'Email' || provider.name === 'Lightning') { + return null + } + const [variant, Icon] = provider.name === 'Twitter' ? ['twitter', TwitterIcon] : ['dark', GithubIcon] - return ( - - ) - })} -
or
- - - - Login with Email - + return ( + + ) + })} +
or
+ + + + Login with Email + + )}
) } + +function LnQRAuth ({ k1, encodedUrl }) { + const query = gql` + { + lnAuth(k1: "${k1}") { + pubkey + k1 + } + }` + const { error, data } = useQuery(query, { pollInterval: 1000 }) + if (error) return
error
+ + if (data && data.lnAuth.pubkey) { + signIn('credentials', { ...data.lnAuth, callbackUrl: router.query.callbackUrl }) + } + + // output pubkey and k1 + return ( + <> + + Does my wallet support lnurl-auth? + + + + ) +} + +export function LightningAuth () { + // 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/pages/wallet.js b/pages/wallet.js index e931d821..da0333f6 100644 --- a/pages/wallet.js +++ b/pages/wallet.js @@ -4,7 +4,7 @@ import Link from 'next/link' import Button from 'react-bootstrap/Button' import * as Yup from 'yup' import { gql, useMutation, useQuery } from '@apollo/client' -import { InvoiceSkeleton } from '../components/invoice' +import { LnQRSkeleton } from '../components/lnqr' import LayoutCenter from '../components/layout-center' import InputGroup from 'react-bootstrap/InputGroup' import { WithdrawlSkeleton } from './withdrawls/[id]' @@ -56,7 +56,7 @@ export function FundForm () { }`) if (called && !error) { - return + return } return ( diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e06fc4b2..f93e5211 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,10 +27,19 @@ model User { freeComments Int @default(5) freePosts Int @default(2) checkedNotesAt DateTime? + pubkey String? @unique @@map(name: "users") } +model LnAuth { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + k1 String @unique + pubkey String? +} + model Message { id Int @id @default(autoincrement()) text String diff --git a/styles/login.module.css b/styles/login.module.css index 71c578a3..fcb43f78 100644 --- a/styles/login.module.css +++ b/styles/login.module.css @@ -8,4 +8,8 @@ .login { max-width: 300px; width: 100%; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; } \ No newline at end of file