Compare commits
39 Commits
master
...
489-accoun
Author | SHA1 | Date |
---|---|---|
ekzyis | 7b7a5ebeaa | |
ekzyis | 499ba408ea | |
ekzyis | 25d5bb53bb | |
ekzyis | 1845db2da3 | |
ekzyis | 3e6748f94b | |
ekzyis | 74304e2f75 | |
ekzyis | 09b468660b | |
ekzyis | 703d7e3cb5 | |
ekzyis | 175124bcc5 | |
ekzyis | 9aeeae99d1 | |
ekzyis | d0ea0d3917 | |
ekzyis | c1e7b93688 | |
ekzyis | 47dc05d285 | |
ekzyis | 0e04daebfb | |
ekzyis | 051ba7397a | |
ekzyis | 31dec0eee6 | |
ekzyis | dbf1fbb6b8 | |
ekzyis | 31cfd22480 | |
ekzyis | 7f1275a2e6 | |
ekzyis | c480fd450b | |
ekzyis | c235ca3fe7 | |
ekzyis | 58a1ee929b | |
ekzyis | 64e176ce1d | |
ekzyis | 3bb24652b3 | |
ekzyis | 260c97624b | |
ekzyis | 111d5ea610 | |
ekzyis | fca2ec3e15 | |
ekzyis | 9baf5063e1 | |
ekzyis | 0caa517cd5 | |
ekzyis | aae0d3765a | |
ekzyis | 4c5e470caf | |
ekzyis | 369328da15 | |
ekzyis | c610f20773 | |
ekzyis | d0a47fd304 | |
ekzyis | 01fe205350 | |
ekzyis | 78e50be2f7 | |
ekzyis | b8243f8a87 | |
ekzyis | 24168f556e | |
ekzyis | 470e0dfc7a |
|
@ -66,8 +66,9 @@ export default {
|
||||||
|
|
||||||
return await models.user.findUnique({ where: { id: me.id } })
|
return await models.user.findUnique({ where: { id: me.id } })
|
||||||
},
|
},
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { id, name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
if (id) id = Number(id)
|
||||||
|
return await models.user.findUnique({ where: { id, name } })
|
||||||
},
|
},
|
||||||
users: async (parent, args, { models }) =>
|
users: async (parent, args, { models }) =>
|
||||||
await models.user.findMany(),
|
await models.user.findMany(),
|
||||||
|
|
|
@ -4,7 +4,7 @@ export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
me: User
|
me: User
|
||||||
settings: User
|
settings: User
|
||||||
user(name: String!): User
|
user(id: ID, name: String): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
nameAvailable(name: String!): Boolean!
|
nameAvailable(name: String!): Boolean!
|
||||||
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Users
|
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Users
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { useMe } from './me'
|
||||||
|
import { ANON_USER_ID, SSR } from '../lib/constants'
|
||||||
|
import { USER } from '../fragments/users'
|
||||||
|
import { useApolloClient, useQuery } from '@apollo/client'
|
||||||
|
import { UserListRow } from './user-list'
|
||||||
|
|
||||||
|
const AccountContext = createContext()
|
||||||
|
|
||||||
|
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
|
||||||
|
const secureCookie = cookie => {
|
||||||
|
return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccountProvider = ({ children }) => {
|
||||||
|
const { me } = useMe()
|
||||||
|
const [accounts, setAccounts] = useState([])
|
||||||
|
const [isAnon, setIsAnon] = useState(true)
|
||||||
|
|
||||||
|
const updateAccountsFromCookie = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
||||||
|
const accounts = multiAuthCookie
|
||||||
|
? JSON.parse(b64Decode(multiAuthCookie))
|
||||||
|
: me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
|
||||||
|
setAccounts(accounts)
|
||||||
|
// required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
|
||||||
|
// this is the case for sessions that existed before we deployed account switching
|
||||||
|
if (!multiAuthCookie && !!me) {
|
||||||
|
document.cookie = secureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error parsing cookies:', err)
|
||||||
|
}
|
||||||
|
}, [setAccounts])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateAccountsFromCookie()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const addAccount = useCallback(user => {
|
||||||
|
setAccounts(accounts => [...accounts, user])
|
||||||
|
}, [setAccounts])
|
||||||
|
|
||||||
|
const removeAccount = useCallback(userId => {
|
||||||
|
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
||||||
|
}, [setAccounts])
|
||||||
|
|
||||||
|
const multiAuthSignout = useCallback(async () => {
|
||||||
|
// switch to next available account
|
||||||
|
const { status } = await fetch('/api/signout', { credentials: 'include' })
|
||||||
|
// if status is 201, this mean the server was able to switch us to the next available account
|
||||||
|
// and the current account was simply removed from the list of available accounts including the corresponding JWT.
|
||||||
|
// -> update needed to sync state with cookies
|
||||||
|
if (status === 201) updateAccountsFromCookie()
|
||||||
|
return status
|
||||||
|
}, [updateAccountsFromCookie])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// document not defined on server
|
||||||
|
if (SSR) return
|
||||||
|
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
|
||||||
|
setIsAnon(multiAuthUserIdCookie === 'anonymous')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount, isAnon, setIsAnon, multiAuthSignout }}>{children}</AccountContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAccounts = () => useContext(AccountContext)
|
||||||
|
|
||||||
|
const AccountListRow = ({ account, ...props }) => {
|
||||||
|
const { isAnon, setIsAnon } = useAccounts()
|
||||||
|
const { me, refreshMe } = useMe()
|
||||||
|
const anonRow = account.id === ANON_USER_ID
|
||||||
|
const selected = (isAnon && anonRow) || Number(me?.id) === Number(account.id)
|
||||||
|
const client = useApolloClient()
|
||||||
|
|
||||||
|
// fetch updated names and photo ids since they might have changed since we were issued the JWTs
|
||||||
|
const [name, setName] = useState(account.name)
|
||||||
|
const [photoId, setPhotoId] = useState(account.photoId)
|
||||||
|
useQuery(USER,
|
||||||
|
{
|
||||||
|
variables: { id: account.id },
|
||||||
|
onCompleted ({ user: { name, photoId } }) {
|
||||||
|
if (photoId) setPhotoId(photoId)
|
||||||
|
if (name) setName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClick = async (e) => {
|
||||||
|
// prevent navigation
|
||||||
|
e.preventDefault()
|
||||||
|
document.cookie = secureCookie(`multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/`)
|
||||||
|
if (anonRow) {
|
||||||
|
// order is important to prevent flashes of no session
|
||||||
|
setIsAnon(true)
|
||||||
|
await refreshMe()
|
||||||
|
} else {
|
||||||
|
await refreshMe()
|
||||||
|
// order is important to prevent flashes of inconsistent data in switch account dialog
|
||||||
|
setIsAnon(account.id === ANON_USER_ID)
|
||||||
|
}
|
||||||
|
await client.refetchQueries({ include: 'active' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='d-flex flex-row'>
|
||||||
|
<UserListRow user={{ ...account, photoId, name }} className='d-flex align-items-center me-2' {...props} onNymClick={onClick} />
|
||||||
|
{selected && <div className='text-muted fst-italic text-muted'>selected</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwitchAccountList () {
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
const router = useRouter()
|
||||||
|
const addAccount = () => {
|
||||||
|
router.push({
|
||||||
|
pathname: '/login',
|
||||||
|
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// can't show hat since the streak is not included in the JWT payload
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='my-2'>
|
||||||
|
<div className='d-flex flex-column flex-wrap'>
|
||||||
|
<AccountListRow account={{ id: ANON_USER_ID, name: 'anon' }} showHat={false} />
|
||||||
|
{
|
||||||
|
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
|
||||||
|
}
|
||||||
|
<div style={{ cursor: 'pointer' }} onClick={addAccount}>+ add account</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ export function AdvPostInitial ({ forward, boost }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdvPostForm ({ children }) {
|
export default function AdvPostForm ({ children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { merge } = useFeeButton()
|
const { merge } = useFeeButton()
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { BALANCE_LIMIT_MSATS } from '../lib/constants'
|
||||||
import { msatsToSats, numWithUnits } from '../lib/format'
|
import { msatsToSats, numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export function WelcomeBanner () {
|
export function WelcomeBanner () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [hidden, setHidden] = useState(true)
|
const [hidden, setHidden] = useState(true)
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function BountyForm ({
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
||||||
const [upsertBounty] = useMutation(
|
const [upsertBounty] = useMutation(
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default function Comment ({
|
||||||
rootText, noComments, noReply, truncate, depth
|
rootText, noComments, noReply, truncate, depth
|
||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
|
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
|
||||||
const [collapse, setCollapse] = useState(
|
const [collapse, setCollapse] = useState(
|
||||||
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function DiscussionForm ({
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
const shareTitle = router.query.title
|
const shareTitle = router.query.title
|
||||||
|
|
|
@ -110,7 +110,7 @@ function FreebieDialog () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { lines, total, disabled: ctxDisabled } = useFeeButton()
|
const { lines, total, disabled: ctxDisabled } = useFeeButton()
|
||||||
// freebies: there's only a base cost and we don't have enough sats
|
// freebies: there's only a base cost and we don't have enough sats
|
||||||
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total
|
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total
|
||||||
|
|
|
@ -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 SwitchAccountList, { useAccounts } from './account'
|
||||||
|
|
||||||
function WalletSummary ({ me }) {
|
function WalletSummary ({ me }) {
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
@ -86,6 +88,8 @@ function NotificationBell () {
|
||||||
|
|
||||||
function NavProfileMenu ({ me, dropNavKey }) {
|
function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
|
const showModal = useShowModal()
|
||||||
|
const { multiAuthSignout } = useAccounts()
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
|
@ -124,8 +128,12 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
|
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountList onClose={onClose} />)}>switch account</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
const status = await multiAuthSignout()
|
||||||
|
// only signout if multiAuth did not find a next available account
|
||||||
|
if (status === 201) return
|
||||||
try {
|
try {
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
@ -151,7 +159,7 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function StackerCorner ({ dropNavKey }) {
|
function StackerCorner ({ dropNavKey }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const walletLimitReached = me.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const walletLimitReached = me.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
|
|
||||||
|
@ -173,6 +181,8 @@ function StackerCorner ({ dropNavKey }) {
|
||||||
function LurkerCorner ({ path }) {
|
function LurkerCorner ({ path }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
|
const { isAnon } = useAccounts()
|
||||||
|
const showModal = useShowModal()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.localStorage.getItem('striked')) {
|
if (!window.localStorage.getItem('striked')) {
|
||||||
|
@ -189,6 +199,23 @@ function LurkerCorner ({ path }) {
|
||||||
query: { callbackUrl: window.location.origin + router.asPath }
|
query: { callbackUrl: window.location.origin + router.asPath }
|
||||||
}), [router])
|
}), [router])
|
||||||
|
|
||||||
|
if (isAnon) {
|
||||||
|
return (
|
||||||
|
<div className='d-flex ms-auto'>
|
||||||
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
|
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
|
||||||
|
<Nav.Link eventKey='anon' as='span' className='p-0'>
|
||||||
|
<AnonIcon className='me-1 fill-muted' width={20} height={20} />@anon
|
||||||
|
</Nav.Link>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountList onClose={onClose} />)}>switch account</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return path !== '/login' && path !== '/signup' && !path.startsWith('/invites') &&
|
return path !== '/login' && path !== '/signup' && !path.startsWith('/invites') &&
|
||||||
<div className='ms-auto'>
|
<div className='ms-auto'>
|
||||||
<Button
|
<Button
|
||||||
|
@ -249,7 +276,7 @@ function NavItems ({ className, sub, prefix }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostItem ({ className, prefix }) {
|
function PostItem ({ className, prefix }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
return (
|
return (
|
||||||
|
@ -275,7 +302,7 @@ export default function Header ({ sub }) {
|
||||||
const prefix = sub ? `/~${sub}` : ''
|
const prefix = sub ? `/~${sub}` : ''
|
||||||
const topNavKey = path.split('/')[sub ? 2 : 1] ?? ''
|
const topNavKey = path.split('/')[sub ? 2 : 1] ?? ''
|
||||||
const dropNavKey = path.split('/').slice(sub ? 2 : 1).join('/')
|
const dropNavKey = path.split('/').slice(sub ? 2 : 1).join('/')
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container as='header' className='px-sm-0'>
|
<Container as='header' className='px-sm-0'>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { abbrNum, numWithUnits } from '../lib/format'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
|
||||||
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
|
|
||||||
// prevent layout shifts when hovering by fixing width to initial rendered width
|
// prevent layout shifts when hovering by fixing width to initial rendered width
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function decodeOriginalUrl (imgproxyUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageOriginal ({ src, topLevel, nofollow, tab, children, onClick, ...props }) {
|
function ImageOriginal ({ src, topLevel, nofollow, tab, children, onClick, ...props }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [showImage, setShowImage] = useState(false)
|
const [showImage, setShowImage] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -159,7 +159,7 @@ const defaultOptions = {
|
||||||
// there's lots of state cascading paired with logic
|
// there's lots of state cascading paired with logic
|
||||||
// independent of the state, and it's hard to follow
|
// independent of the state, and it's hard to follow
|
||||||
export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [createInvoice, { data }] = useMutation(gql`
|
const [createInvoice, { data }] = useMutation(gql`
|
||||||
mutation createInvoice($amount: Int!) {
|
mutation createInvoice($amount: Int!) {
|
||||||
createInvoice(amount: $amount, hodlInvoice: true, expireSecs: 180) {
|
createInvoice(amount: $amount, hodlInvoice: true, expireSecs: 180) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ const addCustomTip = (amount) => {
|
||||||
|
|
||||||
export default function ItemAct ({ onClose, itemId, down, children }) {
|
export default function ItemAct ({ onClose, itemId, down, children }) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { numWithUnits } from '../lib/format'
|
||||||
import { useQuoteReply } from './use-quote-reply'
|
import { useQuoteReply } from './use-quote-reply'
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!item.text) {
|
if (!item.text) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function ItemInfo ({
|
||||||
onQuoteReply, nofollow, extraBadges, nested
|
onQuoteReply, nofollow, extraBadges, nested
|
||||||
}) {
|
}) {
|
||||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] =
|
const [canEdit, setCanEdit] =
|
||||||
useState(item.mine && (Date.now() < editThreshold))
|
useState(item.mine && (Date.now() < editThreshold))
|
||||||
|
|
|
@ -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' />
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ItemButtonBar } from './post'
|
||||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function detectOS () {
|
||||||
export const LoggerContext = createContext()
|
export const LoggerContext = createContext()
|
||||||
|
|
||||||
export function LoggerProvider ({ children }) {
|
export function LoggerProvider ({ children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [name, setName] = useState()
|
const [name, setName] = useState()
|
||||||
const [os, setOS] = useState()
|
const [os, setOS] = useState()
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import NostrAuth from './nostr-auth'
|
||||||
import LoginButton from './login-button'
|
import LoginButton from './login-button'
|
||||||
import { emailSchema } from '../lib/validate'
|
import { emailSchema } from '../lib/validate'
|
||||||
|
|
||||||
export function EmailLoginForm ({ text, callbackUrl }) {
|
export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
|
@ -17,7 +17,7 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
||||||
}}
|
}}
|
||||||
schema={emailSchema}
|
schema={emailSchema}
|
||||||
onSubmit={async ({ email }) => {
|
onSubmit={async ({ email }) => {
|
||||||
signIn('email', { email, callbackUrl })
|
signIn('email', { email, callbackUrl, multiAuth })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -48,16 +48,16 @@ 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') {
|
||||||
return <NostrAuth callbackUrl={callbackUrl} text={text} />
|
return <NostrAuth callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -76,7 +76,7 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
return (
|
return (
|
||||||
<div className='w-100' key={provider.id}>
|
<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} multiAuth={multiAuth} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case 'Lightning':
|
case 'Lightning':
|
||||||
|
@ -103,7 +103,7 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
className={`mt-2 ${styles.providerButton}`}
|
className={`mt-2 ${styles.providerButton}`}
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
type={provider.id.toLowerCase()}
|
type={provider.id.toLowerCase()}
|
||||||
onClick={() => signIn(provider.id, { callbackUrl })}
|
onClick={() => signIn(provider.id, { callbackUrl, multiAuth })}
|
||||||
text={`${text || 'Login'} with`}
|
text={`${text || 'Login'} with`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,10 +8,14 @@ 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' })
|
||||||
|
// this makes sure that we always use the fetched data if it's null.
|
||||||
|
// without this, we would always fallback to the `me` object
|
||||||
|
// which was passed during page load which (visually) breaks switching to anon
|
||||||
|
const futureMe = data?.me ?? (data?.me === null ? null : me)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MeContext.Provider value={data?.me || me}>
|
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
|
||||||
{children}
|
{children}
|
||||||
</MeContext.Provider>
|
</MeContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -64,7 +64,7 @@ function NostrExplainer ({ text }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuth ({ text, callbackUrl }) {
|
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
createAuth {
|
createAuth {
|
||||||
|
@ -112,7 +112,8 @@ export function NostrAuth ({ text, callbackUrl }) {
|
||||||
try {
|
try {
|
||||||
await signIn('nostr', {
|
await signIn('nostr', {
|
||||||
event: JSON.stringify(event),
|
event: JSON.stringify(event),
|
||||||
callbackUrl
|
callbackUrl,
|
||||||
|
multiAuth
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('authorization failed', e)
|
throw new Error('authorization failed', e)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||||
import { useAct } from './item-act'
|
import { useAct } from './item-act'
|
||||||
|
|
||||||
export default function PayBounty ({ children, item }) {
|
export default function PayBounty ({ children, item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { ItemButtonBar } from './post'
|
||||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { POLL_COST } from '../lib/constants'
|
||||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||||
|
|
||||||
export default function Poll ({ item }) {
|
export default function Poll ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [pollVote] = useMutation(
|
const [pollVote] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation pollVote($id: ID!, $hash: String, $hmac: String) {
|
mutation pollVote($id: ID!, $hash: String, $hmac: String) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Delete from './delete'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
|
|
||||||
export function PostForm ({ type, sub, children }) {
|
export function PostForm ({ type, sub, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [errorMessage, setErrorMessage] = useState()
|
const [errorMessage, setErrorMessage] = useState()
|
||||||
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
@ -140,7 +140,7 @@ export function PostForm ({ type, sub, children }) {
|
||||||
return (
|
return (
|
||||||
<FeeButtonProvider
|
<FeeButtonProvider
|
||||||
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, allowFreebies: sub.allowFreebies, me: !!me }) : undefined}
|
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, allowFreebies: sub.allowFreebies, me: !!me }) : undefined}
|
||||||
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
useRemoteLineItems={postCommentUseRemoteLineItems()}
|
||||||
>
|
>
|
||||||
<FormType sub={sub}>{children}</FormType>
|
<FormType sub={sub}>{children}</FormType>
|
||||||
</FeeButtonProvider>
|
</FeeButtonProvider>
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function usePrice () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PriceProvider ({ price, children }) {
|
export function PriceProvider ({ price, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const fiatCurrency = me?.privates?.fiatCurrency
|
const fiatCurrency = me?.privates?.fiatCurrency
|
||||||
const { data } = useQuery(PRICE, {
|
const { data } = useQuery(PRICE, {
|
||||||
variables: { fiatCurrency },
|
variables: { fiatCurrency },
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function ReplyOnAnotherPage ({ item }) {
|
||||||
|
|
||||||
export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, onQuoteReply, onCancelQuote, quote }, ref) {
|
export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, onQuoteReply, onCancelQuote, quote }, ref) {
|
||||||
const [reply, setReply] = useState(replyOpen || quote)
|
const [reply, setReply] = useState(replyOpen || quote)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
@ -159,7 +159,7 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
||||||
<div className={styles.reply}>
|
<div className={styles.reply}>
|
||||||
<FeeButtonProvider
|
<FeeButtonProvider
|
||||||
baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })}
|
baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })}
|
||||||
useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })}
|
useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id })}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
|
|
|
@ -17,7 +17,7 @@ const referrurl = (ipath, me) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Share ({ path, title, className = '' }) {
|
export default function Share ({ path, title, className = '' }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const url = referrurl(path, me)
|
const url = referrurl(path, me)
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export default function Share ({ path, title, className = '' }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CopyLinkDropdownItem ({ item }) {
|
export function CopyLinkDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const url = referrurl(`/items/${item.id}`, me)
|
const url = referrurl(`/items/${item.id}`, me)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { numWithUnits } from '../lib/format'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
show={show}
|
show={show}
|
||||||
|
@ -73,7 +73,7 @@ export default function UpVote ({ item, className }) {
|
||||||
const [voteShow, _setVoteShow] = useState(false)
|
const [voteShow, _setVoteShow] = useState(false)
|
||||||
const [tipShow, _setTipShow] = useState(false)
|
const [tipShow, _setTipShow] = useState(false)
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [setWalkthrough] = useMutation(
|
const [setWalkthrough] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
||||||
|
|
|
@ -154,7 +154,7 @@ function NymEdit ({ user, setEditting }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function NymView ({ user, isMe, setEditting }) {
|
function NymView ({ user, isMe, setEditting }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center mb-2'>
|
<div className='d-flex align-items-center mb-2'>
|
||||||
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
||||||
|
@ -181,7 +181,7 @@ function HeaderNym ({ user, isMe }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeaderHeader ({ user }) {
|
function HeaderHeader ({ user }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,31 @@ function seperate (arr, seperator) {
|
||||||
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
|
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UserListRow = ({ user, stats, className, onNymClick, showHat = true }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.item} mb-2`} key={user.name}>
|
||||||
|
<Link href={`/${user.name}`}>
|
||||||
|
<Image
|
||||||
|
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
||||||
|
className={`${userStyles.userimg} me-2`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<div className={`${styles.hunk} ${className}`}>
|
||||||
|
<Link
|
||||||
|
href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`} onClick={onNymClick}
|
||||||
|
>
|
||||||
|
@{user.name}{showHat && <Hat className='ms-1 fill-grey' height={14} width={14} user={user} />}
|
||||||
|
</Link>
|
||||||
|
{stats && (
|
||||||
|
<div className={styles.other}>
|
||||||
|
{stats.map((Comp, i) => <Comp key={i} user={user} />)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserList ({ ssrData, query, variables, destructureData }) {
|
export default function UserList ({ ssrData, query, variables, destructureData }) {
|
||||||
const { data, fetchMore } = useQuery(query, { variables })
|
const { data, fetchMore } = useQuery(query, { variables })
|
||||||
const dat = useData(data, ssrData)
|
const dat = useData(data, ssrData)
|
||||||
|
@ -62,24 +87,7 @@ export default function UserList ({ ssrData, query, variables, destructureData }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{users?.map(user => (
|
{users?.map(user => <UserListRow key={user.id} user={user} stats={statComps} />)}
|
||||||
<div className={`${styles.item} mb-2`} key={user.name}>
|
|
||||||
<Link href={`/${user.name}`}>
|
|
||||||
<Image
|
|
||||||
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
|
||||||
className={`${userStyles.userimg} me-2`}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<div className={styles.hunk}>
|
|
||||||
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
|
|
||||||
@{user.name}<Hat className='ms-1 fill-grey' height={14} width={14} user={user} />
|
|
||||||
</Link>
|
|
||||||
<div className={styles.other}>
|
|
||||||
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<MoreFooter cursor={cursor} count={users?.length} fetchMore={fetchMore} Skeleton={UsersSkeleton} noMoreText='NO MORE' />
|
<MoreFooter cursor={cursor} count={users?.length} fetchMore={fetchMore} Skeleton={UsersSkeleton} noMoreText='NO MORE' />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const ME = gql`
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
bioId
|
bioId
|
||||||
|
photoId
|
||||||
privates {
|
privates {
|
||||||
autoDropBolt11s
|
autoDropBolt11s
|
||||||
diagnostics
|
diagnostics
|
||||||
|
@ -234,8 +235,8 @@ export const USER_FULL = gql`
|
||||||
|
|
||||||
export const USER = gql`
|
export const USER = gql`
|
||||||
${USER_FIELDS}
|
${USER_FIELDS}
|
||||||
query User($name: String!) {
|
query User($id: ID, $name: String) {
|
||||||
user(name: $name) {
|
user(id: $id, name: $name) {
|
||||||
...UserFields
|
...UserFields
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
|
@ -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,56 @@ 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
|
||||||
|
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const hasCookiePointer = request.cookies?.has(cookiePointerName)
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = request.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const hasSession = request.cookies?.has(sessionCookieName)
|
||||||
|
|
||||||
|
if (!hasCookiePointer || !hasSession) {
|
||||||
|
// no session or no cookie pointer. do nothing.
|
||||||
|
return NextResponse.next({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = request.cookies?.get(cookiePointerName)?.value
|
||||||
|
if (userId === 'anonymous') {
|
||||||
|
// user switched to anon. only delete session cookie.
|
||||||
|
request.cookies.delete(sessionCookieName)
|
||||||
|
return NextResponse.next({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userJWT = request.cookies.get(`multi_auth.${userId}`)?.value
|
||||||
|
if (!userJWT) {
|
||||||
|
// no multi auth JWT found
|
||||||
|
return NextResponse.next({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJWT) {
|
||||||
|
// multi auth JWT found in cookie that pointed to by cookie pointer that is different to current session cookie.
|
||||||
|
request.cookies.set(sessionCookieName, userJWT)
|
||||||
|
return NextResponse.next({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function middleware (request) {
|
||||||
|
const referrerRegexp = /(\/.*)?\/r\/([\w_]+)/
|
||||||
|
if (referrerRegexp.test(request.nextUrl.pathname)) {
|
||||||
|
return referrerMiddleware(request)
|
||||||
|
}
|
||||||
|
return multiAuthMiddleware(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// referrals
|
||||||
|
'/(.*/|)r/([\\w_]+)([?#]?.*)',
|
||||||
|
// account switching
|
||||||
|
'/api/graphql', '/_next/data/(.*)'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default function User ({ ssrData }) {
|
||||||
const [create, setCreate] = useState(false)
|
const [create, setCreate] = useState(false)
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
|
@ -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/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,12 +7,14 @@ 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, res) {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
|
@ -36,6 +38,16 @@ function getCallbacks (req) {
|
||||||
token.sub = Number(token.id)
|
token.sub = Number(token.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// response is only defined during signup/login
|
||||||
|
if (req && res) {
|
||||||
|
req = new NodeNextRequest(req)
|
||||||
|
res = new NodeNextResponse(res)
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const jwt = await encodeJWT({ token, secret })
|
||||||
|
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
||||||
|
setMultiAuthCookies(req, res, { ...me, jwt })
|
||||||
|
}
|
||||||
|
|
||||||
if (isNewUser) {
|
if (isNewUser) {
|
||||||
// if referrer exists, set on user
|
// if referrer exists, set on user
|
||||||
if (req.cookies.sn_referrer && user?.id) {
|
if (req.cookies.sn_referrer && user?.id) {
|
||||||
|
@ -77,8 +89,34 @@ function getCallbacks (req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
|
||||||
const { k1, pubkey } = credentials
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
||||||
|
// default expiration for next-auth JWTs is in 1 month
|
||||||
|
const expiresAt = datePivot(new Date(), { months: 1 })
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: req.secure,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: expiresAt
|
||||||
|
}
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
|
||||||
|
// don't overwrite multi auth cookie, only add
|
||||||
|
let newMultiAuth = [{ id, name, photoId }]
|
||||||
|
if (req.cookies.multi_auth) {
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
// only add if multi auth does not exist yet
|
||||||
|
if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return
|
||||||
|
newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
|
||||||
|
}
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
|
||||||
|
const { k1, pubkey, multiAuth: multiAuthParam } = credentials
|
||||||
|
// multiAuth query param is a string
|
||||||
|
const multiAuth = typeof multiAuthParam === 'string' ? multiAuthParam === 'true' : !!multiAuthParam
|
||||||
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 } })
|
||||||
|
@ -87,12 +125,21 @@ async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
||||||
const token = await getToken({ req })
|
const token = await getToken({ req })
|
||||||
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) {
|
// never update account if multi auth is used, only create
|
||||||
|
if (token?.id && !multiAuth) {
|
||||||
|
// TODO: consider multi auth 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) {
|
||||||
|
// don't switch accounts, we only want to add. switching is done in client via "pointer cookie"
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const userJWT = await encodeJWT({ token: { id: user.id, name: user.name, email: user.email }, secret })
|
||||||
|
setMultiAuthCookies(req, res, { ...user, jwt: userJWT })
|
||||||
|
return token
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +183,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 +191,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 +203,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 +237,9 @@ const providers = [
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
export const getAuthOptions = req => ({
|
export const getAuthOptions = (req, res) => ({
|
||||||
callbacks: getCallbacks(req),
|
callbacks: getCallbacks(req, res),
|
||||||
providers,
|
providers: getProviders(res),
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt'
|
strategy: 'jwt'
|
||||||
|
@ -203,7 +252,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 ({
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {NextApiRequest} req
|
||||||
|
* @param {NextApiResponse} res
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
export default (req, res) => {
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const userId = req.cookies[cookiePointerName]
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = req.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const sessionJWT = req.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!userId || !sessionJWT) {
|
||||||
|
// no cookie pointer or no session cookie present. do nothing.
|
||||||
|
res.status(404).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = []
|
||||||
|
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
secure: req.secure,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: datePivot(new Date(), { months: 1 })
|
||||||
|
}
|
||||||
|
// remove JWT pointed to by cookie pointer
|
||||||
|
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||||
|
|
||||||
|
// update multi_auth cookie and check if there are more accounts available
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
||||||
|
if (newMultiAuth.length === 0) {
|
||||||
|
// no next account available. cleanup: remove multi_auth + pointer cookie
|
||||||
|
cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
res.setHeader('Set-Cookie', cookies)
|
||||||
|
res.status(204).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
const newUserId = newMultiAuth[0].id
|
||||||
|
const newUserJWT = req.cookies[`multi_auth.${newUserId}`]
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
...cookies,
|
||||||
|
cookie.serialize(cookiePointerName, newUserId, { ...cookieOptions, httpOnly: false }),
|
||||||
|
cookie.serialize(sessionCookieName, newUserJWT, cookieOptions)
|
||||||
|
])
|
||||||
|
|
||||||
|
res.status(201).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
|
@ -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 case of auth linking we want to pass the error back to settings
|
||||||
// settings
|
// in the case of multi auth, 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)
|
||||||
|
@ -39,11 +39,23 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providers = await getProviders()
|
||||||
|
if (multiAuth) {
|
||||||
|
// multi auth only supported for login with lightning and nostr
|
||||||
|
const multiAuthSupport = key => ['lightning', 'nostr'].includes(key)
|
||||||
|
Object.keys(providers).forEach(key => {
|
||||||
|
if (!multiAuthSupport(key)) {
|
||||||
|
delete providers[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
providers: await getProviders(),
|
providers,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
error
|
error,
|
||||||
|
multiAuth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const getServerSideProps = getGetServerSideProps({ query: REFERRALS, auth
|
||||||
|
|
||||||
export default function Referrals ({ ssrData }) {
|
export default function Referrals ({ ssrData }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const select = async values => {
|
const select = async values => {
|
||||||
const { when, ...query } = values
|
const { when, ...query } = values
|
||||||
|
|
|
@ -35,7 +35,7 @@ function bech32encode (hexString) {
|
||||||
|
|
||||||
export default function Settings ({ ssrData }) {
|
export default function Settings ({ ssrData }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [setSettings] = useMutation(SET_SETTINGS, {
|
const [setSettings] = useMutation(SET_SETTINGS, {
|
||||||
update (cache, { data: { setSettings } }) {
|
update (cache, { data: { setSettings } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
|
@ -55,6 +55,13 @@ export default function Settings ({ ssrData }) {
|
||||||
const { settings: { privates: settings } } = data || ssrData
|
const { settings: { privates: settings } } = data || ssrData
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
|
||||||
|
// if we switched to anon, me is no longer defined
|
||||||
|
const router = useRouter()
|
||||||
|
if (!me) {
|
||||||
|
router.push('/login')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenterLayout>
|
<CenterLayout>
|
||||||
<div className='py-3 w-100'>
|
<div className='py-3 w-100'>
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default function Wallet () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function YouHaveSats () {
|
function YouHaveSats () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
return (
|
return (
|
||||||
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
||||||
|
@ -97,7 +97,7 @@ export function WalletForm () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FundForm () {
|
export function FundForm () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [showAlert, setShowAlert] = useState(true)
|
const [showAlert, setShowAlert] = useState(true)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [createInvoice, { called, error }] = useMutation(gql`
|
const [createInvoice, { called, error }] = useMutation(gql`
|
||||||
|
@ -200,7 +200,7 @@ export function SelectedWithdrawalForm () {
|
||||||
|
|
||||||
export function InvWithdrawal () {
|
export function InvWithdrawal () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ export function LnWithdrawal () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LnAddrWithdrawal () {
|
export function LnAddrWithdrawal () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
||||||
const defaultOptions = { min: 1 }
|
const defaultOptions = { min: 1 }
|
||||||
|
|
|
@ -114,7 +114,7 @@ function LoadWithdrawl () {
|
||||||
function PrivacyOption ({ wd }) {
|
function PrivacyOption ({ wd }) {
|
||||||
if (!wd.bolt11) return
|
if (!wd.bolt11) return
|
||||||
|
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
||||||
const oldEnough = new Date() >= keepUntil
|
const oldEnough = new Date() >= keepUntil
|
||||||
if (!oldEnough) {
|
if (!oldEnough) {
|
||||||
|
|
Loading…
Reference in New Issue