upgrade to next-auth 4 (bonus: improve error pages)
This commit is contained in:
		
							parent
							
								
									de089aa429
								
							
						
					
					
						commit
						5232b59625
					
				| @ -847,7 +847,7 @@ export default { | |||||||
|       return await models.user.findUnique({ where: { id: item.fwdUserId } }) |       return await models.user.findUnique({ where: { id: item.fwdUserId } }) | ||||||
|     }, |     }, | ||||||
|     comments: async (item, { sort }, { me, models }) => { |     comments: async (item, { sort }, { me, models }) => { | ||||||
|       if (item.comments) return item.comments |       if (typeof item.comments !== 'undefined') return item.comments | ||||||
|       if (item.ncomments === 0) return [] |       if (item.ncomments === 0) return [] | ||||||
| 
 | 
 | ||||||
|       return comments(me, models, item.id, sort || defaultCommentSort(item.pinId, item.bioId, item.createdAt)) |       return comments(me, models, item.id, sort || defaultCommentSort(item.pinId, item.bioId, item.createdAt)) | ||||||
|  | |||||||
| @ -73,7 +73,7 @@ async function authMethods (user, args, { models, me }) { | |||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const oauth = accounts.map(a => a.providerId) |   const oauth = accounts.map(a => a.provider) | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     lightning: !!user.pubkey, |     lightning: !!user.pubkey, | ||||||
| @ -87,7 +87,7 @@ async function authMethods (user, args, { models, me }) { | |||||||
| export default { | export default { | ||||||
|   Query: { |   Query: { | ||||||
|     me: async (parent, { skipUpdate }, { models, me }) => { |     me: async (parent, { skipUpdate }, { models, me }) => { | ||||||
|       if (!me) { |       if (!me?.id) { | ||||||
|         return null |         return null | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -518,7 +518,7 @@ export default { | |||||||
|       let user |       let user | ||||||
|       if (authType === 'twitter' || authType === 'github') { |       if (authType === 'twitter' || authType === 'github') { | ||||||
|         user = await models.user.findUnique({ where: { id: me.id } }) |         user = await models.user.findUnique({ where: { id: me.id } }) | ||||||
|         const account = await models.account.findFirst({ where: { userId: me.id, providerId: authType } }) |         const account = await models.account.findFirst({ where: { userId: me.id, provider: authType } }) | ||||||
|         if (!account) { |         if (!account) { | ||||||
|           throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } }) |           throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } }) | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { ApolloClient, InMemoryCache } from '@apollo/client' | import { ApolloClient, InMemoryCache } from '@apollo/client' | ||||||
| import { SchemaLink } from '@apollo/client/link/schema' | import { SchemaLink } from '@apollo/client/link/schema' | ||||||
| import { makeExecutableSchema } from '@graphql-tools/schema' | import { makeExecutableSchema } from '@graphql-tools/schema' | ||||||
| import { getSession } from 'next-auth/client' |  | ||||||
| import resolvers from './resolvers' | import resolvers from './resolvers' | ||||||
| import typeDefs from './typeDefs' | import typeDefs from './typeDefs' | ||||||
| import models from './models' | import models from './models' | ||||||
| @ -11,9 +10,11 @@ import lnd from './lnd' | |||||||
| import search from './search' | import search from './search' | ||||||
| import { ME } from '../fragments/users' | import { ME } from '../fragments/users' | ||||||
| import { PRICE } from '../fragments/price' | import { PRICE } from '../fragments/price' | ||||||
|  | import { getServerSession } from 'next-auth/next' | ||||||
|  | import { getAuthOptions } from '../pages/api/auth/[...nextauth]' | ||||||
| 
 | 
 | ||||||
| export default async function getSSRApolloClient (req, me = null) { | export default async function getSSRApolloClient ({ req, res, me = null }) { | ||||||
|   const session = req && await getSession({ req }) |   const session = req && await getServerSession(req, res, getAuthOptions(req)) | ||||||
|   const client = new ApolloClient({ |   const client = new ApolloClient({ | ||||||
|     ssrMode: true, |     ssrMode: true, | ||||||
|     link: new SchemaLink({ |     link: new SchemaLink({ | ||||||
| @ -54,7 +55,7 @@ export default async function getSSRApolloClient (req, me = null) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getGetServerSideProps (queryOrFunc, variablesOrFunc = null, notFoundFunc, requireVar) { | export function getGetServerSideProps (queryOrFunc, variablesOrFunc = null, notFoundFunc, requireVar) { | ||||||
|   return async function ({ req, query: params }) { |   return async function ({ req, res, query: params }) { | ||||||
|     const { nodata, ...realParams } = params |     const { nodata, ...realParams } = params | ||||||
|     // we want to use client-side cache
 |     // we want to use client-side cache
 | ||||||
|     if (nodata) return { props: { } } |     if (nodata) return { props: { } } | ||||||
| @ -63,7 +64,7 @@ export function getGetServerSideProps (queryOrFunc, variablesOrFunc = null, notF | |||||||
|     const vars = { ...realParams, ...variables } |     const vars = { ...realParams, ...variables } | ||||||
|     const query = typeof queryOrFunc === 'function' ? queryOrFunc(vars) : queryOrFunc |     const query = typeof queryOrFunc === 'function' ? queryOrFunc(vars) : queryOrFunc | ||||||
| 
 | 
 | ||||||
|     const client = await getSSRApolloClient(req) |     const client = await getSSRApolloClient({ req, res }) | ||||||
| 
 | 
 | ||||||
|     const { data: { me } } = await client.query({ |     const { data: { me } } = await client.query({ | ||||||
|       query: ME, |       query: ME, | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { Component } from 'react' | import { Component } from 'react' | ||||||
| import { StaticLayout } from './layout' | import { StaticLayout } from './layout' | ||||||
| import styles from '../styles/404.module.css' | import styles from '../styles/error.module.css' | ||||||
| import Image from 'react-bootstrap/Image' | import Image from 'react-bootstrap/Image' | ||||||
| 
 | 
 | ||||||
| class ErrorBoundary extends Component { | class ErrorBoundary extends Component { | ||||||
| @ -27,8 +27,8 @@ class ErrorBoundary extends Component { | |||||||
|       // You can render any custom fallback UI
 |       // You can render any custom fallback UI
 | ||||||
|       return ( |       return ( | ||||||
|         <StaticLayout> |         <StaticLayout> | ||||||
|           <Image width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/floating.gif`} fluid /> |           <Image width='500' height='375' className='rounded-1 shadow-sm' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/floating.gif`} fluid /> | ||||||
|           <h1 className={styles.fourZeroFour} style={{ fontSize: '48px' }}>something went wrong</h1> |           <h1 className={styles.status} style={{ fontSize: '48px' }}>something went wrong</h1> | ||||||
|         </StaticLayout> |         </StaticLayout> | ||||||
|       ) |       ) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import NavDropdown from 'react-bootstrap/NavDropdown' | |||||||
| import Price from './price' | import Price from './price' | ||||||
| import { useMe } from './me' | import { useMe } from './me' | ||||||
| import Head from 'next/head' | import Head from 'next/head' | ||||||
| import { signOut } from 'next-auth/client' | import { signOut } from 'next-auth/react' | ||||||
| import { useCallback, useEffect, useState } from 'react' | import { useCallback, useEffect, useState } from 'react' | ||||||
| import { randInRange } from '../lib/rand' | import { randInRange } from '../lib/rand' | ||||||
| import { abbrNum } from '../lib/format' | import { abbrNum } from '../lib/format' | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { gql, useMutation, useQuery } from '@apollo/client' | import { gql, useMutation, useQuery } from '@apollo/client' | ||||||
| import { signIn } from 'next-auth/client' | import { signIn } from 'next-auth/react' | ||||||
| import { useEffect } from 'react' | import { useEffect } from 'react' | ||||||
| import Col from 'react-bootstrap/Col' | import Col from 'react-bootstrap/Col' | ||||||
| import Container from 'react-bootstrap/Container' | import Container from 'react-bootstrap/Container' | ||||||
| @ -20,9 +20,11 @@ function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) { | |||||||
|   }` |   }` | ||||||
|   const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' }) |   const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' }) | ||||||
| 
 | 
 | ||||||
|   if (data && data.lnAuth.pubkey) { |   useEffect(() => { | ||||||
|     signIn(encodedUrl ? 'lightning' : 'slashtags', { ...data.lnAuth, callbackUrl }) |     if (data?.lnAuth?.pubkey) { | ||||||
|   } |       signIn(encodedUrl ? 'lightning' : 'slashtags', { ...data.lnAuth, callbackUrl }) | ||||||
|  |     } | ||||||
|  |   }, [data?.lnAuth]) | ||||||
| 
 | 
 | ||||||
|   // output pubkey and k1
 |   // output pubkey and k1
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { signIn } from 'next-auth/client' | import { signIn } from 'next-auth/react' | ||||||
| import styles from './login.module.css' | import styles from './login.module.css' | ||||||
| import { Form, Input, SubmitButton } from '../components/form' | import { Form, Input, SubmitButton } from '../components/form' | ||||||
| import { useState } from 'react' | import { useState } from 'react' | ||||||
| @ -33,16 +33,15 @@ export function EmailLoginForm ({ text, callbackUrl }) { | |||||||
| 
 | 
 | ||||||
| export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) { | export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) { | ||||||
|   const errors = { |   const errors = { | ||||||
|     Signin: 'Try signing with a different account.', |     OAuthSignin: 'Error constructing OAuth URL. Try again or choose a different method.', | ||||||
|     OAuthSignin: 'Try signing with a different account.', |     OAuthCallback: 'Error handling OAuth response. Try again or choose a different method.', | ||||||
|     OAuthCallback: 'Try signing with a different account.', |     OAuthCreateAccount: 'Could not create OAuth account. Try again or choose a different method.', | ||||||
|     OAuthCreateAccount: 'Try signing with a different account.', |     EmailCreateAccount: 'Could not create Email account. Try again or choose a different method.', | ||||||
|     EmailCreateAccount: 'Try signing with a different account.', |     Callback: 'Error in callback handler. Try again or choose a different method.', | ||||||
|     Callback: 'Try signing with a different account.', |     OAuthAccountNotLinked: 'This auth method is linked to another account. To link to this account first unlink the other account.', | ||||||
|     OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.', |     EmailSignin: 'Failed to send email. Make sure you entered your email address correctly.', | ||||||
|     EmailSignin: 'Check your email address.', |     CredentialsSignin: 'Auth failed. Try again or choose a different method.', | ||||||
|     CredentialsSignin: 'Auth failed', |     default: 'Auth failed. Try again or choose a different method.' | ||||||
|     default: 'Unable to sign in.' |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default)) |   const [errorMessage, setErrorMessage] = useState(error && (errors[error] ?? errors.default)) | ||||||
| @ -70,7 +69,7 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo | |||||||
|         switch (provider.name) { |         switch (provider.name) { | ||||||
|           case 'Email': |           case 'Email': | ||||||
|             return ( |             return ( | ||||||
|               <div className='w-100' key={provider.name}> |               <div className='w-100' key={provider.id}> | ||||||
|                 <div className='mt-2 text-center text-muted fw-bold'>or</div> |                 <div className='mt-2 text-center text-muted fw-bold'>or</div> | ||||||
|                 <EmailLoginForm text={text} callbackUrl={callbackUrl} /> |                 <EmailLoginForm text={text} callbackUrl={callbackUrl} /> | ||||||
|               </div> |               </div> | ||||||
| @ -80,8 +79,8 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo | |||||||
|             return ( |             return ( | ||||||
|               <LoginButton |               <LoginButton | ||||||
|                 className={`mt-2 ${styles.providerButton}`} |                 className={`mt-2 ${styles.providerButton}`} | ||||||
|                 key={provider.name} |                 key={provider.id} | ||||||
|                 type={provider.name.toLowerCase()} |                 type={provider.id.toLowerCase()} | ||||||
|                 onClick={() => router.push({ |                 onClick={() => router.push({ | ||||||
|                   pathname: router.pathname, |                   pathname: router.pathname, | ||||||
|                   query: { callbackUrl: router.query.callbackUrl, type: provider.name.toLowerCase() } |                   query: { callbackUrl: router.query.callbackUrl, type: provider.name.toLowerCase() } | ||||||
| @ -93,8 +92,8 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo | |||||||
|             return ( |             return ( | ||||||
|               <LoginButton |               <LoginButton | ||||||
|                 className={`mt-2 ${styles.providerButton}`} |                 className={`mt-2 ${styles.providerButton}`} | ||||||
|                 key={provider.name} |                 key={provider.id} | ||||||
|                 type={provider.name.toLowerCase()} |                 type={provider.id.toLowerCase()} | ||||||
|                 onClick={() => signIn(provider.id, { callbackUrl })} |                 onClick={() => signIn(provider.id, { callbackUrl })} | ||||||
|                 text={`${text || 'Login'} with`} |                 text={`${text || 'Login'} with`} | ||||||
|               /> |               /> | ||||||
|  | |||||||
| @ -126,7 +126,7 @@ function EarnNotification ({ n }) { | |||||||
|       <HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} /> |       <HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} /> | ||||||
|       <div className='ms-2'> |       <div className='ms-2'> | ||||||
|         <div className='fw-bold text-boost'> |         <div className='fw-bold text-boost'> | ||||||
|           you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1'>{timeSince(new Date(n.sortTime))}</small> |           you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small> | ||||||
|         </div> |         </div> | ||||||
|         {n.sources && |         {n.sources && | ||||||
|           <div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}> |           <div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}> | ||||||
| @ -166,7 +166,7 @@ function InvoicePaid ({ n }) { | |||||||
|     <NotificationLayout href={`/invoices/${n.invoice.id}`}> |     <NotificationLayout href={`/invoices/${n.invoice.id}`}> | ||||||
|       <div className='fw-bold text-info ms-2 py-1'> |       <div className='fw-bold text-info ms-2 py-1'> | ||||||
|         <Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account |         <Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account | ||||||
|         <small className='text-muted ms-1'>{timeSince(new Date(n.sortTime))}</small> |         <small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small> | ||||||
|       </div> |       </div> | ||||||
|     </NotificationLayout> |     </NotificationLayout> | ||||||
|   ) |   ) | ||||||
| @ -177,7 +177,7 @@ function Referral ({ n }) { | |||||||
|     <NotificationLayout> |     <NotificationLayout> | ||||||
|       <small className='fw-bold text-secondary ms-2'> |       <small className='fw-bold text-secondary ms-2'> | ||||||
|         someone joined via one of your <Link href='/referrals/month' className='text-reset'>referral links</Link> |         someone joined via one of your <Link href='/referrals/month' className='text-reset'>referral links</Link> | ||||||
|         <small className='text-muted ms-1'>{timeSince(new Date(n.sortTime))}</small> |         <small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small> | ||||||
|       </small> |       </small> | ||||||
|     </NotificationLayout> |     </NotificationLayout> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ import { timeLeft } from '../lib/time' | |||||||
| import { useMe } from './me' | import { useMe } from './me' | ||||||
| import styles from './poll.module.css' | import styles from './poll.module.css' | ||||||
| import Check from '../svgs/checkbox-circle-fill.svg' | import Check from '../svgs/checkbox-circle-fill.svg' | ||||||
| import { signIn } from 'next-auth/client' | import { signIn } from 'next-auth/react' | ||||||
| import ActionTooltip from './action-tooltip' | import ActionTooltip from './action-tooltip' | ||||||
| import { useShowModal } from './modal' | import { useShowModal } from './modal' | ||||||
| import FundError from './fund-error' | import FundError from './fund-error' | ||||||
|  | |||||||
| @ -35,7 +35,7 @@ const UpvotePopover = ({ target, show, handleClose }) => { | |||||||
|     > |     > | ||||||
|       <Popover id='popover-basic'> |       <Popover id='popover-basic'> | ||||||
|         <Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Zapping |         <Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Zapping | ||||||
|           <button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='sr-only'>Close alert</span></button> |           <button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='visually-hidden-focusable'>Close alert</span></button> | ||||||
|         </Popover.Body> |         </Popover.Body> | ||||||
|         <Popover.Body> |         <Popover.Body> | ||||||
|           <div className='mb-2'>Press the bolt again to zap {me?.tipDefault || 1} more sat{me?.tipDefault > 1 ? 's' : ''}.</div> |           <div className='mb-2'>Press the bolt again to zap {me?.tipDefault || 1} more sat{me?.tipDefault > 1 ? 's' : ''}.</div> | ||||||
| @ -54,7 +54,7 @@ const TipPopover = ({ target, show, handleClose }) => ( | |||||||
|   > |   > | ||||||
|     <Popover id='popover-basic'> |     <Popover id='popover-basic'> | ||||||
|       <Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Press and hold |       <Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Press and hold | ||||||
|         <button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='sr-only'>Close alert</span></button> |         <button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='visually-hidden-focusable'>Close alert</span></button> | ||||||
|       </Popover.Body> |       </Popover.Body> | ||||||
|       <Popover.Body> |       <Popover.Body> | ||||||
|         <div className='mb-2'>Press and hold bolt to zap a custom amount.</div> |         <div className='mb-2'>Press and hold bolt to zap a custom amount.</div> | ||||||
|  | |||||||
| @ -12,8 +12,12 @@ function isFirstPage (cursor, existingThings) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const SSR = typeof window === 'undefined' | ||||||
|  | const defaultFetchPolicy = SSR ? 'cache-only' : 'cache-first' | ||||||
|  | const defaultNextFetchPolicy = SSR ? 'cache-only' : 'cache-first' | ||||||
|  | 
 | ||||||
| export default function getApolloClient () { | export default function getApolloClient () { | ||||||
|   if (typeof window === 'undefined') { |   if (SSR) { | ||||||
|     return getClient(`${process.env.SELF_URL}/api/graphql`) |     return getClient(`${process.env.SELF_URL}/api/graphql`) | ||||||
|   } else { |   } else { | ||||||
|     global.apolloClient ||= getClient('/api/graphql') |     global.apolloClient ||= getClient('/api/graphql') | ||||||
| @ -24,7 +28,7 @@ export default function getApolloClient () { | |||||||
| function getClient (uri) { | function getClient (uri) { | ||||||
|   return new ApolloClient({ |   return new ApolloClient({ | ||||||
|     link: new HttpLink({ uri }), |     link: new HttpLink({ uri }), | ||||||
|     ssrMode: typeof window === 'undefined', |     ssrMode: SSR, | ||||||
|     cache: new InMemoryCache({ |     cache: new InMemoryCache({ | ||||||
|       freezeResults: true, |       freezeResults: true, | ||||||
|       typePolicies: { |       typePolicies: { | ||||||
| @ -143,13 +147,13 @@ function getClient (uri) { | |||||||
|     assumeImmutableResults: true, |     assumeImmutableResults: true, | ||||||
|     defaultOptions: { |     defaultOptions: { | ||||||
|       watchQuery: { |       watchQuery: { | ||||||
|         fetchPolicy: 'cache-first', |         fetchPolicy: defaultFetchPolicy, | ||||||
|         nextFetchPolicy: 'cache-first', |         nextFetchPolicy: defaultNextFetchPolicy, | ||||||
|         canonizeResults: true |         canonizeResults: true | ||||||
|       }, |       }, | ||||||
|       query: { |       query: { | ||||||
|         fetchPolicy: 'cache-first', |         fetchPolicy: defaultFetchPolicy, | ||||||
|         nextFetchPolicy: 'cache-first', |         nextFetchPolicy: defaultNextFetchPolicy, | ||||||
|         canonizeResults: true |         canonizeResults: true | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -56,7 +56,7 @@ function generateRssFeed (items, sub = null) { | |||||||
| export default function getGetRssServerSideProps (query, variables = null) { | export default function getGetRssServerSideProps (query, variables = null) { | ||||||
|   return async function ({ req, res, query: params }) { |   return async function ({ req, res, query: params }) { | ||||||
|     const emptyProps = { props: {} } // to avoid server side warnings
 |     const emptyProps = { props: {} } // to avoid server side warnings
 | ||||||
|     const client = await getSSRApolloClient(req) |     const client = await getSSRApolloClient({ req, res }) | ||||||
|     const { error, data: { items: { items } } } = await client.query({ |     const { error, data: { items: { items } } } = await client.query({ | ||||||
|       query, variables: { ...params, ...variables } |       query, variables: { ...params, ...variables } | ||||||
|     }) |     }) | ||||||
|  | |||||||
							
								
								
									
										1274
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1274
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -12,6 +12,7 @@ | |||||||
|     "@apollo/client": "^3.7.17", |     "@apollo/client": "^3.7.17", | ||||||
|     "@apollo/server": "^4.8.1", |     "@apollo/server": "^4.8.1", | ||||||
|     "@as-integrations/next": "^2.0.1", |     "@as-integrations/next": "^2.0.1", | ||||||
|  |     "@auth/prisma-adapter": "^1.0.1", | ||||||
|     "@graphql-tools/schema": "^10.0.0", |     "@graphql-tools/schema": "^10.0.0", | ||||||
|     "@noble/curves": "^1.1.0", |     "@noble/curves": "^1.1.0", | ||||||
|     "@opensearch-project/opensearch": "^2.3.1", |     "@opensearch-project/opensearch": "^2.3.1", | ||||||
| @ -44,11 +45,12 @@ | |||||||
|     "mdast-util-to-string": "^4.0.0", |     "mdast-util-to-string": "^4.0.0", | ||||||
|     "micromark-extension-gfm": "^3.0.0", |     "micromark-extension-gfm": "^3.0.0", | ||||||
|     "next": "^13.4.12", |     "next": "^13.4.12", | ||||||
|     "next-auth": "^3.29.10", |     "next-auth": "^4.22.3", | ||||||
|     "next-plausible": "^3.10.1", |     "next-plausible": "^3.10.1", | ||||||
|     "next-seo": "^6.1.0", |     "next-seo": "^6.1.0", | ||||||
|     "nextjs-progressbar": "0.0.16", |     "nextjs-progressbar": "0.0.16", | ||||||
|     "node-s3-url-encode": "^0.0.4", |     "node-s3-url-encode": "^0.0.4", | ||||||
|  |     "nodemailer": "^6.9.4", | ||||||
|     "nostr": "^0.2.8", |     "nostr": "^0.2.8", | ||||||
|     "opentimestamps": "^0.4.9", |     "opentimestamps": "^0.4.9", | ||||||
|     "page-metadata-parser": "^1.1.4", |     "page-metadata-parser": "^1.1.4", | ||||||
| @ -79,6 +81,7 @@ | |||||||
|     "web-push": "^3.6.2", |     "web-push": "^3.6.2", | ||||||
|     "webln": "^0.3.2", |     "webln": "^0.3.2", | ||||||
|     "webpack": "^5.88.2", |     "webpack": "^5.88.2", | ||||||
|  |     "workbox-navigation-preload": "^7.0.0", | ||||||
|     "workbox-precaching": "^7.0.0", |     "workbox-precaching": "^7.0.0", | ||||||
|     "workbox-recipes": "^7.0.0", |     "workbox-recipes": "^7.0.0", | ||||||
|     "workbox-routing": "^7.0.0", |     "workbox-routing": "^7.0.0", | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								pages/404.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								pages/404.js
									
									
									
									
									
								
							| @ -1,12 +1,5 @@ | |||||||
| import Image from 'react-bootstrap/Image' | import Error from './_error' | ||||||
| import { StaticLayout } from '../components/layout' |  | ||||||
| import styles from '../styles/404.module.css' |  | ||||||
| 
 | 
 | ||||||
| export default function fourZeroFour () { | export default function Custom404 () { | ||||||
|   return ( |   return <Error statusCode={404} /> | ||||||
|     <StaticLayout> |  | ||||||
|       <Image width='500' height='376' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/maze.gif`} fluid /> |  | ||||||
|       <h1 className={styles.fourZeroFour}><span>404</span><span className={styles.notFound}>page not found</span></h1> |  | ||||||
|     </StaticLayout> |  | ||||||
|   ) |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								pages/500.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								pages/500.js
									
									
									
									
									
								
							| @ -1,12 +1,5 @@ | |||||||
| import Image from 'react-bootstrap/Image' | import Error from './_error' | ||||||
| import { StaticLayout } from '../components/layout' |  | ||||||
| import styles from '../styles/404.module.css' |  | ||||||
| 
 | 
 | ||||||
| export default function fiveHundo () { | export default function Custom500 () { | ||||||
|   return ( |   return <Error statusCode={500} /> | ||||||
|     <StaticLayout> |  | ||||||
|       <Image width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/falling.gif`} fluid /> |  | ||||||
|       <h1 className={styles.fourZeroFour}><span>500</span><span className={styles.notFound}>server error</span></h1> |  | ||||||
|     </StaticLayout> |  | ||||||
|   ) |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import '../styles/globals.scss' | import '../styles/globals.scss' | ||||||
| import { ApolloProvider, gql } from '@apollo/client' | import { ApolloProvider, gql } from '@apollo/client' | ||||||
| import { Provider } from 'next-auth/client' |  | ||||||
| import { MeProvider } from '../components/me' | import { MeProvider } from '../components/me' | ||||||
| import PlausibleProvider from 'next-plausible' | import PlausibleProvider from 'next-plausible' | ||||||
| import getApolloClient from '../lib/apollo' | import getApolloClient from '../lib/apollo' | ||||||
| @ -28,7 +27,7 @@ function writeQuery (client, apollo, data) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function MyApp ({ Component, pageProps: { session, ...props } }) { | function MyApp ({ Component, pageProps: { ...props } }) { | ||||||
|   const client = getApolloClient() |   const client = getApolloClient() | ||||||
|   const router = useRouter() |   const router = useRouter() | ||||||
| 
 | 
 | ||||||
| @ -80,21 +79,19 @@ function MyApp ({ Component, pageProps: { session, ...props } }) { | |||||||
|       </Head> |       </Head> | ||||||
|       <ErrorBoundary> |       <ErrorBoundary> | ||||||
|         <PlausibleProvider domain='stacker.news' trackOutboundLinks> |         <PlausibleProvider domain='stacker.news' trackOutboundLinks> | ||||||
|           <Provider session={session}> |           <ApolloProvider client={client}> | ||||||
|             <ApolloProvider client={client}> |             <MeProvider me={me}> | ||||||
|               <MeProvider me={me}> |               <ServiceWorkerProvider> | ||||||
|                 <ServiceWorkerProvider> |                 <PriceProvider price={price}> | ||||||
|                   <PriceProvider price={price}> |                   <LightningProvider> | ||||||
|                     <LightningProvider> |                     <ShowModalProvider> | ||||||
|                       <ShowModalProvider> |                       <Component ssrData={ssrData} {...otherProps} /> | ||||||
|                         <Component ssrData={ssrData} {...otherProps} /> |                     </ShowModalProvider> | ||||||
|                       </ShowModalProvider> |                   </LightningProvider> | ||||||
|                     </LightningProvider> |                 </PriceProvider> | ||||||
|                   </PriceProvider> |               </ServiceWorkerProvider> | ||||||
|                 </ServiceWorkerProvider> |             </MeProvider> | ||||||
|               </MeProvider> |           </ApolloProvider> | ||||||
|             </ApolloProvider> |  | ||||||
|           </Provider> |  | ||||||
|         </PlausibleProvider> |         </PlausibleProvider> | ||||||
|       </ErrorBoundary> |       </ErrorBoundary> | ||||||
|     </> |     </> | ||||||
|  | |||||||
							
								
								
									
										77
									
								
								pages/_error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								pages/_error.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | |||||||
|  | import Image from 'react-bootstrap/Image' | ||||||
|  | import { StaticLayout } from '../components/layout' | ||||||
|  | import styles from '../styles/error.module.css' | ||||||
|  | 
 | ||||||
|  | const statusDescribe = { | ||||||
|  |   200: 'OK', | ||||||
|  |   201: 'Created', | ||||||
|  |   202: 'Accepted', | ||||||
|  |   203: 'Non-Authoritative Information', | ||||||
|  |   204: 'No Content', | ||||||
|  |   205: 'Reset Content', | ||||||
|  |   206: 'Partial Content', | ||||||
|  |   300: 'Multiple Choices', | ||||||
|  |   301: 'Moved Permanently', | ||||||
|  |   302: 'Found', | ||||||
|  |   303: 'See Other', | ||||||
|  |   304: 'Not Modified', | ||||||
|  |   305: 'Use Proxy', | ||||||
|  |   306: 'Unused', | ||||||
|  |   307: 'Temporary Redirect', | ||||||
|  |   400: 'Bad Request', | ||||||
|  |   401: 'Unauthorized', | ||||||
|  |   402: 'Payment Required', | ||||||
|  |   403: 'Forbidden', | ||||||
|  |   404: 'Not Found', | ||||||
|  |   405: 'Method Not Allowed', | ||||||
|  |   406: 'Not Acceptable', | ||||||
|  |   407: 'Proxy Authentication Required', | ||||||
|  |   408: 'Request Timeout', | ||||||
|  |   409: 'Conflict', | ||||||
|  |   410: 'Gone', | ||||||
|  |   411: 'Length Required', | ||||||
|  |   412: 'Precondition Required', | ||||||
|  |   413: 'Request Entry Too Large', | ||||||
|  |   414: 'Request-URI Too Long', | ||||||
|  |   415: 'Unsupported Media Type', | ||||||
|  |   416: 'Requested Range Not Satisfiable', | ||||||
|  |   417: 'Expectation Failed', | ||||||
|  |   418: 'I\'m a teapot', | ||||||
|  |   429: 'Too Many Requests', | ||||||
|  |   500: 'Internal Server Error', | ||||||
|  |   501: 'Not Implemented', | ||||||
|  |   502: 'Bad Gateway', | ||||||
|  |   503: 'Service Unavailable', | ||||||
|  |   504: 'Gateway Timeout', | ||||||
|  |   505: 'HTTP Version Not Supported' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function ErrorImage ({ statusCode }) { | ||||||
|  |   if (statusCode === 404) { | ||||||
|  |     return <Image className='rounded-1 shadow-sm' width='500' height='376' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/maze.gif`} fluid /> | ||||||
|  |   } | ||||||
|  |   if (statusCode >= 500) { | ||||||
|  |     return <Image className='rounded-1 shadow-sm' width='300' height='225' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/falling.gif`} fluid /> | ||||||
|  |   } | ||||||
|  |   if (statusCode >= 400) { | ||||||
|  |     return <Image className='rounded-1 shadow-sm' width='500' height='381' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/forbidden.gif`} fluid /> | ||||||
|  |   } | ||||||
|  |   if (statusCode >= 300) { | ||||||
|  |     return <Image className='rounded-1 shadow-sm' width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/double.gif`} fluid /> | ||||||
|  |   } | ||||||
|  |   return <Image className='rounded-1 shadow-sm' width='500' height='376' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/falling.gif`} fluid /> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Error ({ statusCode }) { | ||||||
|  |   return ( | ||||||
|  |     <StaticLayout> | ||||||
|  |       <ErrorImage statusCode={statusCode} /> | ||||||
|  |       <h1 className={styles.status}><span>{statusCode}</span><span className={styles.describe}>{statusDescribe[statusCode].toUpperCase()}</span></h1> | ||||||
|  |     </StaticLayout> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Error.getInitialProps = ({ res, err }) => { | ||||||
|  |   const statusCode = res ? res.statusCode : err ? err.statusCode : 404 | ||||||
|  |   return { statusCode } | ||||||
|  | } | ||||||
| @ -1,40 +1,16 @@ | |||||||
| import NextAuth from 'next-auth' | import NextAuth from 'next-auth' | ||||||
| import Providers from 'next-auth/providers' | import CredentialsProvider from 'next-auth/providers/credentials' | ||||||
| import { PrismaLegacyAdapter } from '../../../lib/prisma-adapter' | import GitHubProvider from 'next-auth/providers/github' | ||||||
|  | import TwitterProvider from 'next-auth/providers/twitter' | ||||||
|  | import EmailProvider from 'next-auth/providers/email' | ||||||
| import prisma from '../../../api/models' | import prisma from '../../../api/models' | ||||||
| import nodemailer from 'nodemailer' | import nodemailer from 'nodemailer' | ||||||
| import { getSession } from 'next-auth/client' | import { PrismaAdapter } from '@auth/prisma-adapter' | ||||||
|  | import { getToken } from 'next-auth/jwt' | ||||||
|  | import { NodeNextRequest } from 'next/dist/server/base-http/node' | ||||||
| 
 | 
 | ||||||
| // node v16 and next don't give us parsed headers like v14, so we
 | function getCallbacks (req) { | ||||||
| // do our best to parse them in a similar fashion to like in v14
 |   return { | ||||||
| // getSession requires req.headers.cookie otherwise it will throw
 |  | ||||||
| function parsedHeaders (req) { |  | ||||||
|   return req.rawHeaders.reduce( |  | ||||||
|     (obj, value, index, arr) => { |  | ||||||
|       if (index % 2 === 0) { |  | ||||||
|         const key = value.toLowerCase() |  | ||||||
|         if (typeof obj[key] === 'string') { |  | ||||||
|           if (key === 'cookie') { |  | ||||||
|             obj[key] = obj[key] + '; ' + arr[index + 1] |  | ||||||
|           } else if (key === 'set-cookie') { |  | ||||||
|             obj[key] = obj[key].push(arr[index + 1]) |  | ||||||
|           } else { |  | ||||||
|             obj[key] = obj[key] + ', ' + arr[index + 1] |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           if (key === 'set-cookie') { |  | ||||||
|             obj[key] = [arr[index + 1]] |  | ||||||
|           } else { |  | ||||||
|             obj[key] = arr[index + 1] |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return obj |  | ||||||
|     }, {}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default (req, res) => NextAuth(req, res, { |  | ||||||
|   callbacks: { |  | ||||||
|     /** |     /** | ||||||
|      * @param  {object}  token     Decrypted JSON Web Token |      * @param  {object}  token     Decrypted JSON Web Token | ||||||
|      * @param  {object}  user      User object      (only available on sign in) |      * @param  {object}  user      User object      (only available on sign in) | ||||||
| @ -43,13 +19,18 @@ export default (req, res) => NextAuth(req, res, { | |||||||
|      * @param  {boolean} isNewUser True if new user (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 |      * @return {object}            JSON Web Token that will be saved | ||||||
|      */ |      */ | ||||||
|     async jwt (token, user, account, profile, isNewUser) { |     async jwt ({ token, user, account, profile, isNewUser }) { | ||||||
|       // Add additional session params
 |       if (user) { | ||||||
|       if (user?.id) { |         // token won't have an id on it for new logins, we add it
 | ||||||
|  |         // note: token is what's kept in the jwt
 | ||||||
|         token.id = Number(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) } |       if (token?.id) { | ||||||
|  |         // HACK token.sub is used by nextjs v4 internally and is used like a userId
 | ||||||
|  |         // setting it here allows us to link multiple auth method to an account
 | ||||||
|  |         // ... in v3 this linking field was token.user.id
 | ||||||
|  |         token.sub = Number(token.id) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (isNewUser) { |       if (isNewUser) { | ||||||
| @ -82,149 +63,123 @@ export default (req, res) => NextAuth(req, res, { | |||||||
| 
 | 
 | ||||||
|       return token |       return token | ||||||
|     }, |     }, | ||||||
|     async session (session, token) { |     async session ({ session, token }) { | ||||||
|       // we need to add additional session params here
 |       // note: this function takes the current token (result of running jwt above)
 | ||||||
|       session.user.id = Number(token.id) |       // and returns a new object session that's returned whenever get|use[Server]Session is called
 | ||||||
|  |       session.user.id = token.id | ||||||
|  | 
 | ||||||
|       return session |       return session | ||||||
|     } |     } | ||||||
|   }, |   } | ||||||
|   providers: [ | } | ||||||
|     Providers.Credentials({ |  | ||||||
|       id: 'lightning', |  | ||||||
|       // 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 } }) |  | ||||||
|           await prisma.lnAuth.delete({ where: { k1 } }) |  | ||||||
|           if (lnauth.pubkey === pubkey) { |  | ||||||
|             let user = await prisma.user.findUnique({ where: { pubkey } }) |  | ||||||
|             req.headers = parsedHeaders(req) |  | ||||||
|             const session = await getSession({ req }) |  | ||||||
|             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 } }) |  | ||||||
|               } |  | ||||||
|             } else if (session && session.user?.id !== user.id) { |  | ||||||
|               throw new Error('account not linked') |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return user | async function pubkeyAuth (credentials, req, pubkeyColumnName) { | ||||||
|           } |   const { k1, pubkey } = credentials | ||||||
|         } catch (error) { |   try { | ||||||
|           console.log(error) |     const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } }) | ||||||
|  |     await prisma.lnAuth.delete({ where: { k1 } }) | ||||||
|  |     if (lnauth.pubkey === pubkey) { | ||||||
|  |       let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } }) | ||||||
|  |       const token = await getToken({ req }) | ||||||
|  |       if (!user) { | ||||||
|  |         // if we are logged in, update rather than create
 | ||||||
|  |         if (token?.id) { | ||||||
|  |           user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } }) | ||||||
|  |         } else { | ||||||
|  |           user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } }) | ||||||
|         } |         } | ||||||
| 
 |       } else if (token && token?.id !== user.id) { | ||||||
|         return null |         return null | ||||||
|       } |       } | ||||||
|     }), |  | ||||||
|     Providers.Credentials({ |  | ||||||
|       id: 'slashtags', |  | ||||||
|       // The name to display on the sign in form (e.g. 'Sign in with...')
 |  | ||||||
|       name: 'Slashtags', |  | ||||||
|       // 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 } }) |  | ||||||
|           await prisma.lnAuth.delete({ where: { k1 } }) |  | ||||||
|           if (lnauth.pubkey === pubkey) { |  | ||||||
|             let user = await prisma.user.findUnique({ where: { slashtagId: pubkey } }) |  | ||||||
|             req.headers = parsedHeaders(req) |  | ||||||
|             const session = await getSession({ req }) |  | ||||||
|             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: { slashtagId: pubkey } }) |  | ||||||
|               } else { |  | ||||||
|                 user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), slashtagId: pubkey } }) |  | ||||||
|               } |  | ||||||
|             } else if (session && session.user?.id !== user.id) { |  | ||||||
|               throw new Error('account not linked') |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return user |       return user | ||||||
|           } |     } | ||||||
|         } catch (error) { |   } catch (error) { | ||||||
|           console.log(error) |     console.log(error) | ||||||
|         } |   } | ||||||
| 
 | 
 | ||||||
|         return null |   return null | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const providers = [ | ||||||
|  |   CredentialsProvider({ | ||||||
|  |     id: 'lightning', | ||||||
|  |     name: 'Lightning', | ||||||
|  |     credentials: { | ||||||
|  |       pubkey: { label: 'publickey', type: 'text' }, | ||||||
|  |       k1: { label: 'k1', type: 'text' } | ||||||
|  |     }, | ||||||
|  |     authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey') | ||||||
|  |   }), | ||||||
|  |   CredentialsProvider({ | ||||||
|  |     id: 'slashtags', | ||||||
|  |     name: 'Slashtags', | ||||||
|  |     credentials: { | ||||||
|  |       pubkey: { label: 'publickey', type: 'text' }, | ||||||
|  |       k1: { label: 'k1', type: 'text' } | ||||||
|  |     }, | ||||||
|  |     authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'slashtagId') | ||||||
|  |   }), | ||||||
|  |   GitHubProvider({ | ||||||
|  |     clientId: process.env.GITHUB_ID, | ||||||
|  |     clientSecret: process.env.GITHUB_SECRET, | ||||||
|  |     authorization: { | ||||||
|  |       url: 'https://github.com/login/oauth/authorize', | ||||||
|  |       params: { scope: '' } | ||||||
|  |     }, | ||||||
|  |     profile: profile => { | ||||||
|  |       return { | ||||||
|  |         ...profile, | ||||||
|  |         name: profile.login | ||||||
|       } |       } | ||||||
|     }), |     } | ||||||
|     Providers.GitHub({ |   }), | ||||||
|       clientId: process.env.GITHUB_ID, |   TwitterProvider({ | ||||||
|       clientSecret: process.env.GITHUB_SECRET, |     clientId: process.env.TWITTER_ID, | ||||||
|       authorization: 'https://github.com/login/oauth/authorize', |     clientSecret: process.env.TWITTER_SECRET, | ||||||
|       scope: '', // read-only acces to public information
 |     profile: profile => { | ||||||
|       profile: profile => { |       return { | ||||||
|         return { |         ...profile, | ||||||
|           ...profile, |         name: profile.screen_name | ||||||
|           name: profile.login |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }), |     } | ||||||
|     Providers.Twitter({ |   }), | ||||||
|       clientId: process.env.TWITTER_ID, |   EmailProvider({ | ||||||
|       clientSecret: process.env.TWITTER_SECRET, |     server: process.env.LOGIN_EMAIL_SERVER, | ||||||
|       profile: profile => { |     from: process.env.LOGIN_EMAIL_FROM, | ||||||
|         return { |     sendVerificationRequest | ||||||
|           ...profile, |   }) | ||||||
|           name: profile.screen_name | ] | ||||||
|         } | 
 | ||||||
|       } | export const getAuthOptions = req => ({ | ||||||
|     }), |   callbacks: getCallbacks(req), | ||||||
|     Providers.Email({ |   providers, | ||||||
|       server: process.env.LOGIN_EMAIL_SERVER, |   adapter: PrismaAdapter(prisma), | ||||||
|       from: process.env.LOGIN_EMAIL_FROM, |   session: { | ||||||
|       sendVerificationRequest, |     strategy: 'jwt' | ||||||
|       profile: profile => { |  | ||||||
|         return profile |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   ], |  | ||||||
|   adapter: PrismaLegacyAdapter({ prisma }), |  | ||||||
|   secret: process.env.NEXTAUTH_SECRET, |  | ||||||
|   session: { jwt: true }, |  | ||||||
|   jwt: { |  | ||||||
|     signingKey: process.env.JWT_SIGNING_PRIVATE_KEY |  | ||||||
|   }, |   }, | ||||||
|   pages: { |   pages: { | ||||||
|     signIn: '/login', |     signIn: '/login', | ||||||
|     verifyRequest: '/email' |     verifyRequest: '/email', | ||||||
|  |     error: '/auth/error' | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|  | export default async (req, res) => { | ||||||
|  |   await NextAuth(req, res, getAuthOptions(req)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| async function sendVerificationRequest ({ | async function sendVerificationRequest ({ | ||||||
|   identifier: email, |   identifier: email, | ||||||
|   url, |   url, | ||||||
|   token, |  | ||||||
|   baseUrl, |  | ||||||
|   provider |   provider | ||||||
| }) { | }) { | ||||||
|   const user = await prisma.user.findUnique({ where: { email } }) |   const user = await prisma.user.findUnique({ where: { email } }) | ||||||
| 
 | 
 | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     const { server, from } = provider |     const { server, from } = provider | ||||||
|     // Strip protocol from URL and use domain as site name
 | 
 | ||||||
|     const site = baseUrl.replace(/^https?:\/\//, '') |     const site = new URL(url).host | ||||||
| 
 | 
 | ||||||
|     nodemailer.createTransport(server).sendMail( |     nodemailer.createTransport(server).sendMail( | ||||||
|       { |       { | ||||||
|  | |||||||
| @ -4,7 +4,8 @@ import resolvers from '../../api/resolvers' | |||||||
| import models from '../../api/models' | import models from '../../api/models' | ||||||
| import lnd from '../../api/lnd' | import lnd from '../../api/lnd' | ||||||
| import typeDefs from '../../api/typeDefs' | import typeDefs from '../../api/typeDefs' | ||||||
| import { getSession } from 'next-auth/client' | import { getServerSession } from 'next-auth/next' | ||||||
|  | import { getAuthOptions } from './auth/[...nextauth]' | ||||||
| import search from '../../api/search' | import search from '../../api/search' | ||||||
| import slashtags from '../../api/slashtags' | import slashtags from '../../api/slashtags' | ||||||
| 
 | 
 | ||||||
| @ -21,7 +22,7 @@ const apolloServer = new ApolloServer({ | |||||||
|               return (error, result) => { |               return (error, result) => { | ||||||
|                 const end = process.hrtime.bigint() |                 const end = process.hrtime.bigint() | ||||||
|                 const ms = (end - start) / 1000000n |                 const ms = (end - start) / 1000000n | ||||||
|                 if (ms > 20 && info.parentType.name !== 'User') { |                 if (ms > 5) { | ||||||
|                   console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`) |                   console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`) | ||||||
|                 } |                 } | ||||||
|                 if (error) { |                 if (error) { | ||||||
| @ -42,8 +43,8 @@ const apolloServer = new ApolloServer({ | |||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| export default startServerAndCreateNextHandler(apolloServer, { | export default startServerAndCreateNextHandler(apolloServer, { | ||||||
|   context: async (req) => { |   context: async (req, res) => { | ||||||
|     const session = await getSession({ req }) |     const session = await getServerSession(req, res, getAuthOptions(req)) | ||||||
|     return { |     return { | ||||||
|       models, |       models, | ||||||
|       lnd, |       lnd, | ||||||
|  | |||||||
| @ -61,7 +61,7 @@ async function doWithdrawal (query, res) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // create withdrawal in gql
 |   // create withdrawal in gql
 | ||||||
|   const client = await getSSRApolloClient(null, me) |   const client = await getSSRApolloClient({ me }) | ||||||
|   const { error, data } = await client.mutate({ |   const { error, data } = await client.mutate({ | ||||||
|     mutation: CREATE_WITHDRAWL, |     mutation: CREATE_WITHDRAWL, | ||||||
|     variables: { invoice: query.pr, maxFee: 10 } |     variables: { invoice: query.pr, maxFee: 10 } | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import { ITEM_OTS } from '../../../../fragments/items' | |||||||
| import stringifyCanon from 'canonical-json' | import stringifyCanon from 'canonical-json' | ||||||
| 
 | 
 | ||||||
| export default async function handler (req, res) { | export default async function handler (req, res) { | ||||||
|   const client = await getSSRApolloClient(req) |   const client = await getSSRApolloClient({ req, res }) | ||||||
|   const { data } = await client.query({ |   const { data } = await client.query({ | ||||||
|     query: ITEM_OTS, |     query: ITEM_OTS, | ||||||
|     variables: { id: req.query.id } |     variables: { id: req.query.id } | ||||||
|  | |||||||
							
								
								
									
										63
									
								
								pages/auth/error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								pages/auth/error.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | import Image from 'react-bootstrap/Image' | ||||||
|  | import { StaticLayout } from '../../components/layout' | ||||||
|  | import styles from '../../styles/error.module.css' | ||||||
|  | import LightningIcon from '../../svgs/bolt.svg' | ||||||
|  | import { useRouter } from 'next/router' | ||||||
|  | import Button from 'react-bootstrap/Button' | ||||||
|  | 
 | ||||||
|  | export function getServerSideProps ({ query }) { | ||||||
|  |   return { | ||||||
|  |     props: { | ||||||
|  |       error: query.error | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function AuthError ({ error }) { | ||||||
|  |   const router = useRouter() | ||||||
|  | 
 | ||||||
|  |   if (error === 'AccessDenied') { | ||||||
|  |     return ( | ||||||
|  |       <StaticLayout> | ||||||
|  |         <Image className='rounded-1 shadow-sm' width='500' height='381' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/forbidden.gif`} fluid /> | ||||||
|  |         <h1 className={[styles.status, styles.smaller].join(' ')}><span>ACCESS DENIED</span></h1> | ||||||
|  |       </StaticLayout> | ||||||
|  |     ) | ||||||
|  |   } else if (error === 'Verification') { | ||||||
|  |     return ( | ||||||
|  |       <StaticLayout> | ||||||
|  |         <Image className='rounded-1 shadow-sm' width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/double.gif`} fluid /> | ||||||
|  |         <h2 className='pt-4'>This magic link has expired.</h2> | ||||||
|  |         <h4 className='text-muted pt-2'>Get another by logging in.</h4> | ||||||
|  |         <Button | ||||||
|  |           className='align-items-center my-3' | ||||||
|  |           style={{ borderWidth: '2px' }} | ||||||
|  |           id='login' | ||||||
|  |           onClick={() => router.push('/login')} | ||||||
|  |           size='lg' | ||||||
|  |         > | ||||||
|  |           <LightningIcon | ||||||
|  |             width={24} | ||||||
|  |             height={24} | ||||||
|  |             className='me-2' | ||||||
|  |           />login | ||||||
|  |         </Button> | ||||||
|  |       </StaticLayout> | ||||||
|  |     ) | ||||||
|  |   } else if (error === 'Configuration') { | ||||||
|  |     return ( | ||||||
|  |       <StaticLayout> | ||||||
|  |         <Image className='rounded-1 shadow-sm' width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/double.gif`} fluid /> | ||||||
|  |         <h1 className={[styles.status, styles.smaller].join(' ')}><span>configuration error</span></h1> | ||||||
|  |       </StaticLayout> | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <StaticLayout> | ||||||
|  |       <Image className='rounded-1 shadow-sm' width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/double.gif`} fluid /> | ||||||
|  |       <h1 className={[styles.status, styles.smaller].join(' ')}><span>auth error</span></h1> | ||||||
|  |     </StaticLayout> | ||||||
|  | 
 | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @ -5,9 +5,9 @@ export default function Email () { | |||||||
|   return ( |   return ( | ||||||
|     <StaticLayout> |     <StaticLayout> | ||||||
|       <div className='p-4 text-center'> |       <div className='p-4 text-center'> | ||||||
|         <h1>Check your email</h1> |         <Image className='rounded-1 shadow-sm' width='320' height='223' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/hello.gif`} fluid /> | ||||||
|         <h4 className='pb-4'>A sign in link has been sent to your email address</h4> |         <h2 className='pt-4'>Check your email</h2> | ||||||
|         <Image width='500' height='376' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/hello.gif`} fluid /> |         <h4 className='text-muted pt-2'>A sign in link has been sent to your email address</h4> | ||||||
|       </div> |       </div> | ||||||
|     </StaticLayout> |     </StaticLayout> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import Login from '../../components/login' | import Login from '../../components/login' | ||||||
| import { providers, getSession } from 'next-auth/client' | import { getProviders } from 'next-auth/react' | ||||||
|  | import { getServerSession } from 'next-auth/next' | ||||||
| import models from '../../api/models' | import models from '../../api/models' | ||||||
| import serialize from '../../api/resolvers/serial' | import serialize from '../../api/resolvers/serial' | ||||||
| import { gql } from '@apollo/client' | import { gql } from '@apollo/client' | ||||||
| @ -7,11 +8,12 @@ import { INVITE_FIELDS } from '../../fragments/invites' | |||||||
| import getSSRApolloClient from '../../api/ssrApollo' | import getSSRApolloClient from '../../api/ssrApollo' | ||||||
| import Link from 'next/link' | import Link from 'next/link' | ||||||
| import { CenterLayout } from '../../components/layout' | import { CenterLayout } from '../../components/layout' | ||||||
|  | import { authOptions } from '../api/auth/[...nextauth]' | ||||||
| 
 | 
 | ||||||
| export async function getServerSideProps ({ req, res, query: { id, error = null } }) { | export async function getServerSideProps ({ req, res, query: { id, error = null } }) { | ||||||
|   const session = await getSession({ req }) |   const session = await getServerSession(req, res, authOptions(req)) | ||||||
| 
 | 
 | ||||||
|   const client = await getSSRApolloClient(req) |   const client = await getSSRApolloClient({ req, res }) | ||||||
|   const { data } = await client.query({ |   const { data } = await client.query({ | ||||||
|     query: gql` |     query: gql` | ||||||
|       ${INVITE_FIELDS} |       ${INVITE_FIELDS} | ||||||
| @ -38,16 +40,17 @@ export async function getServerSideProps ({ req, res, query: { id, error = null | |||||||
|       console.log(e) |       console.log(e) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.writeHead(302, { |     return { | ||||||
|       Location: '/' |       redirect: { | ||||||
|     }) |         destination: '/', | ||||||
|     res.end() |         permanent: false | ||||||
|     return { props: {} } |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     props: { |     props: { | ||||||
|       providers: await providers({ req, res }), |       providers: await getProviders(), | ||||||
|       callbackUrl: process.env.PUBLIC_URL + req.url, |       callbackUrl: process.env.PUBLIC_URL + req.url, | ||||||
|       invite: data.invite, |       invite: data.invite, | ||||||
|       error |       error | ||||||
|  | |||||||
| @ -1,11 +1,13 @@ | |||||||
| import { providers, getSession } from 'next-auth/client' | import { getProviders } from 'next-auth/react' | ||||||
|  | import { getServerSession } from 'next-auth/next' | ||||||
|  | import { getAuthOptions } from './api/auth/[...nextauth]' | ||||||
| import Link from 'next/link' | import Link from 'next/link' | ||||||
| import { StaticLayout } from '../components/layout' | import { StaticLayout } from '../components/layout' | ||||||
| import Login from '../components/login' | import Login from '../components/login' | ||||||
| import { isExternal } from '../lib/url' | import { isExternal } from '../lib/url' | ||||||
| 
 | 
 | ||||||
| export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) { | export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) { | ||||||
|   const session = await getSession({ req }) |   const session = await getServerSession(req, res, getAuthOptions(req)) | ||||||
| 
 | 
 | ||||||
|   // prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
 |   // prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
 | ||||||
|   // let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
 |   // let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
 | ||||||
| @ -20,17 +22,18 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro | |||||||
|     callbackUrl = '/' |     callbackUrl = '/' | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (session && res && callbackUrl) { |   if (session && callbackUrl) { | ||||||
|     res.writeHead(302, { |     return { | ||||||
|       Location: callbackUrl |       redirect: { | ||||||
|     }) |         destination: callbackUrl, | ||||||
|     res.end() |         permanent: false | ||||||
|     return { props: {} } |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     props: { |     props: { | ||||||
|       providers: await providers({ req, res }), |       providers: await getProviders(), | ||||||
|       callbackUrl, |       callbackUrl, | ||||||
|       error |       error | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import { useState } from 'react' | |||||||
| import { gql, useMutation, useQuery } 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 LoginButton from '../components/login-button' | ||||||
| import { signIn } from 'next-auth/client' | import { signIn } from 'next-auth/react' | ||||||
| import { LightningAuth, SlashtagsAuth } from '../components/lightning-auth' | import { LightningAuth, SlashtagsAuth } from '../components/lightning-auth' | ||||||
| import { SETTINGS, SET_SETTINGS } from '../fragments/users' | import { SETTINGS, SET_SETTINGS } from '../fragments/users' | ||||||
| import { useRouter } from 'next/router' | import { useRouter } from 'next/router' | ||||||
| @ -359,7 +359,7 @@ function AuthMethods ({ methods }) { | |||||||
|     // if there's only one auth method left
 |     // if there's only one auth method left
 | ||||||
|     const links = providers.reduce((t, p) => t + (methods[p] ? 1 : 0), 0) |     const links = providers.reduce((t, p) => t + (methods[p] ? 1 : 0), 0) | ||||||
|     if (links === 1) { |     if (links === 1) { | ||||||
|       showModal(onClose => (<UnlinkObstacle onClose={onClose} type={type} />)) |       showModal(onClose => (<UnlinkObstacle onClose={onClose} type={type} unlinkAuth={unlinkAuth} />)) | ||||||
|     } else { |     } else { | ||||||
|       await unlinkAuth({ variables: { authType: type } }) |       await unlinkAuth({ variables: { authType: type } }) | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										45
									
								
								prisma/migrations/20230727184222_next_auth_4/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								prisma/migrations/20230727184222_next_auth_4/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | /* | ||||||
|  |   Warnings: | ||||||
|  | 
 | ||||||
|  |   - You are about to drop the column `compound_id` on the `accounts` table. All the data in the column will be lost. | ||||||
|  |   - The `access_token_expires` column on the `accounts` table would be dropped and recreated. This will lead to data loss if there is data in the column. | ||||||
|  |   - You are about to drop the column `access_token` on the `sessions` table. All the data in the column will be lost. | ||||||
|  |   - A unique constraint covering the columns `[provider_id,provider_account_id]` on the table `accounts` will be added. If there are existing duplicate values, this will fail. | ||||||
|  |   - A unique constraint covering the columns `[identifier,token]` on the table `verification_requests` will be added. If there are existing duplicate values, this will fail. | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | -- DropIndex | ||||||
|  | DROP INDEX "accounts.compound_id_unique"; | ||||||
|  | 
 | ||||||
|  | -- DropIndex | ||||||
|  | DROP INDEX "accounts.provider_account_id_index"; | ||||||
|  | 
 | ||||||
|  | -- DropIndex | ||||||
|  | DROP INDEX "accounts.provider_id_index"; | ||||||
|  | 
 | ||||||
|  | -- DropIndex | ||||||
|  | DROP INDEX "sessions.access_token_unique"; | ||||||
|  | 
 | ||||||
|  | -- AlterTable | ||||||
|  | ALTER TABLE "accounts" DROP COLUMN "compound_id", | ||||||
|  | ADD COLUMN     "id_token" TEXT, | ||||||
|  | ADD COLUMN     "scope" TEXT, | ||||||
|  | ADD COLUMN     "session_state" TEXT, | ||||||
|  | ADD COLUMN     "token_type" TEXT; | ||||||
|  | 
 | ||||||
|  | ALTER TABLE accounts ALTER COLUMN "access_token_expires" TYPE TEXT USING CAST(extract(epoch FROM "access_token_expires") AS BIGINT)*1000; | ||||||
|  | 
 | ||||||
|  | -- AlterTable | ||||||
|  | ALTER TABLE "sessions" DROP COLUMN "access_token"; | ||||||
|  | 
 | ||||||
|  | -- CreateIndex | ||||||
|  | CREATE UNIQUE INDEX "accounts_provider_id_provider_account_id_key" ON "accounts"("provider_id", "provider_account_id"); | ||||||
|  | 
 | ||||||
|  | -- CreateIndex | ||||||
|  | CREATE UNIQUE INDEX "verification_requests_identifier_token_key" ON "verification_requests"("identifier", "token"); | ||||||
|  | 
 | ||||||
|  | -- AddForeignKey | ||||||
|  | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||||
|  | 
 | ||||||
|  | -- AddForeignKey | ||||||
|  | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||||
| @ -80,6 +80,8 @@ model User { | |||||||
|   photo               Upload?              @relation(fields: [photoId], references: [id]) |   photo               Upload?              @relation(fields: [photoId], references: [id]) | ||||||
|   referrer            User?                @relation("referrals", fields: [referrerId], references: [id]) |   referrer            User?                @relation("referrals", fields: [referrerId], references: [id]) | ||||||
|   referrees           User[]               @relation("referrals") |   referrees           User[]               @relation("referrals") | ||||||
|  |   Account             Account[] | ||||||
|  |   Session             Session[] | ||||||
| 
 | 
 | ||||||
|   @@index([createdAt], map: "users.created_at_index") |   @@index([createdAt], map: "users.created_at_index") | ||||||
|   @@index([inviteId], map: "users.inviteId_index") |   @@index([inviteId], map: "users.inviteId_index") | ||||||
| @ -426,37 +428,42 @@ model Withdrawl { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model Account { | model Account { | ||||||
|   id                 Int       @id @default(autoincrement()) |   id                Int      @id @default(autoincrement()) | ||||||
|   createdAt          DateTime  @default(now()) @map("created_at") |   createdAt         DateTime @default(now()) @map("created_at") | ||||||
|   updatedAt          DateTime  @updatedAt @map("updated_at") |   updatedAt         DateTime @updatedAt @map("updated_at") | ||||||
|   compoundId         String    @unique(map: "accounts.compound_id_unique") @map("compound_id") |   userId            Int      @map("user_id") | ||||||
|   userId             Int       @map("user_id") |   type              String   @map("provider_type") | ||||||
|   providerType       String    @map("provider_type") |   provider          String   @map("provider_id") | ||||||
|   providerId         String    @map("provider_id") |   providerAccountId String   @map("provider_account_id") | ||||||
|   providerAccountId  String    @map("provider_account_id") |   refresh_token     String?  @map("refresh_token") | ||||||
|   refreshToken       String?   @map("refresh_token") |   access_token      String?  @map("access_token") | ||||||
|   accessToken        String?   @map("access_token") |   expires_at        String?  @map("access_token_expires") | ||||||
|   accessTokenExpires DateTime? @map("access_token_expires") |   token_type        String? | ||||||
|  |   scope             String? | ||||||
|  |   id_token          String? | ||||||
|  |   session_state     String? | ||||||
| 
 | 
 | ||||||
|   @@index([providerAccountId], map: "accounts.provider_account_id_index") |   user User @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||||
|   @@index([providerId], map: "accounts.provider_id_index") | 
 | ||||||
|  |   @@unique([provider, providerAccountId]) | ||||||
|   @@index([userId], map: "accounts.user_id_index") |   @@index([userId], map: "accounts.user_id_index") | ||||||
|   @@map("accounts") |   @@map("accounts") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model Session { | model Session { | ||||||
|   id           Int      @id @default(autoincrement()) |   id           Int      @id @default(autoincrement()) | ||||||
|  |   sessionToken String   @unique(map: "sessions.session_token_unique") @map("session_token") | ||||||
|   createdAt    DateTime @default(now()) @map("created_at") |   createdAt    DateTime @default(now()) @map("created_at") | ||||||
|   updatedAt    DateTime @updatedAt @map("updated_at") |   updatedAt    DateTime @updatedAt @map("updated_at") | ||||||
|   userId       Int      @map("user_id") |   userId       Int      @map("user_id") | ||||||
|   expires      DateTime |   expires      DateTime | ||||||
|   sessionToken String   @unique(map: "sessions.session_token_unique") @map("session_token") | 
 | ||||||
|   accessToken  String   @unique(map: "sessions.access_token_unique") @map("access_token") |   user User @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||||
| 
 | 
 | ||||||
|   @@map("sessions") |   @@map("sessions") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model VerificationRequest { | model VerificationToken { | ||||||
|   id         Int      @id @default(autoincrement()) |   id         Int      @id @default(autoincrement()) | ||||||
|   createdAt  DateTime @default(now()) @map("created_at") |   createdAt  DateTime @default(now()) @map("created_at") | ||||||
|   updatedAt  DateTime @updatedAt @map("updated_at") |   updatedAt  DateTime @updatedAt @map("updated_at") | ||||||
| @ -464,6 +471,7 @@ model VerificationRequest { | |||||||
|   token      String   @unique(map: "verification_requests.token_unique") |   token      String   @unique(map: "verification_requests.token_unique") | ||||||
|   expires    DateTime |   expires    DateTime | ||||||
| 
 | 
 | ||||||
|  |   @@unique([identifier, token]) | ||||||
|   @@map("verification_requests") |   @@map("verification_requests") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/double.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/double.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 631 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/forbidden.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/forbidden.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 642 KiB | 
| @ -1,13 +0,0 @@ | |||||||
| .fourZeroFour { |  | ||||||
|     font-family: 'lightning'; |  | ||||||
|     font-size: 96px; |  | ||||||
|     display: flex; |  | ||||||
|     justify-content: space-evenly; |  | ||||||
|     align-items: center; |  | ||||||
|     margin-top: 1rem; |  | ||||||
|     width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .notFound { |  | ||||||
|     font-size: 24px; |  | ||||||
| } |  | ||||||
| @ -8,6 +8,10 @@ | |||||||
|     width: 100%; |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .smaller { | ||||||
|  |     font-size: 48px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .describe { | .describe { | ||||||
|     font-size: 24px; |     font-size: 24px; | ||||||
| } | } | ||||||
| @ -167,6 +167,10 @@ mark { | |||||||
|   color: #ffffff !important; |   color: #ffffff !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .btn-outline-grey-darkmode:hover, .btn-outline-grey-darkmode:active { | ||||||
|  |   color: #ffffff !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .table { | .table { | ||||||
|   color: var(--theme-color); |   color: var(--theme-color); | ||||||
|   background-color: var(--theme-body); |   background-color: var(--theme-body); | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| const PgBoss = require('pg-boss') | const PgBoss = require('pg-boss') | ||||||
| const dotenv = require('dotenv') | require('@next/env').loadEnvConfig('..') | ||||||
| dotenv.config({ path: '..' }) |  | ||||||
| const { PrismaClient } = require('@prisma/client') | const { PrismaClient } = require('@prisma/client') | ||||||
| const { checkInvoice, checkWithdrawal } = require('./wallet') | const { checkInvoice, checkWithdrawal } = require('./wallet') | ||||||
| const { repin } = require('./repin') | const { repin } = require('./repin') | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user