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 { clearNotifications } from '../lib/badge'
|
||||||
import { useServiceWorker } from './serviceworker'
|
import { useServiceWorker } from './serviceworker'
|
||||||
import SubSelect from './sub-select'
|
import SubSelect from './sub-select'
|
||||||
|
import { useShowModal } from './modal'
|
||||||
|
import SwitchAccountDialog from './switch-account'
|
||||||
|
|
||||||
function WalletSummary ({ me }) {
|
function WalletSummary ({ me }) {
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
@ -86,6 +88,7 @@ function NotificationBell () {
|
||||||
|
|
||||||
function NavProfileMenu ({ me, dropNavKey }) {
|
function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
|
const showModal = useShowModal()
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
|
@ -124,6 +127,7 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
|
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import BackIcon from '../svgs/arrow-left-line.svg'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { SSR } from '../lib/constants'
|
import { SSR } from '../lib/constants'
|
||||||
|
|
||||||
function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||||
const query = gql`
|
const query = gql`
|
||||||
{
|
{
|
||||||
lnAuth(k1: "${k1}") {
|
lnAuth(k1: "${k1}") {
|
||||||
|
@ -23,7 +23,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.lnAuth?.pubkey) {
|
if (data?.lnAuth?.pubkey) {
|
||||||
signIn('lightning', { ...data.lnAuth, callbackUrl })
|
signIn('lightning', { ...data.lnAuth, callbackUrl, multiAuth })
|
||||||
}
|
}
|
||||||
}, [data?.lnAuth])
|
}, [data?.lnAuth])
|
||||||
|
|
||||||
|
@ -89,15 +89,15 @@ function LightningExplainer ({ text, children }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuthWithExplainer ({ text, callbackUrl }) {
|
export function LightningAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
return (
|
return (
|
||||||
<LightningExplainer text={text}>
|
<LightningExplainer text={text}>
|
||||||
<LightningAuth callbackUrl={callbackUrl} />
|
<LightningAuth callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</LightningExplainer>
|
</LightningExplainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuth ({ callbackUrl }) {
|
export function LightningAuth ({ callbackUrl, multiAuth }) {
|
||||||
// query for challenge
|
// query for challenge
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
|
@ -113,5 +113,5 @@ export function LightningAuth ({ callbackUrl }) {
|
||||||
|
|
||||||
if (error) return <div>error</div>
|
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)
|
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 [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
if (router.query.type === 'lightning') {
|
if (router.query.type === 'lightning') {
|
||||||
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} />
|
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (router.query.type === 'nostr') {
|
if (router.query.type === 'nostr') {
|
||||||
|
|
|
@ -8,15 +8,21 @@ export const MeContext = React.createContext({
|
||||||
})
|
})
|
||||||
|
|
||||||
export function MeProvider ({ me, children }) {
|
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 (
|
return (
|
||||||
<MeContext.Provider value={data?.me || me}>
|
<MeContext.Provider value={{ me: data?.me || me, refetch }}>
|
||||||
{children}
|
{children}
|
||||||
</MeContext.Provider>
|
</MeContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMe () {
|
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
|
id
|
||||||
name
|
name
|
||||||
bioId
|
bioId
|
||||||
|
photoId
|
||||||
privates {
|
privates {
|
||||||
autoDropBolt11s
|
autoDropBolt11s
|
||||||
diagnostics
|
diagnostics
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
export function middleware (request) {
|
const referrerMiddleware = (request) => {
|
||||||
const regex = /(\/.*)?\/r\/([\w_]+)/
|
const regex = /(\/.*)?\/r\/([\w_]+)/
|
||||||
const m = regex.exec(request.nextUrl.pathname)
|
const m = regex.exec(request.nextUrl.pathname)
|
||||||
|
|
||||||
|
@ -13,6 +13,23 @@ export function middleware (request) {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
const multiAuthMiddleware = (request) => {
|
||||||
matcher: ['/(.*/|)r/([\\w_]+)([?#]?.*)']
|
// 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",
|
"bootstrap": "^5.3.2",
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"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": {
|
"node_modules/@auth/prisma-adapter": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
||||||
|
@ -5723,9 +5732,9 @@
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
|
@ -7341,6 +7350,14 @@
|
||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"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": {
|
"node_modules/next-plausible": {
|
||||||
"version": "3.11.1",
|
"version": "3.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
||||||
|
@ -16845,6 +16870,13 @@
|
||||||
"oauth4webapi": "^2.0.6",
|
"oauth4webapi": "^2.0.6",
|
||||||
"preact": "10.11.3",
|
"preact": "10.11.3",
|
||||||
"preact-render-to-string": "5.2.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": {
|
"@auth/prisma-adapter": {
|
||||||
|
@ -20626,9 +20658,9 @@
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
},
|
},
|
||||||
"cookie": {
|
"cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||||
},
|
},
|
||||||
"cookie-signature": {
|
"cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
|
@ -21811,6 +21843,11 @@
|
||||||
"vary": "~1.1.2"
|
"vary": "~1.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
@ -24577,6 +24614,13 @@
|
||||||
"preact": "^10.6.3",
|
"preact": "^10.6.3",
|
||||||
"preact-render-to-string": "^5.1.19",
|
"preact-render-to-string": "^5.1.19",
|
||||||
"uuid": "^8.3.2"
|
"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": {
|
"next-plausible": {
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import NProgress from 'nprogress'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
import { LoggerProvider } from '../components/logger'
|
import { LoggerProvider } from '../components/logger'
|
||||||
import { ChainFeeProvider } from '../components/chain-fee.js'
|
import { ChainFeeProvider } from '../components/chain-fee.js'
|
||||||
|
import { AccountProvider } from '../components/switch-account'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||||
|
@ -95,22 +96,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||||
<MeProvider me={me}>
|
<MeProvider me={me}>
|
||||||
<LoggerProvider>
|
<LoggerProvider>
|
||||||
<ServiceWorkerProvider>
|
<ServiceWorkerProvider>
|
||||||
<PriceProvider price={price}>
|
<AccountProvider>
|
||||||
<LightningProvider>
|
<PriceProvider price={price}>
|
||||||
<ToastProvider>
|
<LightningProvider>
|
||||||
<ShowModalProvider>
|
<ToastProvider>
|
||||||
<BlockHeightProvider blockHeight={blockHeight}>
|
<ShowModalProvider>
|
||||||
<ChainFeeProvider chainFee={chainFee}>
|
<BlockHeightProvider blockHeight={blockHeight}>
|
||||||
<ErrorBoundary>
|
<ChainFeeProvider chainFee={chainFee}>
|
||||||
<Component ssrData={ssrData} {...otherProps} />
|
<ErrorBoundary>
|
||||||
<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} />
|
<Component ssrData={ssrData} {...otherProps} />
|
||||||
</ErrorBoundary>
|
<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} />
|
||||||
</ChainFeeProvider>
|
</ErrorBoundary>
|
||||||
</BlockHeightProvider>
|
</ChainFeeProvider>
|
||||||
</ShowModalProvider>
|
</BlockHeightProvider>
|
||||||
</ToastProvider>
|
</ShowModalProvider>
|
||||||
</LightningProvider>
|
</ToastProvider>
|
||||||
</PriceProvider>
|
</LightningProvider>
|
||||||
|
</PriceProvider>
|
||||||
|
</AccountProvider>
|
||||||
</ServiceWorkerProvider>
|
</ServiceWorkerProvider>
|
||||||
</LoggerProvider>
|
</LoggerProvider>
|
||||||
</MeProvider>
|
</MeProvider>
|
||||||
|
|
|
@ -7,10 +7,12 @@ 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 { PrismaAdapter } from '@auth/prisma-adapter'
|
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import { getToken } from 'next-auth/jwt'
|
import { getToken, encode as encodeJWT } from 'next-auth/jwt'
|
||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { datePivot } from '../../../lib/time'
|
||||||
|
import { NodeNextRequest, NodeNextResponse } from 'next/dist/server/base-http/node'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { sendUserNotification } from '../../../api/webPush'
|
import { sendUserNotification } from '../../../api/webPush'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
|
||||||
function getCallbacks (req) {
|
function getCallbacks (req) {
|
||||||
return {
|
return {
|
||||||
|
@ -77,8 +79,8 @@ function getCallbacks (req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
|
||||||
const { k1, pubkey } = credentials
|
const { k1, pubkey, multiAuth } = credentials
|
||||||
try {
|
try {
|
||||||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||||
await prisma.lnAuth.delete({ where: { k1 } })
|
await prisma.lnAuth.delete({ where: { k1 } })
|
||||||
|
@ -88,11 +90,47 @@ async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// if we are logged in, update rather than create
|
// if we are logged in, update rather than create
|
||||||
if (token?.id) {
|
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 } })
|
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
|
||||||
} else {
|
} else {
|
||||||
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
||||||
}
|
}
|
||||||
} else if (token && token?.id !== user.id) {
|
} 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +174,7 @@ async function nostrEventAuth (event) {
|
||||||
return { k1, pubkey }
|
return { k1, pubkey }
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = [
|
const getProviders = res => [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'lightning',
|
id: 'lightning',
|
||||||
name: 'Lightning',
|
name: 'Lightning',
|
||||||
|
@ -144,7 +182,9 @@ const providers = [
|
||||||
pubkey: { label: 'publickey', type: 'text' },
|
pubkey: { label: 'publickey', type: 'text' },
|
||||||
k1: { label: 'k1', 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({
|
CredentialsProvider({
|
||||||
id: 'nostr',
|
id: 'nostr',
|
||||||
|
@ -154,7 +194,7 @@ const providers = [
|
||||||
},
|
},
|
||||||
authorize: async ({ event }, req) => {
|
authorize: async ({ event }, req) => {
|
||||||
const credentials = await nostrEventAuth(event)
|
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({
|
GitHubProvider({
|
||||||
|
@ -188,9 +228,9 @@ const providers = [
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
export const getAuthOptions = req => ({
|
export const getAuthOptions = (req, res) => ({
|
||||||
callbacks: getCallbacks(req),
|
callbacks: getCallbacks(req),
|
||||||
providers,
|
providers: getProviders(res),
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt'
|
strategy: 'jwt'
|
||||||
|
@ -203,7 +243,7 @@ export const getAuthOptions = req => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await NextAuth(req, res, getAuthOptions(req))
|
await NextAuth(req, res, getAuthOptions(req, res))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVerificationRequest ({
|
async function sendVerificationRequest ({
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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, multiAuth = false, error = null } }) {
|
||||||
const session = await getServerSession(req, res, getAuthOptions(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
|
||||||
|
@ -22,9 +22,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
callbackUrl = '/'
|
callbackUrl = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && callbackUrl) {
|
if (session && callbackUrl && !multiAuth) {
|
||||||
// in the cause of auth linking we want to pass the error back to
|
// in the cause of auth linking we want to pass the error back to settings
|
||||||
// settings
|
// in the case of multiauth, don't redirect if there is already a session
|
||||||
if (error) {
|
if (error) {
|
||||||
const url = new URL(callbackUrl, process.env.PUBLIC_URL)
|
const url = new URL(callbackUrl, process.env.PUBLIC_URL)
|
||||||
url.searchParams.set('error', error)
|
url.searchParams.set('error', error)
|
||||||
|
@ -43,7 +43,8 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
props: {
|
props: {
|
||||||
providers: await getProviders(),
|
providers: await getProviders(),
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
error
|
error,
|
||||||
|
multiAuth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue