upgrade to next-auth 4 (bonus: improve error pages)

This commit is contained in:
keyan 2023-07-29 14:38:20 -05:00
parent de089aa429
commit 5232b59625
35 changed files with 643 additions and 1374 deletions

View File

@ -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))

View File

@ -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' } })
} }

View File

@ -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,

View File

@ -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>
) )
} }

View File

@ -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'

View File

@ -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 (

View File

@ -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`}
/> />

View File

@ -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>
) )

View File

@ -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'

View File

@ -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>

View File

@ -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
} }
} }

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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>
)
} }

View File

@ -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>
)
} }

View File

@ -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
View 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 }
}

View File

@ -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(
{ {

View File

@ -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,

View File

@ -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 }

View File

@ -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
View 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>
)
}

View File

@ -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>
) )

View File

@ -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

View File

@ -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
} }

View File

@ -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 } })
} }

View 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;

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

BIN
public/forbidden.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

View File

@ -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;
}

View File

@ -8,6 +8,10 @@
width: 100%; width: 100%;
} }
.smaller {
font-size: 48px;
}
.describe { .describe {
font-size: 24px; font-size: 24px;
} }

View File

@ -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);

View File

@ -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')