Compare commits
No commits in common. "051cb69f5eb531742221ab4b49ec860c8d402a57" and "cdeaa35ff4c4fd5cd9dc5e7609d7c85d82d78bc7" have entirely different histories.
051cb69f5e
...
cdeaa35ff4
@ -36,10 +36,6 @@ LNWITH_URL=
|
|||||||
LOGIN_EMAIL_SERVER=smtp://mailhog:1025
|
LOGIN_EMAIL_SERVER=smtp://mailhog:1025
|
||||||
LOGIN_EMAIL_FROM=sndev@mailhog.dev
|
LOGIN_EMAIL_FROM=sndev@mailhog.dev
|
||||||
|
|
||||||
# email salt
|
|
||||||
# openssl rand -hex 32
|
|
||||||
EMAIL_SALT=202c90943c313b829e65e3f29164fb5dd7ea3370d7262c4159691c2f6493bb8b
|
|
||||||
|
|
||||||
# static things
|
# static things
|
||||||
NEXTAUTH_URL=http://localhost:3000/api/auth
|
NEXTAUTH_URL=http://localhost:3000/api/auth
|
||||||
SELF_URL=http://app:3000
|
SELF_URL=http://app:3000
|
||||||
|
@ -9,7 +9,6 @@ import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS }
|
|||||||
import { viewGroup } from './growth'
|
import { viewGroup } from './growth'
|
||||||
import { timeUnitForRange, whenRange } from '@/lib/time'
|
import { timeUnitForRange, whenRange } from '@/lib/time'
|
||||||
import assertApiKeyNotPermitted from './apiKey'
|
import assertApiKeyNotPermitted from './apiKey'
|
||||||
import { hashEmail } from '@/lib/crypto'
|
|
||||||
|
|
||||||
const contributors = new Set()
|
const contributors = new Set()
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ async function authMethods (user, args, { models, me }) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
lightning: !!user.pubkey,
|
lightning: !!user.pubkey,
|
||||||
email: !!(user.emailVerified && user.emailHash),
|
email: user.emailVerified && user.email,
|
||||||
twitter: oauth.indexOf('twitter') >= 0,
|
twitter: oauth.indexOf('twitter') >= 0,
|
||||||
github: oauth.indexOf('github') >= 0,
|
github: oauth.indexOf('github') >= 0,
|
||||||
nostr: !!user.nostrAuthPubkey,
|
nostr: !!user.nostrAuthPubkey,
|
||||||
@ -687,7 +686,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await models.user.update({
|
await models.user.update({
|
||||||
where: { id: me.id },
|
where: { id: me.id },
|
||||||
data: { emailHash: hashEmail({ email }) }
|
data: { email: email.toLowerCase() }
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'P2002') {
|
if (error.code === 'P2002') {
|
||||||
|
@ -108,7 +108,7 @@ export default gql`
|
|||||||
nostr: Boolean!
|
nostr: Boolean!
|
||||||
github: Boolean!
|
github: Boolean!
|
||||||
twitter: Boolean!
|
twitter: Boolean!
|
||||||
email: Boolean!
|
email: String
|
||||||
apiKey: Boolean
|
apiKey: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
awards.csv
14
awards.csv
@ -66,17 +66,3 @@ benalleng,pr,#1099,#794,medium-hard,,,refined in a commit,450k,benalleng@mutiny.
|
|||||||
dillon-co,helpfulness,#1099,#794,medium-hard,,,#988 did much of the legwork,225k,bolt11,2024-04-29
|
dillon-co,helpfulness,#1099,#794,medium-hard,,,#988 did much of the legwork,225k,bolt11,2024-04-29
|
||||||
abhiShandy,pr,#1119,#1110,good-first-issue,,,,20k,abhishandy@stacker.news,2024-04-28
|
abhiShandy,pr,#1119,#1110,good-first-issue,,,,20k,abhishandy@stacker.news,2024-04-28
|
||||||
felipebueno,issue,#1119,#1110,good-first-issue,,,,2k,felipe@stacker.news,2024-04-28
|
felipebueno,issue,#1119,#1110,good-first-issue,,,,2k,felipe@stacker.news,2024-04-28
|
||||||
SatsAllDay,pr,#1111,#622,medium-hard,,,,500k,weareallsatoshi@getalby.com,2024-05-04
|
|
||||||
itsrealfake,pr,#1130,#622,good-first-issue,,,,20k,itsrealfake2@stacker.news,2024-05-06
|
|
||||||
Darth-Coin,issue,#1130,#622,easy,,,,2k,darthcoin@stacker.news,2024-05-04
|
|
||||||
benalleng,pr,#1137,#1125,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-05-04
|
|
||||||
SatsAllDay,issue,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-04
|
|
||||||
SatsAllDay,helpfulness,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-04
|
|
||||||
itsrealfake,pr,#1138,#995,good-first-issue,,,,20k,itsrealfake2@stacker.news,2024-05-06
|
|
||||||
SouthKoreaLN,issue,#1138,#995,good-first-issue,,,,2k,south_korea_ln@stacker.news,2024-05-04
|
|
||||||
mateusdeap,helpfulness,#1138,#995,good-first-issue,,,,1k,???,???
|
|
||||||
felipebueno,pr,#1094,,,,2,,80k,felipebueno@getalby.com,2024-05-06
|
|
||||||
benalleng,helpfulness,#1127,#927,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-05-04
|
|
||||||
itsrealfake,pr,#1135,#1016,good-first-issue,,,nonideal solution,10k,itsrealfake2@stacker.news,2024-05-06
|
|
||||||
SatsAllDay,issue,#1135,#1016,good-first-issue,,,,1k,weareallsatoshi@getalby.com,2024-05-04
|
|
||||||
s373nZ,issue,#1136,#1107,medium,high,,,50k,se7enz@minibits.cash,2024-05-05
|
|
|
@ -65,7 +65,7 @@ function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }
|
|||||||
|
|
||||||
function ImageProxy ({ src, srcSet: { dimensions, ...srcSetObj } = {}, onClick, topLevel, onError, ...props }) {
|
function ImageProxy ({ src, srcSet: { dimensions, ...srcSetObj } = {}, onClick, topLevel, onError, ...props }) {
|
||||||
const srcSet = useMemo(() => {
|
const srcSet = useMemo(() => {
|
||||||
if (Object.keys(srcSetObj).length === 0) return undefined
|
if (!srcSetObj) return undefined
|
||||||
// srcSetObj shape: { [widthDescriptor]: <imgproxyUrl>, ... }
|
// srcSetObj shape: { [widthDescriptor]: <imgproxyUrl>, ... }
|
||||||
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url], i, arr) => {
|
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url], i, arr) => {
|
||||||
// backwards compatibility: we used to replace image urls with imgproxy urls rather just storing paths
|
// backwards compatibility: we used to replace image urls with imgproxy urls rather just storing paths
|
||||||
@ -79,7 +79,7 @@ function ImageProxy ({ src, srcSet: { dimensions, ...srcSetObj } = {}, onClick,
|
|||||||
|
|
||||||
// get source url in best resolution
|
// get source url in best resolution
|
||||||
const bestResSrc = useMemo(() => {
|
const bestResSrc = useMemo(() => {
|
||||||
if (Object.keys(srcSetObj).length === 0) return src
|
if (!srcSetObj) return src
|
||||||
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url]) => {
|
return Object.entries(srcSetObj).reduce((acc, [wDescriptor, url]) => {
|
||||||
if (!url.startsWith('http')) {
|
if (!url.startsWith('http')) {
|
||||||
url = new URL(url, process.env.NEXT_PUBLIC_IMGPROXY_URL).toString()
|
url = new URL(url, process.env.NEXT_PUBLIC_IMGPROXY_URL).toString()
|
||||||
@ -107,7 +107,7 @@ function ImageProxy ({ src, srcSet: { dimensions, ...srcSetObj } = {}, onClick,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Image = memo(({ className, src, srcSet, sizes, width, height, onClick, onError }) => {
|
const Image = memo(({ className, src, srcSet, sizes, width, height, bestResSrc, onClick, onError }) => {
|
||||||
const style = width && height
|
const style = width && height
|
||||||
? { '--height': `${height}px`, '--width': `${width}px`, '--aspect-ratio': `${width} / ${height}` }
|
? { '--height': `${height}px`, '--width': `${width}px`, '--aspect-ratio': `${width} / ${height}` }
|
||||||
: undefined
|
: undefined
|
||||||
@ -116,7 +116,7 @@ const Image = memo(({ className, src, srcSet, sizes, width, height, onClick, onE
|
|||||||
<img
|
<img
|
||||||
className={className}
|
className={className}
|
||||||
// browsers that don't support srcSet and sizes will use src. use best resolution possible in that case
|
// browsers that don't support srcSet and sizes will use src. use best resolution possible in that case
|
||||||
src={src}
|
src={bestResSrc}
|
||||||
srcSet={srcSet}
|
srcSet={srcSet}
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
width={width}
|
width={width}
|
||||||
|
@ -21,7 +21,6 @@ import MuteDropdownItem from './mute'
|
|||||||
import { DropdownItemUpVote } from './upvote'
|
import { DropdownItemUpVote } from './upvote'
|
||||||
import { useRoot } from './root'
|
import { useRoot } from './root'
|
||||||
import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
|
import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
|
||||||
import UserPopover from './user-popover'
|
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
@ -102,12 +101,10 @@ export default function ItemInfo ({
|
|||||||
</Link>
|
</Link>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<span>
|
<span>
|
||||||
<UserPopover name={item.user.name}>
|
<Link href={`/${item.user.name}`}>
|
||||||
<Link href={`/${item.user.name}`}>
|
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
||||||
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
{embellishUser}
|
||||||
{embellishUser}
|
</Link>
|
||||||
</Link>
|
|
||||||
</UserPopover>
|
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
||||||
{timeSince(new Date(item.createdAt))}
|
{timeSince(new Date(item.createdAt))}
|
||||||
|
@ -126,14 +126,14 @@ export function useServiceWorkerLogger () {
|
|||||||
const WalletLoggerContext = createContext()
|
const WalletLoggerContext = createContext()
|
||||||
const WalletLogsContext = createContext()
|
const WalletLogsContext = createContext()
|
||||||
|
|
||||||
const initIndexedDB = async (dbName, storeName) => {
|
const initIndexedDB = async (storeName) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!window.indexedDB) {
|
if (!window.indexedDB) {
|
||||||
return reject(new Error('IndexedDB not supported'))
|
return reject(new Error('IndexedDB not supported'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
|
||||||
const request = window.indexedDB.open(dbName, 1)
|
const request = window.indexedDB.open('app:storage', 1)
|
||||||
|
|
||||||
let db
|
let db
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
@ -159,12 +159,7 @@ const initIndexedDB = async (dbName, storeName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WalletLoggerProvider = ({ children }) => {
|
const WalletLoggerProvider = ({ children }) => {
|
||||||
const me = useMe()
|
|
||||||
const [logs, setLogs] = useState([])
|
const [logs, setLogs] = useState([])
|
||||||
let dbName = 'app:storage'
|
|
||||||
if (me) {
|
|
||||||
dbName = `${dbName}:${me.id}`
|
|
||||||
}
|
|
||||||
const idbStoreName = 'wallet_logs'
|
const idbStoreName = 'wallet_logs'
|
||||||
const idb = useRef()
|
const idb = useRef()
|
||||||
const logQueue = useRef([])
|
const logQueue = useRef([])
|
||||||
@ -216,7 +211,7 @@ const WalletLoggerProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initIndexedDB(dbName, idbStoreName)
|
initIndexedDB(idbStoreName)
|
||||||
.then(db => {
|
.then(db => {
|
||||||
idb.current = db
|
idb.current = db
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ import classNames from 'classnames'
|
|||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { useWebLNConfigurator } from '../webln'
|
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
@ -163,6 +162,8 @@ export function NavWalletSummary ({ className }) {
|
|||||||
|
|
||||||
export function MeDropdown ({ me, dropNavKey }) {
|
export function MeDropdown ({ me, dropNavKey }) {
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
|
const { deleteLogs } = useWalletLogger()
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
@ -201,7 +202,22 @@ export function MeDropdown ({ me, dropNavKey }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<LogoutDropdownItem />
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
if (pushSubscription) {
|
||||||
|
// don't prevent signout because of an unsubscription error
|
||||||
|
await togglePushSubscription().catch(console.error)
|
||||||
|
}
|
||||||
|
// delete client wallet logs to prevent leak of private data if a shared device was used
|
||||||
|
await deleteLogs(Wallet.NWC).catch(console.error)
|
||||||
|
await deleteLogs(Wallet.LNbits).catch(console.error)
|
||||||
|
await deleteLogs(Wallet.LNC).catch(console.error)
|
||||||
|
await signOut({ callbackUrl: '/' })
|
||||||
|
}}
|
||||||
|
>logout
|
||||||
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{!me.bioId &&
|
{!me.bioId &&
|
||||||
@ -255,31 +271,6 @@ export default function LoginButton ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogoutDropdownItem () {
|
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
|
||||||
const webLN = useWebLNConfigurator()
|
|
||||||
const { deleteLogs } = useWalletLogger()
|
|
||||||
return (
|
|
||||||
<Dropdown.Item
|
|
||||||
onClick={async () => {
|
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
|
||||||
if (pushSubscription) {
|
|
||||||
await togglePushSubscription().catch(console.error)
|
|
||||||
}
|
|
||||||
// detach wallets
|
|
||||||
await webLN.clearConfig().catch(console.error)
|
|
||||||
// delete client wallet logs to prevent leak of private data if a shared device was used
|
|
||||||
await deleteLogs(Wallet.NWC).catch(console.error)
|
|
||||||
await deleteLogs(Wallet.LNbits).catch(console.error)
|
|
||||||
await deleteLogs(Wallet.LNC).catch(console.error)
|
|
||||||
await signOut({ callbackUrl: '/' })
|
|
||||||
}}
|
|
||||||
>logout
|
|
||||||
</Dropdown.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoginButtons () {
|
export function LoginButtons () {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -52,11 +52,11 @@ export default function BottomBar ({ sub }) {
|
|||||||
<div className={classNames(styles.footer, styles.footerPadding)}>
|
<div className={classNames(styles.footer, styles.footerPadding)}>
|
||||||
<Navbar className='container px-0'>
|
<Navbar className='container px-0'>
|
||||||
<Nav className={styles.footerNav}>
|
<Nav className={styles.footerNav}>
|
||||||
<Brand />
|
|
||||||
<SearchItem {...props} />
|
|
||||||
<PostItem {...props} className='btn-sm' />
|
|
||||||
<NavNotifications />
|
|
||||||
<Offcanvas me={me} {...props} />
|
<Offcanvas me={me} {...props} />
|
||||||
|
<SearchItem {...props} />
|
||||||
|
<Brand />
|
||||||
|
<NavNotifications />
|
||||||
|
<PostItem {...props} className='btn-sm' />
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,9 @@ import { useState } from 'react'
|
|||||||
import { Dropdown, Image, Nav, Navbar, Offcanvas } from 'react-bootstrap'
|
import { Dropdown, Image, Nav, Navbar, Offcanvas } from 'react-bootstrap'
|
||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { LoginButtons, LogoutDropdownItem, NavWalletSummary } from '../common'
|
import { useServiceWorker } from '@/components/serviceworker'
|
||||||
|
import { signOut } from 'next-auth/react'
|
||||||
|
import { LoginButtons, NavWalletSummary } from '../common'
|
||||||
import AnonIcon from '@/svgs/spy-fill.svg'
|
import AnonIcon from '@/svgs/spy-fill.svg'
|
||||||
import styles from './footer.module.css'
|
import styles from './footer.module.css'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
@ -12,6 +14,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
|
|
||||||
const handleClose = () => setShow(false)
|
const handleClose = () => setShow(false)
|
||||||
const handleShow = () => setShow(true)
|
const handleShow = () => setShow(true)
|
||||||
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
|
|
||||||
const MeImage = ({ onClick }) => me
|
const MeImage = ({ onClick }) => me
|
||||||
? (
|
? (
|
||||||
@ -28,7 +31,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
<>
|
<>
|
||||||
<MeImage onClick={handleShow} />
|
<MeImage onClick={handleShow} />
|
||||||
|
|
||||||
<Offcanvas style={{ maxWidth: '250px', zIndex: '10000' }} show={show} onHide={handleClose} placement='end'>
|
<Offcanvas style={{ maxWidth: '250px', zIndex: '10000' }} show={show} onHide={handleClose} placement='start'>
|
||||||
<Offcanvas.Header closeButton>
|
<Offcanvas.Header closeButton>
|
||||||
<Offcanvas.Title><NavWalletSummary /></Offcanvas.Title>
|
<Offcanvas.Title><NavWalletSummary /></Offcanvas.Title>
|
||||||
</Offcanvas.Header>
|
</Offcanvas.Header>
|
||||||
@ -75,7 +78,22 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<LogoutDropdownItem />
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
if (pushSubscription) {
|
||||||
|
await togglePushSubscription()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// don't prevent signout because of an unsubscription error
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
await signOut({ callbackUrl: '/' })
|
||||||
|
}}
|
||||||
|
>logout
|
||||||
|
</Dropdown.Item>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: <LoginButtons />}
|
: <LoginButtons />}
|
||||||
|
@ -21,7 +21,6 @@ import { useRouter } from 'next/router'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
import UserPopover from './user-popover'
|
|
||||||
|
|
||||||
export function SearchText ({ text }) {
|
export function SearchText ({ text }) {
|
||||||
return (
|
return (
|
||||||
@ -197,18 +196,7 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (text.startsWith?.('@')) {
|
if (href.startsWith('/') || url?.origin === internalURL) {
|
||||||
return (
|
|
||||||
<UserPopover name={text.replace('@', '')}>
|
|
||||||
<Link
|
|
||||||
id={props.id}
|
|
||||||
href={href}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Link>
|
|
||||||
</UserPopover>
|
|
||||||
)
|
|
||||||
} else if (href.startsWith('/') || url?.origin === internalURL) {
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
id={props.id}
|
id={props.id}
|
||||||
|
@ -11,7 +11,6 @@ import Hat from './hat'
|
|||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import { NymActionDropdown } from '@/components/user-header'
|
import { NymActionDropdown } from '@/components/user-header'
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
// all of this nonsense is to show the stat we are sorting by first
|
// all of this nonsense is to show the stat we are sorting by first
|
||||||
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
||||||
@ -40,29 +39,7 @@ 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 function UserBase ({ user, className, children, nymActionDropdown }) {
|
function User ({ user, rank, statComps, Embellish, nymActionDropdown = false }) {
|
||||||
return (
|
|
||||||
<div className={classNames(styles.item, className)}>
|
|
||||||
<Link href={`/${user.name}`}>
|
|
||||||
<Image
|
|
||||||
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
|
||||||
className={`${userStyles.userimg} me-2`}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<div className={styles.hunk}>
|
|
||||||
<div className='d-flex'>
|
|
||||||
<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>
|
|
||||||
{nymActionDropdown && <NymActionDropdown user={user} className='' />}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
|
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const showStatComps = statComps && statComps.length > 0
|
const showStatComps = statComps && statComps.length > 0
|
||||||
return (
|
return (
|
||||||
@ -73,13 +50,27 @@ export function User ({ user, rank, statComps, className = 'mb-2', Embellish, ny
|
|||||||
{rank}
|
{rank}
|
||||||
</div>)
|
</div>)
|
||||||
: <div />}
|
: <div />}
|
||||||
<UserBase user={user} nymActionDropdown={nymActionDropdown} className={(me?.id === user.id && me.privates?.hideFromTopUsers) ? userStyles.hidden : 'mb-2'}>
|
<div className={`${styles.item} ${me?.id === user.id && me.privates?.hideFromTopUsers ? userStyles.hidden : 'mb-2'}`}>
|
||||||
{showStatComps &&
|
<Link href={`/${user.name}`}>
|
||||||
<div className={styles.other}>
|
<Image
|
||||||
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
||||||
</div>}
|
className={`${userStyles.userimg} me-2`}
|
||||||
{Embellish && <Embellish rank={rank} />}
|
/>
|
||||||
</UserBase>
|
</Link>
|
||||||
|
<div className={`${styles.hunk} ${!showStatComps && 'd-flex flex-column justify-content-around'}`}>
|
||||||
|
<div className='d-flex'>
|
||||||
|
<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>
|
||||||
|
{nymActionDropdown && <NymActionDropdown user={user} className='' />}
|
||||||
|
</div>
|
||||||
|
{showStatComps &&
|
||||||
|
<div className={styles.other}>
|
||||||
|
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
||||||
|
</div>}
|
||||||
|
{Embellish && <Embellish rank={rank} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -161,31 +152,23 @@ export function UsersSkeleton () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>{users.map((_, i) => (
|
<div>{users.map((_, i) => (
|
||||||
<UserSkeleton key={i} className='mb-2'>
|
<div className={`${styles.item} ${styles.skeleton} mb-2`} key={i}>
|
||||||
<div className={styles.other}>
|
<Image
|
||||||
<span className={`${styles.otherItem} clouds`} />
|
src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/clouds.jpeg`}
|
||||||
<span className={`${styles.otherItem} clouds`} />
|
width='32' height='32'
|
||||||
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
className={`${userStyles.userimg} clouds me-2`}
|
||||||
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
/>
|
||||||
|
<div className={styles.hunk}>
|
||||||
|
<div className={`${styles.name} clouds text-reset`} />
|
||||||
|
<div className={styles.other}>
|
||||||
|
<span className={`${styles.otherItem} clouds`} />
|
||||||
|
<span className={`${styles.otherItem} clouds`} />
|
||||||
|
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
||||||
|
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UserSkeleton>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserSkeleton ({ children, className }) {
|
|
||||||
return (
|
|
||||||
<div className={`${styles.item} ${styles.skeleton} ${className}`}>
|
|
||||||
<Image
|
|
||||||
src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/clouds.jpeg`}
|
|
||||||
width='32' height='32'
|
|
||||||
className={`${userStyles.userimg} clouds me-2`}
|
|
||||||
/>
|
|
||||||
<div className={styles.hunk}>
|
|
||||||
<div className={`${styles.name} clouds text-reset`} />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
import { USER } from '@/fragments/users'
|
|
||||||
import errorStyles from '@/styles/error.module.css'
|
|
||||||
import { useLazyQuery } from '@apollo/client'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRef, useState } from 'react'
|
|
||||||
import { Popover } from 'react-bootstrap'
|
|
||||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
|
||||||
import { UserBase, UserSkeleton } from './user-list'
|
|
||||||
import styles from './user-popover.module.css'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
function StackingSince ({ since }) {
|
|
||||||
return (
|
|
||||||
<small className='text-muted d-flex-inline'>
|
|
||||||
stacking since:{' '}
|
|
||||||
{since
|
|
||||||
? <Link href={`/items/${since}`}>#{since}</Link>
|
|
||||||
: <span>never</span>}
|
|
||||||
</small>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserPopover ({ name, children }) {
|
|
||||||
const [showOverlay, setShowOverlay] = useState(false)
|
|
||||||
|
|
||||||
const [getUser, { loading, data }] = useLazyQuery(
|
|
||||||
USER,
|
|
||||||
{
|
|
||||||
variables: { name },
|
|
||||||
fetchPolicy: 'cache-first'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const timeoutId = useRef(null)
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
clearTimeout(timeoutId.current)
|
|
||||||
getUser()
|
|
||||||
timeoutId.current = setTimeout(() => {
|
|
||||||
setShowOverlay(true)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
clearTimeout(timeoutId.current)
|
|
||||||
timeoutId.current = setTimeout(() => setShowOverlay(false), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayTrigger
|
|
||||||
show={showOverlay}
|
|
||||||
placement='bottom'
|
|
||||||
onHide={handleMouseLeave}
|
|
||||||
overlay={
|
|
||||||
<Popover
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
className={styles.userPopover}
|
|
||||||
>
|
|
||||||
<Popover.Body className={styles.userPopBody}>
|
|
||||||
{!data || loading
|
|
||||||
? <UserSkeleton />
|
|
||||||
: !data.user
|
|
||||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
|
|
||||||
: (
|
|
||||||
<UserBase user={data.user} className='mb-0 pb-0'>
|
|
||||||
<StackingSince since={data.user.since} />
|
|
||||||
</UserBase>
|
|
||||||
)}
|
|
||||||
</Popover.Body>
|
|
||||||
</Popover>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</OverlayTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
.userPopover {
|
|
||||||
border: 1px solid var(--theme-toolbarActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
.userPopBody {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
@ -33,15 +33,6 @@ export const Status = {
|
|||||||
Error: 'Error'
|
Error: 'Error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migrateLocalStorage (oldStorageKey, newStorageKey) {
|
|
||||||
const item = window.localStorage.getItem(oldStorageKey)
|
|
||||||
if (item) {
|
|
||||||
window.localStorage.setItem(newStorageKey, item)
|
|
||||||
window.localStorage.removeItem(oldStorageKey)
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
function RawWebLNProvider ({ children }) {
|
function RawWebLNProvider ({ children }) {
|
||||||
const lnbits = useLNbits()
|
const lnbits = useLNbits()
|
||||||
const nwc = useNWC()
|
const nwc = useNWC()
|
||||||
@ -123,14 +114,8 @@ function RawWebLNProvider ({ children }) {
|
|||||||
})
|
})
|
||||||
}, [setEnabledProviders])
|
}, [setEnabledProviders])
|
||||||
|
|
||||||
const clearConfig = useCallback(async () => {
|
|
||||||
lnbits.clearConfig()
|
|
||||||
nwc.clearConfig()
|
|
||||||
await lnc.clearConfig()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebLNContext.Provider value={{ provider: isEnabled(provider) ? { sendPayment: sendPaymentWithToast } : null, enabledProviders, setProvider, clearConfig }}>
|
<WebLNContext.Provider value={{ provider: isEnabled(provider) ? { sendPayment: sendPaymentWithToast } : null, enabledProviders, setProvider }}>
|
||||||
{children}
|
{children}
|
||||||
</WebLNContext.Provider>
|
</WebLNContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { Status, migrateLocalStorage } from '.'
|
import { Status } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
import { useMe } from '../me'
|
|
||||||
|
|
||||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
||||||
|
|
||||||
@ -66,17 +65,13 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LNbitsProvider ({ children }) {
|
export function LNbitsProvider ({ children }) {
|
||||||
const me = useMe()
|
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
const [adminKey, setAdminKey] = useState('')
|
const [adminKey, setAdminKey] = useState('')
|
||||||
const [status, setStatus] = useState()
|
const [status, setStatus] = useState()
|
||||||
const { logger } = useWalletLogger(Wallet.LNbits)
|
const { logger } = useWalletLogger(Wallet.LNbits)
|
||||||
|
|
||||||
const name = 'LNbits'
|
const name = 'LNbits'
|
||||||
let storageKey = 'webln:provider:lnbits'
|
const storageKey = 'webln:provider:lnbits'
|
||||||
if (me) {
|
|
||||||
storageKey = `${storageKey}:${me.id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInfo = useCallback(async () => {
|
const getInfo = useCallback(async () => {
|
||||||
const response = await getWallet(url, adminKey)
|
const response = await getWallet(url, adminKey)
|
||||||
@ -115,18 +110,11 @@ export function LNbitsProvider ({ children }) {
|
|||||||
}, [logger, url, adminKey])
|
}, [logger, url, adminKey])
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
let configStr = window.localStorage.getItem(storageKey)
|
const configStr = window.localStorage.getItem(storageKey)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
if (!configStr) {
|
if (!configStr) {
|
||||||
if (me) {
|
logger.info('no existing config found')
|
||||||
// backwards compatibility: try old storageKey
|
return
|
||||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
|
||||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
|
||||||
}
|
|
||||||
if (!configStr) {
|
|
||||||
logger.info('no existing config found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
const config = JSON.parse(configStr)
|
||||||
@ -153,7 +141,7 @@ export function LNbitsProvider ({ children }) {
|
|||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [me, logger])
|
}, [logger])
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
const saveConfig = useCallback(async (config) => {
|
||||||
// immediately store config so it's not lost even if config is invalid
|
// immediately store config so it's not lost even if config is invalid
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import LNC from '@lightninglabs/lnc-web'
|
import LNC from '@lightninglabs/lnc-web'
|
||||||
import { Status, migrateLocalStorage } from '.'
|
import { Status } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import useModal from '../modal'
|
import useModal from '../modal'
|
||||||
import { Form, PasswordInput, SubmitButton } from '../form'
|
import { Form, PasswordInput, SubmitButton } from '../form'
|
||||||
import CancelButton from '../cancel-button'
|
import CancelButton from '../cancel-button'
|
||||||
import { Mutex } from 'async-mutex'
|
import { Mutex } from 'async-mutex'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
import { useMe } from '../me'
|
|
||||||
|
|
||||||
const LNCContext = createContext()
|
const LNCContext = createContext()
|
||||||
const mutex = new Mutex()
|
const mutex = new Mutex()
|
||||||
|
|
||||||
async function getLNC ({ me }) {
|
async function getLNC () {
|
||||||
if (window.lnc) return window.lnc
|
if (window.lnc) return window.lnc
|
||||||
// backwards compatibility: migrate to new storage key
|
window.lnc = new LNC({ })
|
||||||
if (me) migrateLocalStorage('lnc-web:default', `lnc-web:${me.id}`)
|
|
||||||
window.lnc = new LNC({ namespace: me?.id })
|
|
||||||
return window.lnc
|
return window.lnc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +33,6 @@ function validateNarrowPerms (lnc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LNCProvider ({ children }) {
|
export function LNCProvider ({ children }) {
|
||||||
const me = useMe()
|
|
||||||
const { logger } = useWalletLogger(Wallet.LNC)
|
const { logger } = useWalletLogger(Wallet.LNC)
|
||||||
const [config, setConfig] = useState({})
|
const [config, setConfig] = useState({})
|
||||||
const [lnc, setLNC] = useState()
|
const [lnc, setLNC] = useState()
|
||||||
@ -169,7 +165,7 @@ export function LNCProvider ({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const lnc = await getLNC({ me })
|
const lnc = await getLNC()
|
||||||
setLNC(lnc)
|
setLNC(lnc)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
if (lnc.credentials.isPaired) {
|
if (lnc.credentials.isPaired) {
|
||||||
@ -189,7 +185,7 @@ export function LNCProvider ({ children }) {
|
|||||||
setStatus(Status.Error)
|
setStatus(Status.Error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [me, setStatus, setConfig, logger])
|
}, [setStatus, setConfig, logger])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
||||||
|
@ -4,15 +4,13 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
|||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
||||||
import { parseNwcUrl } from '@/lib/url'
|
import { parseNwcUrl } from '@/lib/url'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { Status, migrateLocalStorage } from '.'
|
import { Status } from '.'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
import { useMe } from '../me'
|
|
||||||
|
|
||||||
const NWCContext = createContext()
|
const NWCContext = createContext()
|
||||||
|
|
||||||
export function NWCProvider ({ children }) {
|
export function NWCProvider ({ children }) {
|
||||||
const me = useMe()
|
|
||||||
const [nwcUrl, setNwcUrl] = useState('')
|
const [nwcUrl, setNwcUrl] = useState('')
|
||||||
const [walletPubkey, setWalletPubkey] = useState()
|
const [walletPubkey, setWalletPubkey] = useState()
|
||||||
const [relayUrl, setRelayUrl] = useState()
|
const [relayUrl, setRelayUrl] = useState()
|
||||||
@ -21,10 +19,7 @@ export function NWCProvider ({ children }) {
|
|||||||
const { logger } = useWalletLogger(Wallet.NWC)
|
const { logger } = useWalletLogger(Wallet.NWC)
|
||||||
|
|
||||||
const name = 'NWC'
|
const name = 'NWC'
|
||||||
let storageKey = 'webln:provider:nwc'
|
const storageKey = 'webln:provider:nwc'
|
||||||
if (me) {
|
|
||||||
storageKey = `${storageKey}:${me.id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
||||||
logger.info(`requesting info event from ${relayUrl}`)
|
logger.info(`requesting info event from ${relayUrl}`)
|
||||||
@ -102,18 +97,11 @@ export function NWCProvider ({ children }) {
|
|||||||
}, [logger])
|
}, [logger])
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
let configStr = window.localStorage.getItem(storageKey)
|
const configStr = window.localStorage.getItem(storageKey)
|
||||||
setStatus(Status.Initialized)
|
setStatus(Status.Initialized)
|
||||||
if (!configStr) {
|
if (!configStr) {
|
||||||
if (me) {
|
logger.info('no existing config found')
|
||||||
// backwards compatibility: try old storageKey
|
return
|
||||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
|
||||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
|
||||||
}
|
|
||||||
if (!configStr) {
|
|
||||||
logger.info('no existing config found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
const config = JSON.parse(configStr)
|
||||||
@ -142,7 +130,7 @@ export function NWCProvider ({ children }) {
|
|||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [me, validateParams, logger])
|
}, [validateParams, logger])
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
const saveConfig = useCallback(async (config) => {
|
||||||
// immediately store config so it's not lost even if config is invalid
|
// immediately store config so it's not lost even if config is invalid
|
||||||
|
@ -7,5 +7,4 @@ bitcoinplebdev
|
|||||||
benthecarman
|
benthecarman
|
||||||
stargut
|
stargut
|
||||||
mz
|
mz
|
||||||
btcbagehot
|
btcbagehot
|
||||||
felipe
|
|
@ -397,8 +397,6 @@ services:
|
|||||||
- '--autopilot.disable'
|
- '--autopilot.disable'
|
||||||
- '--pool.auctionserver=test.pool.lightning.finance:12010'
|
- '--pool.auctionserver=test.pool.lightning.finance:12010'
|
||||||
- '--loop.server.host=test.swap.lightning.today:11010'
|
- '--loop.server.host=test.swap.lightning.today:11010'
|
||||||
labels:
|
|
||||||
CONNECT: "localhost:8443"
|
|
||||||
stacker_cln:
|
stacker_cln:
|
||||||
build:
|
build:
|
||||||
context: ./docker/cln
|
context: ./docker/cln
|
||||||
@ -468,8 +466,6 @@ services:
|
|||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
links:
|
links:
|
||||||
- app
|
- app
|
||||||
labels:
|
|
||||||
CONNECT: "localhost:8025"
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
os:
|
os:
|
||||||
|
21
lib/cln.js
21
lib/cln.js
@ -1,27 +1,10 @@
|
|||||||
import fetch from 'cross-fetch'
|
import fetch from 'cross-fetch'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from '@/lib/proxy'
|
|
||||||
|
|
||||||
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
||||||
let protocol, agent
|
const agent = cert ? new https.Agent({ ca: Buffer.from(cert, 'base64') }) : undefined
|
||||||
const httpsAgentOptions = { ca: cert ? Buffer.from(cert, 'base64') : undefined }
|
const url = 'https://' + socket + '/v1/invoice'
|
||||||
const isOnion = /\.onion(:[0-9]+)?$/.test(socket)
|
|
||||||
if (isOnion) {
|
|
||||||
// we support HTTP and HTTPS over Tor
|
|
||||||
protocol = cert ? 'https:' : 'http:'
|
|
||||||
// we need to use our Tor proxy to resolve onion addresses
|
|
||||||
const proxyOptions = { proxy: 'http://127.0.0.1:7050/' }
|
|
||||||
agent = protocol === 'https:'
|
|
||||||
? new HttpsProxyAgent({ ...proxyOptions, ...httpsAgentOptions })
|
|
||||||
: new HttpProxyAgent(proxyOptions)
|
|
||||||
} else {
|
|
||||||
// we only support HTTPS over clearnet
|
|
||||||
agent = new https.Agent(httpsAgentOptions)
|
|
||||||
protocol = 'https:'
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${protocol}//${socket}/v1/invoice`
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { createHash } from 'node:crypto'
|
|
||||||
|
|
||||||
export function hashEmail ({
|
|
||||||
email,
|
|
||||||
salt = process.env.EMAIL_SALT
|
|
||||||
}) {
|
|
||||||
const saltedEmail = `${email.toLowerCase()}${salt}`
|
|
||||||
return createHash('sha256').update(saltedEmail).digest('hex')
|
|
||||||
}
|
|
120
lib/proxy.js
120
lib/proxy.js
@ -1,120 +0,0 @@
|
|||||||
import http from 'http'
|
|
||||||
import https from 'https'
|
|
||||||
|
|
||||||
// from https://github.com/delvedor/hpagent
|
|
||||||
|
|
||||||
export class HttpProxyAgent extends http.Agent {
|
|
||||||
constructor (options) {
|
|
||||||
const { proxy, proxyRequestOptions, ...opts } = options
|
|
||||||
super(opts)
|
|
||||||
this.proxy = typeof proxy === 'string'
|
|
||||||
? new URL(proxy)
|
|
||||||
: proxy
|
|
||||||
this.proxyRequestOptions = proxyRequestOptions || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
createConnection (options, callback) {
|
|
||||||
const requestOptions = {
|
|
||||||
...this.proxyRequestOptions,
|
|
||||||
method: 'CONNECT',
|
|
||||||
host: this.proxy.hostname,
|
|
||||||
port: this.proxy.port,
|
|
||||||
path: `${options.host}:${options.port}`,
|
|
||||||
setHost: false,
|
|
||||||
headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` },
|
|
||||||
agent: false,
|
|
||||||
timeout: options.timeout || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.proxy.username || this.proxy.password) {
|
|
||||||
const base64 = Buffer.from(`${decodeURIComponent(this.proxy.username || '')}:${decodeURIComponent(this.proxy.password || '')}`).toString('base64')
|
|
||||||
requestOptions.headers['proxy-authorization'] = `Basic ${base64}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.proxy.protocol === 'https:') {
|
|
||||||
requestOptions.servername = this.proxy.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = (this.proxy.protocol === 'http:' ? http : https).request(requestOptions)
|
|
||||||
request.once('connect', (response, socket, head) => {
|
|
||||||
request.removeAllListeners()
|
|
||||||
socket.removeAllListeners()
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
callback(null, socket)
|
|
||||||
} else {
|
|
||||||
socket.destroy()
|
|
||||||
callback(new Error(`Bad response: ${response.statusCode}`), null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
request.once('timeout', () => {
|
|
||||||
request.destroy(new Error('Proxy timeout'))
|
|
||||||
})
|
|
||||||
|
|
||||||
request.once('error', err => {
|
|
||||||
request.removeAllListeners()
|
|
||||||
callback(err, null)
|
|
||||||
})
|
|
||||||
|
|
||||||
request.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HttpsProxyAgent extends https.Agent {
|
|
||||||
constructor (options) {
|
|
||||||
const { proxy, proxyRequestOptions, ...opts } = options
|
|
||||||
super(opts)
|
|
||||||
this.proxy = typeof proxy === 'string'
|
|
||||||
? new URL(proxy)
|
|
||||||
: proxy
|
|
||||||
this.proxyRequestOptions = proxyRequestOptions || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
createConnection (options, callback) {
|
|
||||||
const requestOptions = {
|
|
||||||
...this.proxyRequestOptions,
|
|
||||||
method: 'CONNECT',
|
|
||||||
host: this.proxy.hostname,
|
|
||||||
port: this.proxy.port,
|
|
||||||
path: `${options.host}:${options.port}`,
|
|
||||||
setHost: false,
|
|
||||||
headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` },
|
|
||||||
agent: false,
|
|
||||||
timeout: options.timeout || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.proxy.username || this.proxy.password) {
|
|
||||||
const base64 = Buffer.from(`${decodeURIComponent(this.proxy.username || '')}:${decodeURIComponent(this.proxy.password || '')}`).toString('base64')
|
|
||||||
requestOptions.headers['proxy-authorization'] = `Basic ${base64}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Necessary for the TLS check with the proxy to succeed.
|
|
||||||
if (this.proxy.protocol === 'https:') {
|
|
||||||
requestOptions.servername = this.proxy.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = (this.proxy.protocol === 'http:' ? http : https).request(requestOptions)
|
|
||||||
request.once('connect', (response, socket, head) => {
|
|
||||||
request.removeAllListeners()
|
|
||||||
socket.removeAllListeners()
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
const secureSocket = super.createConnection({ ...options, socket })
|
|
||||||
callback(null, secureSocket)
|
|
||||||
} else {
|
|
||||||
socket.destroy()
|
|
||||||
callback(new Error(`Bad response: ${response.statusCode}`), null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
request.once('timeout', () => {
|
|
||||||
request.destroy(new Error('Proxy timeout'))
|
|
||||||
})
|
|
||||||
|
|
||||||
request.once('error', err => {
|
|
||||||
request.removeAllListeners()
|
|
||||||
callback(err, null)
|
|
||||||
})
|
|
||||||
|
|
||||||
request.end()
|
|
||||||
}
|
|
||||||
}
|
|
@ -77,7 +77,7 @@ export function BioForm ({ handleDone, bio }) {
|
|||||||
|
|
||||||
export function UserLayout ({ user, children, containClassName }) {
|
export function UserLayout ({ user, children, containClassName }) {
|
||||||
return (
|
return (
|
||||||
<Layout user={user} footer footerLinks={false} containClassName={containClassName}>
|
<Layout user={user} footer={false} containClassName={containClassName}>
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} />
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -11,7 +11,6 @@ import { getToken } from 'next-auth/jwt'
|
|||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { notifyReferral } from '@/lib/webPush'
|
import { notifyReferral } from '@/lib/webPush'
|
||||||
import { hashEmail } from '@/lib/crypto'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores userIds in user table
|
* Stores userIds in user table
|
||||||
@ -72,6 +71,24 @@ function getCallbacks (req) {
|
|||||||
token.sub = Number(token.id)
|
token.sub = Number(token.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sign them up for the newsletter
|
||||||
|
if (isNewUser && user?.email && process.env.LIST_MONK_URL && process.env.LIST_MONK_AUTH) {
|
||||||
|
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: user.email,
|
||||||
|
name: 'blank',
|
||||||
|
lists: [2],
|
||||||
|
status: 'enabled',
|
||||||
|
preconfirm_subscriptions: true
|
||||||
|
})
|
||||||
|
}).then(async r => console.log(await r.json())).catch(console.log)
|
||||||
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session ({ session, token }) {
|
async session ({ session, token }) {
|
||||||
@ -200,49 +217,7 @@ const providers = [
|
|||||||
export const getAuthOptions = req => ({
|
export const getAuthOptions = req => ({
|
||||||
callbacks: getCallbacks(req),
|
callbacks: getCallbacks(req),
|
||||||
providers,
|
providers,
|
||||||
adapter: {
|
adapter: PrismaAdapter(prisma),
|
||||||
...PrismaAdapter(prisma),
|
|
||||||
createUser: data => {
|
|
||||||
// replace email with email hash in new user payload
|
|
||||||
if (data.email) {
|
|
||||||
const { email } = data
|
|
||||||
data.emailHash = hashEmail({ email })
|
|
||||||
delete data.email
|
|
||||||
// data.email used to be used for name of new accounts. since it's missing, let's generate a new name
|
|
||||||
data.name = data.emailHash.substring(0, 10)
|
|
||||||
// sign them up for the newsletter
|
|
||||||
// don't await it, let it run async
|
|
||||||
enrollInNewsletter({ email })
|
|
||||||
}
|
|
||||||
return prisma.user.create({ data })
|
|
||||||
},
|
|
||||||
getUserByEmail: async email => {
|
|
||||||
const hashedEmail = hashEmail({ email })
|
|
||||||
let user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
// lookup by email hash since we don't store plaintext emails any more
|
|
||||||
emailHash: hashedEmail
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!user) {
|
|
||||||
user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
// lookup by email as a fallback in case a user attempts to login by email during the migration
|
|
||||||
// and their email hasn't been migrated yet
|
|
||||||
email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// HACK! This is required to satisfy next-auth's check here:
|
|
||||||
// https://github.com/nextauthjs/next-auth/blob/5b647e1ac040250ad055e331ba97f8fa461b63cc/packages/next-auth/src/core/routes/callback.ts#L227
|
|
||||||
// since we are nulling `email`, but it expects it to be truthy there.
|
|
||||||
// Since we have the email from the input request, we can copy it here and pretend like we store user emails, even though we don't.
|
|
||||||
if (user) {
|
|
||||||
user.email = email
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt'
|
strategy: 'jwt'
|
||||||
},
|
},
|
||||||
@ -254,34 +229,6 @@ export const getAuthOptions = req => ({
|
|||||||
events: getEventCallbacks()
|
events: getEventCallbacks()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function enrollInNewsletter ({ email }) {
|
|
||||||
if (process.env.LIST_MONK_URL && process.env.LIST_MONK_AUTH) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email,
|
|
||||||
name: 'blank',
|
|
||||||
lists: [2],
|
|
||||||
status: 'enabled',
|
|
||||||
preconfirm_subscriptions: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const jsonResponse = await response.json()
|
|
||||||
console.log(jsonResponse)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error signing user up for newsletter')
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('LIST MONK env vars not set, skipping newsletter enrollment')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await NextAuth(req, res, getAuthOptions(req))
|
await NextAuth(req, res, getAuthOptions(req))
|
||||||
}
|
}
|
||||||
@ -291,21 +238,7 @@ async function sendVerificationRequest ({
|
|||||||
url,
|
url,
|
||||||
provider
|
provider
|
||||||
}) {
|
}) {
|
||||||
let user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({ where: { email } })
|
||||||
where: {
|
|
||||||
// Look for the user by hashed email
|
|
||||||
emailHash: hashEmail({ email })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!user) {
|
|
||||||
user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
// or plaintext email, in case a user tries to login via email during the migration
|
|
||||||
// before their particular record has been migrated
|
|
||||||
email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { server, from } = provider
|
const { server, from } = provider
|
||||||
|
@ -57,7 +57,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
|
|||||||
let session
|
let session
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const [user] = await models.$queryRaw`
|
const [user] = await models.$queryRaw`
|
||||||
SELECT id, name, "apiKeyEnabled"
|
SELECT id, name, email, "apiKeyEnabled"
|
||||||
FROM users
|
FROM users
|
||||||
WHERE "apiKeyHash" = encode(digest(${apiKey}, 'sha256'), 'hex')
|
WHERE "apiKeyHash" = encode(digest(${apiKey}, 'sha256'), 'hex')
|
||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
|
@ -71,7 +71,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate invoice
|
// generate invoice
|
||||||
const expiresAt = datePivot(new Date(), { minutes: 5 })
|
const expiresAt = datePivot(new Date(), { minutes: 1 })
|
||||||
const invoice = await createInvoice({
|
const invoice = await createInvoice({
|
||||||
description,
|
description,
|
||||||
description_hash: descriptionHash,
|
description_hash: descriptionHash,
|
||||||
|
@ -714,8 +714,15 @@ function AuthMethods ({ methods, apiKeyEnabled }) {
|
|||||||
return methods.email
|
return methods.email
|
||||||
? (
|
? (
|
||||||
<div key={provider} className='mt-2 d-flex align-items-center'>
|
<div key={provider} className='mt-2 d-flex align-items-center'>
|
||||||
|
<Input
|
||||||
|
name='email'
|
||||||
|
placeholder={methods.email}
|
||||||
|
groupClassName='mb-0'
|
||||||
|
readOnly
|
||||||
|
noForm
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant='secondary' onClick={
|
className='ms-2' variant='secondary' onClick={
|
||||||
async () => {
|
async () => {
|
||||||
await unlink('email')
|
await unlink('email')
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[emailHash]` on the table `users` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "users" ADD COLUMN "emailHash" TEXT;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "users.email_hash_unique" ON "users"("emailHash");
|
|
||||||
|
|
||||||
-- hack ... prisma doesn't know about our other schemas (e.g. pgboss)
|
|
||||||
-- and this is only really a problem on their "shadow database"
|
|
||||||
-- so we catch the exception it throws and ignore it
|
|
||||||
CREATE OR REPLACE FUNCTION submit_migrate_existing_user_emails_job() RETURNS void AS $$
|
|
||||||
BEGIN
|
|
||||||
-- Submit a job to salt and hash emails after the updated worker has spun-up
|
|
||||||
INSERT INTO pgboss.job (name, data, priority, startafter, expirein)
|
|
||||||
SELECT 'saltAndHashEmails', jsonb_build_object(), -100, now() + interval '10 minutes', interval '1 day';
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
-- catch the exception for prisma dev execution, but do nothing with it
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
-- execute the function once to submit the one-time job
|
|
||||||
SELECT submit_migrate_existing_user_emails_job();
|
|
||||||
-- then drop it since we don't need it anymore
|
|
||||||
DROP FUNCTION submit_migrate_existing_user_emails_job();
|
|
||||||
|
|
||||||
-- function that accepts a salt and migrates all existing emails using the salt then hashing the salted email
|
|
||||||
CREATE OR REPLACE FUNCTION migrate_existing_user_emails(salt TEXT) RETURNS void AS $$
|
|
||||||
BEGIN
|
|
||||||
UPDATE "users"
|
|
||||||
SET "emailHash" = encode(digest(LOWER("email") || salt, 'sha256'), 'hex')
|
|
||||||
WHERE "email" IS NOT NULL;
|
|
||||||
|
|
||||||
-- then wipe the email values
|
|
||||||
UPDATE "users"
|
|
||||||
SET email = NULL;
|
|
||||||
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
@ -19,7 +19,6 @@ model User {
|
|||||||
name String? @unique(map: "users.name_unique") @db.Citext
|
name String? @unique(map: "users.name_unique") @db.Citext
|
||||||
email String? @unique(map: "users.email_unique")
|
email String? @unique(map: "users.email_unique")
|
||||||
emailVerified DateTime? @map("email_verified")
|
emailVerified DateTime? @map("email_verified")
|
||||||
emailHash String? @unique(map: "users.email_hash_unique")
|
|
||||||
image String?
|
image String?
|
||||||
msats BigInt @default(0)
|
msats BigInt @default(0)
|
||||||
freeComments Int @default(5)
|
freeComments Int @default(5)
|
||||||
@ -163,7 +162,7 @@ model Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model WalletLog {
|
model WalletLog {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
userId Int
|
userId Int
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
42
sndev
42
sndev
@ -106,39 +106,6 @@ OPTIONS"
|
|||||||
docker__compose down --help | awk '/Options:/{y=1;next}y'
|
docker__compose down --help | awk '/Options:/{y=1;next}y'
|
||||||
}
|
}
|
||||||
|
|
||||||
sndev__open() {
|
|
||||||
shift
|
|
||||||
service=$(docker__compose ps $1 --format '{{.Label "CONNECT"}}')
|
|
||||||
if [ -z "$service" ]; then
|
|
||||||
echo "no url found for $1"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
service="http://$service"
|
|
||||||
|
|
||||||
echo "opening $1 ... $service"
|
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
|
||||||
open $service
|
|
||||||
elif [ "$(uname)" = "Linux" ]; then
|
|
||||||
xdg-open $service
|
|
||||||
elif [ "$(uname)" = "Windows_NT" ]; then
|
|
||||||
start $service
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
sndev__help_open() {
|
|
||||||
help="
|
|
||||||
open a container's url if it has one
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
$ sndev open SERVICE
|
|
||||||
|
|
||||||
OPTIONS
|
|
||||||
no options currently exist
|
|
||||||
"
|
|
||||||
|
|
||||||
echo "$help"
|
|
||||||
}
|
|
||||||
|
|
||||||
sndev__restart() {
|
sndev__restart() {
|
||||||
shift
|
shift
|
||||||
docker__compose restart "$@"
|
docker__compose restart "$@"
|
||||||
@ -451,16 +418,14 @@ sndev__login() {
|
|||||||
# "SNDEV-TOKEN3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"
|
# "SNDEV-TOKEN3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"
|
||||||
# next-auth concats the token with the secret from env and then sha256's it
|
# next-auth concats the token with the secret from env and then sha256's it
|
||||||
token="d5fce54babffcb070c39f78d947761fd9ec37647fafcecb9734a3085a78e5c5e"
|
token="d5fce54babffcb070c39f78d947761fd9ec37647fafcecb9734a3085a78e5c5e"
|
||||||
salt="202c90943c313b829e65e3f29164fb5dd7ea3370d7262c4159691c2f6493bb8b"
|
|
||||||
# upsert user with nym and nym@sndev.team
|
# upsert user with nym and nym@sndev.team
|
||||||
email="$1@sndev.team"
|
|
||||||
docker__exec db psql -U sn -d stackernews -q <<EOF
|
docker__exec db psql -U sn -d stackernews -q <<EOF
|
||||||
INSERT INTO users (name) VALUES ('$1') ON CONFLICT DO NOTHING;
|
INSERT INTO users (name) VALUES ('$1') ON CONFLICT DO NOTHING;
|
||||||
UPDATE users SET email = '$email', "emailHash" = encode(digest(LOWER('$email')||'$salt', 'sha256'), 'hex') WHERE name = '$1';
|
UPDATE users SET email = '$1@sndev.team' WHERE name = '$1';
|
||||||
INSERT INTO verification_requests (identifier, token, expires)
|
INSERT INTO verification_requests (identifier, token, expires)
|
||||||
VALUES ('$email', '$token', NOW() + INTERVAL '1 day')
|
VALUES ('$1@sndev.team', '$token', NOW() + INTERVAL '1 day')
|
||||||
ON CONFLICT (token) DO UPDATE
|
ON CONFLICT (token) DO UPDATE
|
||||||
SET identifier = '$email', expires = NOW() + INTERVAL '1 day';
|
SET identifier = '$1@sndev.team', expires = NOW() + INTERVAL '1 day';
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@ -531,7 +496,6 @@ COMMANDS
|
|||||||
dev:
|
dev:
|
||||||
pr fetch and checkout a pr
|
pr fetch and checkout a pr
|
||||||
lint run linters
|
lint run linters
|
||||||
open open container url in browser
|
|
||||||
|
|
||||||
other:
|
other:
|
||||||
compose docker compose passthrough
|
compose docker compose passthrough
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2.5rem 0 1rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.createFormContainer {
|
.createFormContainer {
|
||||||
|
@ -23,7 +23,6 @@ import { deleteUnusedImages } from './deleteUnusedImages.js'
|
|||||||
import { territoryBilling, territoryRevenue } from './territory.js'
|
import { territoryBilling, territoryRevenue } from './territory.js'
|
||||||
import { ofac } from './ofac.js'
|
import { ofac } from './ofac.js'
|
||||||
import { autoWithdraw } from './autowithdraw.js'
|
import { autoWithdraw } from './autowithdraw.js'
|
||||||
import { saltAndHashEmails } from './saltAndHashEmails.js'
|
|
||||||
|
|
||||||
const { loadEnvConfig } = nextEnv
|
const { loadEnvConfig } = nextEnv
|
||||||
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
|
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
|
||||||
@ -101,7 +100,6 @@ async function work () {
|
|||||||
await boss.work('territoryBilling', jobWrapper(territoryBilling))
|
await boss.work('territoryBilling', jobWrapper(territoryBilling))
|
||||||
await boss.work('territoryRevenue', jobWrapper(territoryRevenue))
|
await boss.work('territoryRevenue', jobWrapper(territoryRevenue))
|
||||||
await boss.work('ofac', jobWrapper(ofac))
|
await boss.work('ofac', jobWrapper(ofac))
|
||||||
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
|
|
||||||
|
|
||||||
console.log('working jobs')
|
console.log('working jobs')
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export async function saltAndHashEmails ({ models }) {
|
|
||||||
try {
|
|
||||||
console.log('Migrating existing emails to salt and hash them...')
|
|
||||||
await models.$executeRaw`select migrate_existing_user_emails(${process.env.EMAIL_SALT})`
|
|
||||||
console.log('Successfully migrated existing emails to salt and hash them!')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error occurred while salting and hashing existing emails:', err)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user