Merge branch 'master' into 266-zaps-without-account
This commit is contained in:
commit
76b4156ccb
|
@ -0,0 +1,20 @@
|
||||||
|
name: Eslint Check
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
eslint-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18.17.0"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
|
@ -18,6 +18,14 @@ We're experimenting with providing an SN-like service on nostr in [Outer Space](
|
||||||
|
|
||||||
You should then be able to access the site at `localhost:3000` and any changes you make will hot reload. If you want to login locally or use lnd you'll need to modify `.env.sample` appropriately. More details [here](./docs/local-auth.md) and [here](./docs/local-lnd.md). If you have trouble please open an issue so I can help and update the README for everyone else.
|
You should then be able to access the site at `localhost:3000` and any changes you make will hot reload. If you want to login locally or use lnd you'll need to modify `.env.sample` appropriately. More details [here](./docs/local-auth.md) and [here](./docs/local-lnd.md). If you have trouble please open an issue so I can help and update the README for everyone else.
|
||||||
|
|
||||||
|
# web push
|
||||||
|
|
||||||
|
To enable Web Push locally, you will need to set the `VAPID_*` env vars. `VAPID_MAILTO` needs to be a email address using the `mailto:` scheme. For `NEXT_PUBLIC_VAPID_KEY` and `VAPID_PRIVKEY`, you can run `npx web-push generate-vapid-keys`.
|
||||||
|
|
||||||
|
# imgproxy
|
||||||
|
|
||||||
|
To configure the image proxy, you will need to set the `IMGPROXY_` env vars. `NEXT_PUBLIC_IMGPROXY_URL` needs to point to the image proxy service. `IMGPROXY_KEY` and `IMGPROXY_SALT` can be set using `openssl rand -hex 64`.
|
||||||
|
|
||||||
# stack
|
# stack
|
||||||
The site is written in javascript using Next.js, a React framework. The backend API is provided via graphql. The database is postgresql modelled with prisma. The job queue is also maintained in postgresql. We use lnd for our lightning node. A customized Bootstrap theme is used for styling.
|
The site is written in javascript using Next.js, a React framework. The backend API is provided via graphql. The database is postgresql modelled with prisma. The job queue is also maintained in postgresql. We use lnd for our lightning node. A customized Bootstrap theme is used for styling.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { createHmac } from 'node:crypto'
|
import { createHmac } from 'node:crypto'
|
||||||
import { extractUrls } from '../../../lib/md'
|
import { extractUrls } from '../../../lib/md'
|
||||||
|
|
||||||
|
const imgProxyEnabled = process.env.NODE_ENV === 'production' ||
|
||||||
|
(process.env.NEXT_PUBLIC_IMGPROXY_URL && process.env.IMGPROXY_SALT && process.env.IMGPROXY_KEY)
|
||||||
|
|
||||||
|
if (!imgProxyEnabled) {
|
||||||
|
console.warn('IMGPROXY_* env vars not set, imgproxy calls are no-ops now')
|
||||||
|
}
|
||||||
|
|
||||||
const IMGPROXY_URL = process.env.NEXT_PUBLIC_IMGPROXY_URL
|
const IMGPROXY_URL = process.env.NEXT_PUBLIC_IMGPROXY_URL
|
||||||
const IMGPROXY_SALT = process.env.IMGPROXY_SALT
|
const IMGPROXY_SALT = process.env.IMGPROXY_SALT
|
||||||
const IMGPROXY_KEY = process.env.IMGPROXY_KEY
|
const IMGPROXY_KEY = process.env.IMGPROXY_KEY
|
||||||
|
@ -36,6 +43,8 @@ const isImageURL = async url => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const proxyImages = async text => {
|
export const proxyImages = async text => {
|
||||||
|
if (!imgProxyEnabled) return text
|
||||||
|
|
||||||
const urls = extractUrls(text)
|
const urls = extractUrls(text)
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (url.startsWith(IMGPROXY_URL)) continue
|
if (url.startsWith(IMGPROXY_URL)) continue
|
||||||
|
|
|
@ -80,7 +80,8 @@ async function authMethods (user, args, { models, me }) {
|
||||||
email: user.emailVerified && user.email,
|
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,
|
||||||
slashtags: !!user.slashtagId
|
slashtags: !!user.slashtagId,
|
||||||
|
nostr: !!user.nostrAuthPubkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,6 +528,8 @@ export default {
|
||||||
user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
|
user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
|
||||||
} else if (authType === 'slashtags') {
|
} else if (authType === 'slashtags') {
|
||||||
user = await models.user.update({ where: { id: me.id }, data: { slashtagId: null } })
|
user = await models.user.update({ where: { id: me.id }, data: { slashtagId: null } })
|
||||||
|
} else if (authType === 'nostr') {
|
||||||
|
user = await models.user.update({ where: { id: me.id }, data: { nostrAuthPubkey: null } })
|
||||||
} else if (authType === 'email') {
|
} else if (authType === 'email') {
|
||||||
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
|
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default gql`
|
||||||
type AuthMethods {
|
type AuthMethods {
|
||||||
lightning: Boolean!
|
lightning: Boolean!
|
||||||
slashtags: Boolean!
|
slashtags: Boolean!
|
||||||
|
nostr: Boolean!
|
||||||
github: Boolean!
|
github: Boolean!
|
||||||
twitter: Boolean!
|
twitter: Boolean!
|
||||||
email: String
|
email: String
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
import webPush from 'web-push'
|
import webPush from 'web-push'
|
||||||
import models from '../models'
|
import models from '../models'
|
||||||
import { COMMENT_DEPTH_LIMIT } from '../../lib/constants'
|
import { COMMENT_DEPTH_LIMIT } from '../../lib/constants'
|
||||||
|
import removeMd from 'remove-markdown'
|
||||||
|
|
||||||
webPush.setVapidDetails(
|
const webPushEnabled = process.env.NODE_ENV === 'production' ||
|
||||||
process.env.VAPID_MAILTO,
|
(process.env.VAPID_MAILTO && process.env.NEXT_PUBLIC_VAPID_PUBKEY && process.env.VAPID_PRIVKEY)
|
||||||
process.env.NEXT_PUBLIC_VAPID_PUBKEY,
|
|
||||||
process.env.VAPID_PRIVKEY
|
if (webPushEnabled) {
|
||||||
)
|
webPush.setVapidDetails(
|
||||||
|
process.env.VAPID_MAILTO,
|
||||||
|
process.env.NEXT_PUBLIC_VAPID_PUBKEY,
|
||||||
|
process.env.VAPID_PRIVKEY
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.warn('VAPID_* env vars not set, skipping webPush setup')
|
||||||
|
}
|
||||||
|
|
||||||
const createPayload = (notification) => {
|
const createPayload = (notification) => {
|
||||||
// https://web.dev/push-notifications-display-a-notification/#visual-options
|
// https://web.dev/push-notifications-display-a-notification/#visual-options
|
||||||
const { title, ...options } = notification
|
let { title, body, ...options } = notification
|
||||||
|
if (body) body = removeMd(body)
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
title,
|
title,
|
||||||
options: {
|
options: {
|
||||||
|
body,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
icon: '/icons/icon_x96.png',
|
icon: '/icons/icon_x96.png',
|
||||||
...options
|
...options
|
||||||
|
@ -41,12 +51,16 @@ const createItemUrl = async ({ id }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendNotification = (subscription, payload) => {
|
const sendNotification = (subscription, payload) => {
|
||||||
|
if (!webPushEnabled) {
|
||||||
|
console.warn('webPush not configured. skipping notification')
|
||||||
|
return
|
||||||
|
}
|
||||||
const { id, endpoint, p256dh, auth } = subscription
|
const { id, endpoint, p256dh, auth } = subscription
|
||||||
return webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, payload)
|
return webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, payload)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.statusCode === 400) {
|
if (err.statusCode === 400) {
|
||||||
console.log('[webPush] invalid request: ', err)
|
console.log('[webPush] invalid request: ', err)
|
||||||
} else if (err.statusCode === 403) {
|
} else if ([401, 403].includes(err.statusCode)) {
|
||||||
console.log('[webPush] auth error: ', err)
|
console.log('[webPush] auth error: ', err)
|
||||||
} else if (err.statusCode === 404 || err.statusCode === 410) {
|
} else if (err.statusCode === 404 || err.statusCode === 410) {
|
||||||
console.log('[webPush] subscription has expired or is no longer valid: ', err)
|
console.log('[webPush] subscription has expired or is no longer valid: ', err)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
.login {
|
.login {
|
||||||
justify-content: center;
|
justify-content: start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
|
min-height: 600px;
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ import GithubIcon from '../svgs/github-fill.svg'
|
||||||
import TwitterIcon from '../svgs/twitter-fill.svg'
|
import TwitterIcon from '../svgs/twitter-fill.svg'
|
||||||
import LightningIcon from '../svgs/bolt.svg'
|
import LightningIcon from '../svgs/bolt.svg'
|
||||||
import SlashtagsIcon from '../svgs/slashtags.svg'
|
import SlashtagsIcon from '../svgs/slashtags.svg'
|
||||||
|
import NostrIcon from '../svgs/nostr.svg'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
|
|
||||||
export default function LoginButton ({ text, type, className, onClick }) {
|
export default function LoginButton ({ text, type, className, onClick }) {
|
||||||
|
@ -19,6 +20,10 @@ export default function LoginButton ({ text, type, className, onClick }) {
|
||||||
Icon = SlashtagsIcon
|
Icon = SlashtagsIcon
|
||||||
variant = 'grey-medium'
|
variant = 'grey-medium'
|
||||||
break
|
break
|
||||||
|
case 'nostr':
|
||||||
|
Icon = NostrIcon
|
||||||
|
variant = 'nostr'
|
||||||
|
break
|
||||||
case 'lightning':
|
case 'lightning':
|
||||||
default:
|
default:
|
||||||
Icon = LightningIcon
|
Icon = LightningIcon
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useState } from 'react'
|
||||||
import Alert from 'react-bootstrap/Alert'
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { LightningAuthWithExplainer, SlashtagsAuth } from './lightning-auth'
|
import { LightningAuthWithExplainer, SlashtagsAuth } from './lightning-auth'
|
||||||
|
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'
|
||||||
|
|
||||||
|
@ -59,6 +60,10 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
return <SlashtagsAuth callbackUrl={callbackUrl} text={text} />
|
return <SlashtagsAuth callbackUrl={callbackUrl} text={text} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (router.query.type === 'nostr') {
|
||||||
|
return <NostrAuth callbackUrl={callbackUrl} text={text} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
{Header && <Header />}
|
{Header && <Header />}
|
||||||
|
@ -80,6 +85,7 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
||||||
)
|
)
|
||||||
case 'Lightning':
|
case 'Lightning':
|
||||||
case 'Slashtags':
|
case 'Slashtags':
|
||||||
|
case 'Nostr':
|
||||||
return (
|
return (
|
||||||
<LoginButton
|
<LoginButton
|
||||||
className={`mt-2 ${styles.providerButton}`}
|
className={`mt-2 ${styles.providerButton}`}
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { gql, useMutation } from '@apollo/client'
|
||||||
|
import { signIn } from 'next-auth/react'
|
||||||
|
import Container from 'react-bootstrap/Container'
|
||||||
|
import Col from 'react-bootstrap/Col'
|
||||||
|
import Row from 'react-bootstrap/Row'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import AccordianItem from './accordian-item'
|
||||||
|
import BackIcon from '../svgs/arrow-left-line.svg'
|
||||||
|
import styles from './lightning-auth.module.css'
|
||||||
|
|
||||||
|
function ExtensionError ({ message, details }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className='fw-bold text-danger pb-1'>error: {message}</h4>
|
||||||
|
<div className='text-muted pb-4'>{details}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NostrExplainer ({ text }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExtensionError message='nostr extension not found' details='Nostr extensions are the safest way to use your nostr identity on Stacker News.' />
|
||||||
|
<Row className='w-100 text-muted'>
|
||||||
|
<AccordianItem
|
||||||
|
header={`Which extensions can I use to ${(text || 'Login').toLowerCase()} with Nostr?`}
|
||||||
|
show
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href='https://getalby.com'>Alby</a><br />
|
||||||
|
available for: chrome, firefox, and safari
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://www.getflamingo.org/'>Flamingo</a><br />
|
||||||
|
available for: chrome
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a><br />
|
||||||
|
available for: chrome
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a><br />
|
||||||
|
available for: firefox
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/horse'>horse</a><br />
|
||||||
|
available for: chrome<br />
|
||||||
|
supports hardware signing
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NostrAuth ({ text, callbackUrl }) {
|
||||||
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
|
mutation createAuth {
|
||||||
|
createAuth {
|
||||||
|
k1
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
const [hasExtension, setHasExtension] = useState(undefined)
|
||||||
|
const [extensionError, setExtensionError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createAuth()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const k1 = data?.createAuth.k1
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!k1) return
|
||||||
|
setHasExtension(!!window.nostr)
|
||||||
|
if (!window.nostr) {
|
||||||
|
const err = { message: 'nostr extension not found' }
|
||||||
|
console.error(err.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.info('nostr extension detected')
|
||||||
|
let mounted = true;
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
// have them sign a message with the challenge
|
||||||
|
let event
|
||||||
|
try {
|
||||||
|
event = await window.nostr.signEvent({
|
||||||
|
kind: 22242,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['challenge', k1]],
|
||||||
|
content: 'Stacker News Authentication'
|
||||||
|
})
|
||||||
|
if (!event) throw new Error('extension returned empty event')
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === 'window.nostr call already executing') return
|
||||||
|
setExtensionError({ message: 'nostr extension failed to sign event', details: e.message })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign them in
|
||||||
|
try {
|
||||||
|
const { error, ok } = await signIn('nostr', {
|
||||||
|
event: JSON.stringify(event),
|
||||||
|
callbackUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
throw new Error('auth failed')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('authorization failed', e)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return
|
||||||
|
setExtensionError({ message: `${text} failed`, details: e.message })
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => { mounted = false }
|
||||||
|
}, [k1, hasExtension])
|
||||||
|
|
||||||
|
if (error) return <div>error</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasExtension === false && <NostrExplainer text={text} />}
|
||||||
|
{extensionError && <ExtensionError {...extensionError} />}
|
||||||
|
{hasExtension && !extensionError &&
|
||||||
|
<>
|
||||||
|
<h4 className='fw-bold text-success pb-1'>nostr extension found</h4>
|
||||||
|
<h6 className='text-muted pb-4'>authorize event signature in extension</h6>
|
||||||
|
</>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NostrAuthWithExplainer ({ text, callbackUrl }) {
|
||||||
|
const router = useRouter()
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className={styles.login}>
|
||||||
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
||||||
|
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
||||||
|
<NostrAuth text={text} callbackUrl={callbackUrl} />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
|
@ -62,6 +62,13 @@ export const ServiceWorkerProvider = ({ children }) => {
|
||||||
let pushSubscription = await registration.pushManager.subscribe(subscribeOptions)
|
let pushSubscription = await registration.pushManager.subscribe(subscribeOptions)
|
||||||
// convert keys from ArrayBuffer to string
|
// convert keys from ArrayBuffer to string
|
||||||
pushSubscription = JSON.parse(JSON.stringify(pushSubscription))
|
pushSubscription = JSON.parse(JSON.stringify(pushSubscription))
|
||||||
|
// Send subscription to service worker to save it so we can use it later during `pushsubscriptionchange`
|
||||||
|
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
action: 'STORE_SUBSCRIPTION',
|
||||||
|
subscription: pushSubscription
|
||||||
|
})
|
||||||
|
// send subscription to server
|
||||||
const variables = {
|
const variables = {
|
||||||
endpoint: pushSubscription.endpoint,
|
endpoint: pushSubscription.endpoint,
|
||||||
p256dh: pushSubscription.keys.p256dh,
|
p256dh: pushSubscription.keys.p256dh,
|
||||||
|
@ -89,6 +96,10 @@ export const ServiceWorkerProvider = ({ children }) => {
|
||||||
pushManager: 'PushManager' in window
|
pushManager: 'PushManager' in window
|
||||||
})
|
})
|
||||||
setPermission({ notification: 'Notification' in window ? window.Notification.permission : 'denied' })
|
setPermission({ notification: 'Notification' in window ? window.Notification.permission : 'denied' })
|
||||||
|
// since (a lot of) browsers don't support the pushsubscriptionchange event,
|
||||||
|
// we sync with server manually by checking on every page reload if the push subscription changed.
|
||||||
|
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
||||||
|
navigator.serviceWorker.controller.postMessage({ action: 'SYNC_SUBSCRIPTION' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -58,6 +58,7 @@ export const SETTINGS_FIELDS = gql`
|
||||||
authMethods {
|
authMethods {
|
||||||
lightning
|
lightning
|
||||||
slashtags
|
slashtags
|
||||||
|
nostr
|
||||||
github
|
github
|
||||||
twitter
|
twitter
|
||||||
email
|
email
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"tldts": "^6.0.13",
|
"tldts": "^6.0.13",
|
||||||
|
"serviceworker-storage": "^0.1.0",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"url-unshort": "^6.1.0",
|
"url-unshort": "^6.1.0",
|
||||||
|
@ -16934,6 +16935,11 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/serviceworker-storage": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A=="
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
||||||
|
@ -31637,6 +31643,11 @@
|
||||||
"send": "0.18.0"
|
"send": "0.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"serviceworker-storage": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A=="
|
||||||
|
},
|
||||||
"set-cookie-parser": {
|
"set-cookie-parser": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"dev": "NODE_OPTIONS='--trace-warnings' next dev",
|
"dev": "NODE_OPTIONS='--trace-warnings' next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"migrate": "prisma migrate deploy",
|
"migrate": "prisma migrate deploy",
|
||||||
"start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT --keepAliveTimeout 120000"
|
"start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT --keepAliveTimeout 120000",
|
||||||
|
"lint": "standard"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.17",
|
"@apollo/client": "^3.7.17",
|
||||||
|
@ -76,6 +77,7 @@
|
||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"tldts": "^6.0.13",
|
"tldts": "^6.0.13",
|
||||||
|
"serviceworker-storage": "^0.1.0",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"url-unshort": "^6.1.0",
|
"url-unshort": "^6.1.0",
|
||||||
|
@ -101,7 +103,8 @@
|
||||||
],
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"next"
|
"next"
|
||||||
]
|
],
|
||||||
|
"ignore": ["**/spawn"]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.22.9",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
import NextAuth from 'next-auth'
|
import NextAuth from 'next-auth'
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials'
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||||
import GitHubProvider from 'next-auth/providers/github'
|
import GitHubProvider from 'next-auth/providers/github'
|
||||||
|
@ -9,6 +10,7 @@ import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import { decode, getToken } from 'next-auth/jwt'
|
import { decode, getToken } from 'next-auth/jwt'
|
||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||||
import jose1 from 'jose1'
|
import jose1 from 'jose1'
|
||||||
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
|
|
||||||
function getCallbacks (req) {
|
function getCallbacks (req) {
|
||||||
return {
|
return {
|
||||||
|
@ -102,6 +104,37 @@ async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function nostrEventAuth (event) {
|
||||||
|
// parse event
|
||||||
|
const e = JSON.parse(event)
|
||||||
|
|
||||||
|
// is the event id a hash of this event
|
||||||
|
const id = createHash('sha256').update(
|
||||||
|
JSON.stringify(
|
||||||
|
[0, e.pubkey, e.created_at, e.kind, e.tags, e.content]
|
||||||
|
)
|
||||||
|
).digest('hex')
|
||||||
|
if (id !== e.id) {
|
||||||
|
throw new Error('invalid event id')
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the signature valid
|
||||||
|
if (!(await schnorr.verify(e.sig, e.id, e.pubkey))) {
|
||||||
|
throw new Error('invalid signature')
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the challenge present in the event
|
||||||
|
if (!(e.tags[0].length === 2 && e.tags[0][0] === 'challenge')) {
|
||||||
|
throw new Error('expected tags = [["challenge", <challenge>]]')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pubkey = e.pubkey
|
||||||
|
const k1 = e.tags[0][1]
|
||||||
|
await prisma.lnAuth.update({ data: { pubkey }, where: { k1 } })
|
||||||
|
|
||||||
|
return { k1, pubkey }
|
||||||
|
}
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'lightning',
|
id: 'lightning',
|
||||||
|
@ -112,6 +145,17 @@ const providers = [
|
||||||
},
|
},
|
||||||
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
|
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
|
||||||
}),
|
}),
|
||||||
|
CredentialsProvider({
|
||||||
|
id: 'nostr',
|
||||||
|
name: 'Nostr',
|
||||||
|
credentials: {
|
||||||
|
event: { label: 'event', type: 'text' }
|
||||||
|
},
|
||||||
|
authorize: async ({ event }, req) => {
|
||||||
|
const credentials = await nostrEventAuth(event)
|
||||||
|
return pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
|
||||||
|
}
|
||||||
|
}),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'slashtags',
|
id: 'slashtags',
|
||||||
name: 'Slashtags',
|
name: 'Slashtags',
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { SUPPORTED_CURRENCIES } from '../lib/currency'
|
||||||
import PageLoading from '../components/page-loading'
|
import PageLoading from '../components/page-loading'
|
||||||
import { useShowModal } from '../components/modal'
|
import { useShowModal } from '../components/modal'
|
||||||
import { authErrorMessage } from '../components/login'
|
import { authErrorMessage } from '../components/login'
|
||||||
|
import { NostrAuth } from '../components/nostr-auth'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||||
|
|
||||||
|
@ -297,6 +298,23 @@ function QRLinkButton ({ provider, unlink, status }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NostrLinkButton ({ unlink, status }) {
|
||||||
|
const showModal = useShowModal()
|
||||||
|
const text = status ? 'Unlink' : 'Link'
|
||||||
|
const onClick = status
|
||||||
|
? unlink
|
||||||
|
: () => showModal(onClose =>
|
||||||
|
<div className='d-flex flex-column align-items-center'>
|
||||||
|
<NostrAuth text='Unlink' />
|
||||||
|
</div>)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoginButton
|
||||||
|
className='d-block mt-2' type='nostr' text={text} onClick={onClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
@ -341,6 +359,7 @@ function AuthMethods ({ methods }) {
|
||||||
email
|
email
|
||||||
twitter
|
twitter
|
||||||
github
|
github
|
||||||
|
nostr
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
update (cache, { data: { unlinkAuth } }) {
|
update (cache, { data: { unlinkAuth } }) {
|
||||||
|
@ -416,6 +435,8 @@ function AuthMethods ({ methods }) {
|
||||||
status={methods[provider]} unlink={async () => await unlink(provider)}
|
status={methods[provider]} unlink={async () => await unlink(provider)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (provider === 'nostr') {
|
||||||
|
return <NostrLinkButton key='nostr' status={methods[provider]} unlink={async () => await unlink(provider)} />
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<LoginButton
|
<LoginButton
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[nostrAuthPubkey]` on the table `users` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "nostrAuthPubkey" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users.nostrAuthPubkey_unique" ON "users"("nostrAuthPubkey");
|
|
@ -51,6 +51,7 @@ model User {
|
||||||
turboTipping Boolean @default(false)
|
turboTipping Boolean @default(false)
|
||||||
referrerId Int?
|
referrerId Int?
|
||||||
nostrPubkey String?
|
nostrPubkey String?
|
||||||
|
nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique")
|
||||||
slashtagId String? @unique(map: "users.slashtagId_unique")
|
slashtagId String? @unique(map: "users.slashtagId_unique")
|
||||||
noteCowboyHat Boolean @default(true)
|
noteCowboyHat Boolean @default(true)
|
||||||
streak Int?
|
streak Int?
|
||||||
|
|
|
@ -7,6 +7,7 @@ $twitter: #1da1f2;
|
||||||
$boost: #8c25f4;
|
$boost: #8c25f4;
|
||||||
$light: #f8f9fa;
|
$light: #f8f9fa;
|
||||||
$dark: #212529;
|
$dark: #212529;
|
||||||
|
$nostr: #8d45dd;
|
||||||
|
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
"primary" : #FADA5E,
|
"primary" : #FADA5E,
|
||||||
|
@ -21,6 +22,7 @@ $theme-colors: (
|
||||||
"grey" : #e9ecef,
|
"grey" : #e9ecef,
|
||||||
"grey-medium" : #d2d2d2,
|
"grey-medium" : #d2d2d2,
|
||||||
"grey-darkmode": #8c8c8c,
|
"grey-darkmode": #8c8c8c,
|
||||||
|
"nostr": #8d45dd,
|
||||||
);
|
);
|
||||||
|
|
||||||
$body-bg: #fcfcff;
|
$body-bg: #fcfcff;
|
||||||
|
@ -227,6 +229,13 @@ mark {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-nostr, .btn-nostr:hover, .btn-nostr:active {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
stroke: #ffffff !important;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-outline-grey-darkmode:hover, .btn-outline-grey-darkmode:active {
|
.btn-outline-grey-darkmode:hover, .btn-outline-grey-darkmode:active {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
@ -568,6 +577,13 @@ div[contenteditable]:focus,
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-nostr svg {
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-dark svg {
|
.btn-dark svg {
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 875 875">
|
||||||
|
<path class="cls-1" d="m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
70
sw/index.js
70
sw/index.js
|
@ -5,6 +5,11 @@ import { setDefaultHandler } from 'workbox-routing'
|
||||||
import { NetworkOnly } from 'workbox-strategies'
|
import { NetworkOnly } from 'workbox-strategies'
|
||||||
import { enable } from 'workbox-navigation-preload'
|
import { enable } from 'workbox-navigation-preload'
|
||||||
import manifest from './precache-manifest.json'
|
import manifest from './precache-manifest.json'
|
||||||
|
import ServiceWorkerStorage from 'serviceworker-storage'
|
||||||
|
|
||||||
|
self.__WB_DISABLE_DEV_LOGS = true
|
||||||
|
|
||||||
|
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
||||||
|
|
||||||
// preloading improves startup performance
|
// preloading improves startup performance
|
||||||
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
||||||
|
@ -80,35 +85,54 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
event.notification.close()
|
event.notification.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('pushsubscriptionchange', (event) => {
|
// https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data.action === 'STORE_SUBSCRIPTION') {
|
||||||
|
return event.waitUntil(storage.setItem('subscription', event.data.subscription))
|
||||||
|
}
|
||||||
|
if (event.data.action === 'SYNC_SUBSCRIPTION') {
|
||||||
|
return event.waitUntil(handlePushSubscriptionChange())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handlePushSubscriptionChange (oldSubscription, newSubscription) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
|
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
|
||||||
|
// fallbacks since browser may not set oldSubscription and newSubscription
|
||||||
|
oldSubscription ??= await storage.getItem('subscription')
|
||||||
|
newSubscription ??= await self.registration.pushManager.getSubscription()
|
||||||
|
if (!newSubscription) {
|
||||||
|
// no subscription exists at the moment
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (oldSubscription?.endpoint === newSubscription.endpoint) {
|
||||||
|
// subscription did not change. no need to sync with server
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// convert keys from ArrayBuffer to string
|
||||||
|
newSubscription = JSON.parse(JSON.stringify(newSubscription))
|
||||||
|
const variables = {
|
||||||
|
endpoint: newSubscription.endpoint,
|
||||||
|
p256dh: newSubscription.keys.p256dh,
|
||||||
|
auth: newSubscription.keys.auth,
|
||||||
|
oldEndpoint: oldSubscription?.endpoint
|
||||||
|
}
|
||||||
const query = `
|
const query = `
|
||||||
mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) {
|
mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) {
|
||||||
savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) {
|
savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
const body = JSON.stringify({ query, variables })
|
||||||
|
await fetch('/api/graphql', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/json'
|
||||||
|
},
|
||||||
|
body
|
||||||
|
})
|
||||||
|
await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
|
||||||
|
}
|
||||||
|
|
||||||
const subscription = self.registration.pushManager
|
self.addEventListener('pushsubscriptionchange', (event) => {
|
||||||
.subscribe(event.oldSubscription.options)
|
event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
|
||||||
.then((subscription) => {
|
|
||||||
// convert keys from ArrayBuffer to string
|
|
||||||
subscription = JSON.parse(JSON.stringify(subscription))
|
|
||||||
const variables = {
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
p256dh: subscription.keys.p256dh,
|
|
||||||
auth: subscription.keys.auth,
|
|
||||||
oldEndpoint: event.oldSubscription.endpoint
|
|
||||||
}
|
|
||||||
const body = JSON.stringify({ query, variables })
|
|
||||||
return fetch('/api/graphql', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-type': 'application/json'
|
|
||||||
},
|
|
||||||
body
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
event.waitUntil(subscription)
|
|
||||||
}, false)
|
}, false)
|
||||||
|
|
Loading…
Reference in New Issue