Account switching
This commit is contained in:
parent
5137f1423d
commit
470e0dfc7a
|
@ -27,6 +27,8 @@ import HiddenWalletSummary from './hidden-wallet-summary'
|
|||
import { clearNotifications } from '../lib/badge'
|
||||
import { useServiceWorker } from './serviceworker'
|
||||
import SubSelect from './sub-select'
|
||||
import { useShowModal } from './modal'
|
||||
import SwitchAccountDialog from './switch-account'
|
||||
|
||||
function WalletSummary ({ me }) {
|
||||
if (!me) return null
|
||||
|
@ -86,6 +88,7 @@ function NotificationBell () {
|
|||
|
||||
function NavProfileMenu ({ me, dropNavKey }) {
|
||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
const showModal = useShowModal()
|
||||
return (
|
||||
<div className='position-relative'>
|
||||
<Dropdown className={styles.dropdown} align='end'>
|
||||
|
@ -124,6 +127,7 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
|||
</Link>
|
||||
</div>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
|
|
@ -11,7 +11,7 @@ import BackIcon from '../svgs/arrow-left-line.svg'
|
|||
import { useRouter } from 'next/router'
|
||||
import { SSR } from '../lib/constants'
|
||||
|
||||
function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
||||
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||
const query = gql`
|
||||
{
|
||||
lnAuth(k1: "${k1}") {
|
||||
|
@ -23,7 +23,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
|||
|
||||
useEffect(() => {
|
||||
if (data?.lnAuth?.pubkey) {
|
||||
signIn('lightning', { ...data.lnAuth, callbackUrl })
|
||||
signIn('lightning', { ...data.lnAuth, callbackUrl, multiAuth })
|
||||
}
|
||||
}, [data?.lnAuth])
|
||||
|
||||
|
@ -89,15 +89,15 @@ function LightningExplainer ({ text, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function LightningAuthWithExplainer ({ text, callbackUrl }) {
|
||||
export function LightningAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||
return (
|
||||
<LightningExplainer text={text}>
|
||||
<LightningAuth callbackUrl={callbackUrl} />
|
||||
<LightningAuth callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||
</LightningExplainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function LightningAuth ({ callbackUrl }) {
|
||||
export function LightningAuth ({ callbackUrl, multiAuth }) {
|
||||
// query for challenge
|
||||
const [createAuth, { data, error }] = useMutation(gql`
|
||||
mutation createAuth {
|
||||
|
@ -113,5 +113,5 @@ export function LightningAuth ({ callbackUrl }) {
|
|||
|
||||
if (error) return <div>error</div>
|
||||
|
||||
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} /> : <QrSkeleton status='generating' />
|
||||
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} multiAuth={multiAuth} /> : <QrSkeleton status='generating' />
|
||||
}
|
||||
|
|
|
@ -48,12 +48,12 @@ export function authErrorMessage (error) {
|
|||
return error && (authErrorMessages[error] ?? authErrorMessages.default)
|
||||
}
|
||||
|
||||
export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) {
|
||||
export default function Login ({ providers, callbackUrl, error, multiAuth, text, Header, Footer }) {
|
||||
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
||||
const router = useRouter()
|
||||
|
||||
if (router.query.type === 'lightning') {
|
||||
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} />
|
||||
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||
}
|
||||
|
||||
if (router.query.type === 'nostr') {
|
||||
|
|
|
@ -8,15 +8,21 @@ export const MeContext = React.createContext({
|
|||
})
|
||||
|
||||
export function MeProvider ({ me, children }) {
|
||||
const { data } = useQuery(ME, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||
|
||||
return (
|
||||
<MeContext.Provider value={data?.me || me}>
|
||||
<MeContext.Provider value={{ me: data?.me || me, refetch }}>
|
||||
{children}
|
||||
</MeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useMe () {
|
||||
return useContext(MeContext)
|
||||
const { me } = useContext(MeContext)
|
||||
return me
|
||||
}
|
||||
|
||||
export function useMeRefresh () {
|
||||
const { refetch } = useContext(MeContext)
|
||||
return refetch
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import AnonIcon from '../svgs/spy-fill.svg'
|
||||
import { useRouter } from 'next/router'
|
||||
import cookie from 'cookie'
|
||||
import { useMe, useMeRefresh } from './me'
|
||||
import Image from 'react-bootstrap/Image'
|
||||
import Link from 'next/link'
|
||||
|
||||
const AccountContext = createContext()
|
||||
|
||||
export const AccountProvider = ({ children }) => {
|
||||
const me = useMe()
|
||||
const [accounts, setAccounts] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
||||
const accounts = multiAuthCookie
|
||||
? JSON.parse(multiAuthCookie)
|
||||
: me ? [{ id: me.id, name: me.name, photoId: me.photoId }] : []
|
||||
setAccounts(accounts)
|
||||
}, [])
|
||||
|
||||
const addAccount = useCallback(user => {
|
||||
setAccounts(accounts => [...accounts, user])
|
||||
}, [setAccounts])
|
||||
|
||||
const removeAccount = useCallback(userId => {
|
||||
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
||||
}, [setAccounts])
|
||||
|
||||
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount }}>{children}</AccountContext.Provider>
|
||||
}
|
||||
|
||||
const useAccounts = () => useContext(AccountContext)
|
||||
|
||||
const AnonAccount = () => {
|
||||
const me = useMe()
|
||||
const refreshMe = useMeRefresh()
|
||||
return (
|
||||
<div
|
||||
className='d-flex flex-column me-2 my-1 text-center'
|
||||
>
|
||||
<AnonIcon
|
||||
className='fill-muted'
|
||||
width='135' height='135' style={{ cursor: 'pointer' }} onClick={() => {
|
||||
document.cookie = 'multi_auth.user-id=anonymous'
|
||||
refreshMe()
|
||||
}}
|
||||
/>
|
||||
<div className='fst-italic'>anonymous</div>
|
||||
{!me && <div className='text-muted fst-italic'>selected</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Account = ({ account, className }) => {
|
||||
const me = useMe()
|
||||
const refreshMe = useMeRefresh()
|
||||
const src = account.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${account.photoId}` : '/dorian400.jpg'
|
||||
return (
|
||||
<div
|
||||
className='d-flex flex-column me-2 my-1 text-center'
|
||||
>
|
||||
<Image
|
||||
width='135' height='135' src={src} style={{ cursor: 'pointer' }} onClick={() => {
|
||||
document.cookie = `multi_auth.user-id=${account.id}`
|
||||
refreshMe()
|
||||
}}
|
||||
/>
|
||||
<Link href={`/${account.name}`}>@{account.name}</Link>
|
||||
{Number(me?.id) === Number(account.id) && <div className='text-muted fst-italic'>selected</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AddAccount = () => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className='d-flex flex-column me-2 my-1 text-center'>
|
||||
<Image
|
||||
width='135' height='135' src='https://imgs.search.brave.com/t8qv-83e1m_kaajLJoJ0GNID5ch0WvBGmy7Pkyr4kQY/rs:fit:860:0:0/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvY29tbW9ucy84/Lzg5L1BvcnRyYWl0/X1BsYWNlaG9sZGVy/LnBuZw' style={{ cursor: 'pointer' }} onClick={() => {
|
||||
router.push({
|
||||
pathname: '/login',
|
||||
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div className='fst-italic'>+ add account</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SwitchAccountDialog () {
|
||||
const { accounts } = useAccounts()
|
||||
return (
|
||||
<>
|
||||
<h3>Switch Account</h3>
|
||||
<div className='my-2'>
|
||||
<div className='d-flex flex-row flex-wrap'>
|
||||
<AnonAccount />
|
||||
{
|
||||
accounts.map((account) => <Account key={account.id} account={account} />)
|
||||
}
|
||||
<AddAccount />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -8,6 +8,7 @@ export const ME = gql`
|
|||
id
|
||||
name
|
||||
bioId
|
||||
photoId
|
||||
privates {
|
||||
autoDropBolt11s
|
||||
diagnostics
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export function middleware (request) {
|
||||
const referrerMiddleware = (request) => {
|
||||
const regex = /(\/.*)?\/r\/([\w_]+)/
|
||||
const m = regex.exec(request.nextUrl.pathname)
|
||||
|
||||
|
@ -13,6 +13,23 @@ export function middleware (request) {
|
|||
return resp
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/(.*/|)r/([\\w_]+)([?#]?.*)']
|
||||
const multiAuthMiddleware = (request) => {
|
||||
// switch next-auth session cookie with multi_auth cookie if cookie pointer present
|
||||
const userId = request.cookies?.get('multi_auth.user-id')?.value
|
||||
const sessionCookieName = '__Secure-next-auth.session-token'
|
||||
const hasSession = request.cookies?.has(sessionCookieName)
|
||||
if (userId && hasSession) {
|
||||
const userJWT = request.cookies.get(`multi_auth.${userId}`)?.value
|
||||
if (userJWT) request.cookies.set(sessionCookieName, userJWT)
|
||||
}
|
||||
const response = NextResponse.next({ request })
|
||||
return response
|
||||
}
|
||||
|
||||
export function middleware (request) {
|
||||
const referrerRegexp = /(\/.*)?\/r\/([\w_]+)/
|
||||
if (referrerRegexp.test(request.nextUrl.pathname)) {
|
||||
return referrerMiddleware(request)
|
||||
}
|
||||
return multiAuthMiddleware(request)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"bootstrap": "^5.3.2",
|
||||
"canonical-json": "0.0.4",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"cookie": "^0.6.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"domino": "^2.1.6",
|
||||
|
@ -504,6 +505,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/prisma-adapter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
||||
|
@ -5723,9 +5732,9 @@
|
|||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
@ -7341,6 +7350,14 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -11443,6 +11460,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next-plausible": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
||||
|
@ -16845,6 +16870,13 @@
|
|||
"oauth4webapi": "^2.0.6",
|
||||
"preact": "10.11.3",
|
||||
"preact-render-to-string": "5.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@auth/prisma-adapter": {
|
||||
|
@ -20626,9 +20658,9 @@
|
|||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
|
@ -21811,6 +21843,11 @@
|
|||
"vary": "~1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -24577,6 +24614,13 @@
|
|||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"next-plausible": {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"bootstrap": "^5.3.2",
|
||||
"canonical-json": "0.0.4",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"cookie": "^0.6.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"domino": "^2.1.6",
|
||||
|
|
|
@ -18,6 +18,7 @@ import NProgress from 'nprogress'
|
|||
import 'nprogress/nprogress.css'
|
||||
import { LoggerProvider } from '../components/logger'
|
||||
import { ChainFeeProvider } from '../components/chain-fee.js'
|
||||
import { AccountProvider } from '../components/switch-account'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||
|
@ -95,22 +96,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||
<MeProvider me={me}>
|
||||
<LoggerProvider>
|
||||
<ServiceWorkerProvider>
|
||||
<PriceProvider price={price}>
|
||||
<LightningProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
<PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</LightningProvider>
|
||||
</PriceProvider>
|
||||
<AccountProvider>
|
||||
<PriceProvider price={price}>
|
||||
<LightningProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
<PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</LightningProvider>
|
||||
</PriceProvider>
|
||||
</AccountProvider>
|
||||
</ServiceWorkerProvider>
|
||||
</LoggerProvider>
|
||||
</MeProvider>
|
||||
|
|
|
@ -7,10 +7,12 @@ import EmailProvider from 'next-auth/providers/email'
|
|||
import prisma from '../../../api/models'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||
import { getToken } from 'next-auth/jwt'
|
||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||
import { getToken, encode as encodeJWT } from 'next-auth/jwt'
|
||||
import { datePivot } from '../../../lib/time'
|
||||
import { NodeNextRequest, NodeNextResponse } from 'next/dist/server/base-http/node'
|
||||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { sendUserNotification } from '../../../api/webPush'
|
||||
import cookie from 'cookie'
|
||||
|
||||
function getCallbacks (req) {
|
||||
return {
|
||||
|
@ -77,8 +79,8 @@ function getCallbacks (req) {
|
|||
}
|
||||
}
|
||||
|
||||
async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
||||
const { k1, pubkey } = credentials
|
||||
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
|
||||
const { k1, pubkey, multiAuth } = credentials
|
||||
try {
|
||||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||
await prisma.lnAuth.delete({ where: { k1 } })
|
||||
|
@ -88,11 +90,47 @@ async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
|||
if (!user) {
|
||||
// if we are logged in, update rather than create
|
||||
if (token?.id) {
|
||||
// TODO: consider multiauth if logged in but user does not exist yet
|
||||
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) {
|
||||
if (multiAuth) {
|
||||
// we want to add a new account to 'switch accounts'
|
||||
const secret = process.env.NEXTAUTH_SECRET
|
||||
// default expiration for next-auth JWTs is in 1 month
|
||||
const expiresAt = datePivot(new Date(), { months: 1 })
|
||||
const cookieOptions = {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt
|
||||
}
|
||||
const userJWT = await encodeJWT({
|
||||
token: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email
|
||||
},
|
||||
secret
|
||||
})
|
||||
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
||||
const tokenJWT = await encodeJWT({ token, secret })
|
||||
// NOTE: why can't I put this in a function with a for loop?!
|
||||
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${user.id}`, userJWT, cookieOptions))
|
||||
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${me.id}`, tokenJWT, cookieOptions))
|
||||
res.appendHeader('Set-Cookie',
|
||||
cookie.serialize('multi_auth',
|
||||
JSON.stringify([
|
||||
{ id: user.id, name: user.name, photoId: user.photoId },
|
||||
{ id: me.id, name: me.name, photoId: me.photoId }
|
||||
]),
|
||||
{ ...cookieOptions, httpOnly: false }))
|
||||
// don't switch accounts, we only want to add. switching is done in client via "pointer cookie"
|
||||
return token
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -136,7 +174,7 @@ async function nostrEventAuth (event) {
|
|||
return { k1, pubkey }
|
||||
}
|
||||
|
||||
const providers = [
|
||||
const getProviders = res => [
|
||||
CredentialsProvider({
|
||||
id: 'lightning',
|
||||
name: 'Lightning',
|
||||
|
@ -144,7 +182,9 @@ const providers = [
|
|||
pubkey: { label: 'publickey', type: 'text' },
|
||||
k1: { label: 'k1', type: 'text' }
|
||||
},
|
||||
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
|
||||
authorize: async (credentials, req) => {
|
||||
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'pubkey')
|
||||
}
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: 'nostr',
|
||||
|
@ -154,7 +194,7 @@ const providers = [
|
|||
},
|
||||
authorize: async ({ event }, req) => {
|
||||
const credentials = await nostrEventAuth(event)
|
||||
return await pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
|
||||
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'nostrAuthPubkey')
|
||||
}
|
||||
}),
|
||||
GitHubProvider({
|
||||
|
@ -188,9 +228,9 @@ const providers = [
|
|||
})
|
||||
]
|
||||
|
||||
export const getAuthOptions = req => ({
|
||||
export const getAuthOptions = (req, res) => ({
|
||||
callbacks: getCallbacks(req),
|
||||
providers,
|
||||
providers: getProviders(res),
|
||||
adapter: PrismaAdapter(prisma),
|
||||
session: {
|
||||
strategy: 'jwt'
|
||||
|
@ -203,7 +243,7 @@ export const getAuthOptions = req => ({
|
|||
})
|
||||
|
||||
export default async (req, res) => {
|
||||
await NextAuth(req, res, getAuthOptions(req))
|
||||
await NextAuth(req, res, getAuthOptions(req, res))
|
||||
}
|
||||
|
||||
async function sendVerificationRequest ({
|
||||
|
|
|
@ -6,7 +6,7 @@ import { StaticLayout } from '../components/layout'
|
|||
import Login from '../components/login'
|
||||
import { isExternal } from '../lib/url'
|
||||
|
||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
|
||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, multiAuth = false, error = null } }) {
|
||||
const session = await getServerSession(req, res, getAuthOptions(req))
|
||||
|
||||
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
|
||||
|
@ -22,9 +22,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
|||
callbackUrl = '/'
|
||||
}
|
||||
|
||||
if (session && callbackUrl) {
|
||||
// in the cause of auth linking we want to pass the error back to
|
||||
// settings
|
||||
if (session && callbackUrl && !multiAuth) {
|
||||
// in the cause of auth linking we want to pass the error back to settings
|
||||
// in the case of multiauth, don't redirect if there is already a session
|
||||
if (error) {
|
||||
const url = new URL(callbackUrl, process.env.PUBLIC_URL)
|
||||
url.searchParams.set('error', error)
|
||||
|
@ -43,7 +43,8 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
|||
props: {
|
||||
providers: await getProviders(),
|
||||
callbackUrl,
|
||||
error
|
||||
error,
|
||||
multiAuth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue