allow nip05 for users
This commit is contained in:
parent
7a4a24c6df
commit
2d012ba7fe
|
@ -334,12 +334,29 @@ export default {
|
|||
throw error
|
||||
}
|
||||
},
|
||||
setSettings: async (parent, data, { me, models }) => {
|
||||
setSettings: async (parent, { nostrRelays, ...data }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
return await models.user.update({ where: { id: me.id }, data })
|
||||
if (nostrRelays?.length) {
|
||||
const connectOrCreate = []
|
||||
for (const nr of nostrRelays) {
|
||||
await models.nostrRelay.upsert({
|
||||
where: { addr: nr },
|
||||
update: { addr: nr },
|
||||
create: { addr: nr }
|
||||
})
|
||||
connectOrCreate.push({
|
||||
where: { userId_nostrRelayAddr: { userId: me.id, nostrRelayAddr: nr } },
|
||||
create: { nostrRelayAddr: nr }
|
||||
})
|
||||
}
|
||||
|
||||
return await models.user.update({ where: { id: me.id }, data: { ...data, nostrRelays: { deleteMany: {}, connectOrCreate } } })
|
||||
} else {
|
||||
return await models.user.update({ where: { id: me.id }, data: { ...data, nostrRelays: { deleteMany: {} } } })
|
||||
}
|
||||
},
|
||||
setWalkthrough: async (parent, { upvotePopover, tipPopover }, { me, models }) => {
|
||||
if (!me) {
|
||||
|
@ -533,6 +550,13 @@ export default {
|
|||
}).invites({ take: 1 })
|
||||
|
||||
return invites.length > 0
|
||||
},
|
||||
nostrRelays: async (user, args, { models }) => {
|
||||
const relays = await models.userNostrRelay.findMany({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
|
||||
return relays?.map(r => r.nostrRelayAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export default gql`
|
|||
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!,
|
||||
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
|
||||
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
|
||||
wildWestMode: Boolean!, greeterMode: Boolean!): User
|
||||
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!]): User
|
||||
setPhoto(photoId: ID!): Int!
|
||||
upsertBio(bio: String!): User!
|
||||
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
||||
|
@ -52,6 +52,8 @@ export default gql`
|
|||
tipDefault: Int!
|
||||
turboTipping: Boolean!
|
||||
fiatCurrency: String!
|
||||
nostrPubkey: String
|
||||
nostrRelays: [String!]
|
||||
bio: Item
|
||||
bioId: Int
|
||||
photoId: Int
|
||||
|
|
|
@ -299,7 +299,7 @@ export function Input ({ label, groupClassName, ...props }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function VariableInput ({ label, groupClassName, name, hint, max, readOnlyLen, ...props }) {
|
||||
export function VariableInput ({ label, groupClassName, name, hint, max, min, readOnlyLen, ...props }) {
|
||||
return (
|
||||
<FormGroup label={label} className={groupClassName}>
|
||||
<FieldArray name={name}>
|
||||
|
@ -307,11 +307,11 @@ export function VariableInput ({ label, groupClassName, name, hint, max, readOnl
|
|||
const options = form.values[name]
|
||||
return (
|
||||
<>
|
||||
{options.map((_, i) => (
|
||||
{options?.map((_, i) => (
|
||||
<div key={i}>
|
||||
<BootstrapForm.Row className='mb-2'>
|
||||
<Col>
|
||||
<InputInner name={`${name}[${i}]`} {...props} readOnly={i < readOnlyLen} placeholder={i > 1 ? 'optional' : undefined} />
|
||||
<InputInner name={`${name}[${i}]`} {...props} readOnly={i < readOnlyLen} placeholder={i >= min ? 'optional' : undefined} />
|
||||
</Col>
|
||||
{options.length - 1 === i && options.length !== max
|
||||
? <AddIcon className='fill-grey align-self-center pointer mx-2' onClick={() => fieldArrayHelpers.push('')} />
|
||||
|
|
|
@ -86,6 +86,7 @@ export function PollForm ({ item, editThreshold }) {
|
|||
name='options'
|
||||
readOnlyLen={initialOptions?.length}
|
||||
max={MAX_POLL_NUM_CHOICES}
|
||||
min={2}
|
||||
hint={editThreshold
|
||||
? <div className='text-muted font-weight-bold'><Countdown date={editThreshold} /></div>
|
||||
: null}
|
||||
|
|
|
@ -46,6 +46,8 @@ export const SETTINGS_FIELDS = gql`
|
|||
noteJobIndicator
|
||||
hideInvoiceDesc
|
||||
hideFromTopUsers
|
||||
nostrPubkey
|
||||
nostrRelays
|
||||
wildWestMode
|
||||
greeterMode
|
||||
authMethods {
|
||||
|
@ -70,12 +72,12 @@ ${SETTINGS_FIELDS}
|
|||
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!,
|
||||
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
|
||||
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
|
||||
$wildWestMode: Boolean!, $greeterMode: Boolean!) {
|
||||
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!]) {
|
||||
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency,
|
||||
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
|
||||
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
|
||||
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
|
||||
wildWestMode: $wildWestMode, greeterMode: $greeterMode) {
|
||||
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays) {
|
||||
...SettingsFields
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,3 +16,4 @@ export const ITEM_SPAM_INTERVAL = '10m'
|
|||
export const MAX_POLL_NUM_CHOICES = 10
|
||||
export const ITEM_FILTER_THRESHOLD = 1.2
|
||||
export const DONT_LIKE_THIS_COST = 1
|
||||
export const MAX_NOSTR_RELAY_NUM = 20
|
||||
|
|
|
@ -6,4 +6,7 @@ export function ensureProtocol (value) {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const URL_REGEXP = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
|
||||
export const URL_REGEXP = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const WS_REGEXP = /^(wss?:\/\/)([0-9]{1,3}(?:\.[0-9]{1,3}){3}|(?=[^\/]{1,254}(?![^\/]))(?:(?=[a-zA-Z0-9-]{1,63}\.)(?:xn--+)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63})(:([0-9]{1,5}))?$/
|
||||
|
|
|
@ -42,6 +42,18 @@ module.exports = withPlausibleProxy()({
|
|||
value: 'public, max-age=31536000, immutable'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
source: '/.well-known/:slug*',
|
||||
headers: [
|
||||
...corsHeaders
|
||||
]
|
||||
},
|
||||
{
|
||||
source: '/api/lnauth',
|
||||
headers: [
|
||||
...corsHeaders
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -71,6 +83,10 @@ module.exports = withPlausibleProxy()({
|
|||
source: '/.well-known/lnurlp/:username',
|
||||
destination: '/api/lnurlp/:username'
|
||||
},
|
||||
{
|
||||
source: '/.well-known/nostr.json',
|
||||
destination: '/api/nostr/nip05'
|
||||
},
|
||||
{
|
||||
source: '/~:sub',
|
||||
destination: '/~/:sub'
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import models from '../../../api/models'
|
||||
|
||||
export default async function Nip05 ({ query: { name } }, res) {
|
||||
const names = {}
|
||||
let relays = {}
|
||||
|
||||
const users = await models.user.findMany({
|
||||
where: {
|
||||
name,
|
||||
nostrPubkey: { not: null }
|
||||
},
|
||||
include: { nostrRelays: true }
|
||||
})
|
||||
|
||||
for (const user of users) {
|
||||
names[user.name] = user.nostrPubkey
|
||||
if (user.nostrRelays.length) {
|
||||
// append relays with key pubkey
|
||||
relays[user.nostrPubkey] = []
|
||||
for (const relay of user.nostrRelays) {
|
||||
relays[user.nostrPubkey].push(relay.nostrRelayAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relays = Object.keys(relays).length ? relays : undefined
|
||||
return res.status(200).json({ names, relays })
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Checkbox, Form, Input, SubmitButton, Select } from '../components/form'
|
||||
import { Checkbox, Form, Input, SubmitButton, Select, VariableInput } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { Alert, Button, InputGroup, Modal } from 'react-bootstrap'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
|
@ -15,6 +15,8 @@ import Info from '../components/info'
|
|||
import { CURRENCY_SYMBOLS } from '../components/price'
|
||||
import Link from 'next/link'
|
||||
import AccordianItem from '../components/accordian-item'
|
||||
import { MAX_NOSTR_RELAY_NUM } from '../lib/constants'
|
||||
import { WS_REGEXP } from '../lib/url'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||
|
||||
|
@ -23,7 +25,12 @@ const supportedCurrencies = Object.keys(CURRENCY_SYMBOLS)
|
|||
export const SettingsSchema = Yup.object({
|
||||
tipDefault: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole'),
|
||||
fiatCurrency: Yup.string().required('required').oneOf(supportedCurrencies)
|
||||
fiatCurrency: Yup.string().required('required').oneOf(supportedCurrencies),
|
||||
nostrPubkey: Yup.string().matches(/^[0-9a-fA-F]{64}$/, 'must be 64 hex chars'),
|
||||
nostrRelays: Yup.array().of(
|
||||
Yup.string().matches(WS_REGEXP, 'invalid web socket address')
|
||||
).max(MAX_NOSTR_RELAY_NUM,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`)
|
||||
})
|
||||
|
||||
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
|
||||
|
@ -72,11 +79,26 @@ export default function Settings ({ data: { settings } }) {
|
|||
hideInvoiceDesc: settings?.hideInvoiceDesc,
|
||||
hideFromTopUsers: settings?.hideFromTopUsers,
|
||||
wildWestMode: settings?.wildWestMode,
|
||||
greeterMode: settings?.greeterMode
|
||||
greeterMode: settings?.greeterMode,
|
||||
nostrPubkey: settings?.nostrPubkey || '',
|
||||
nostrRelays: settings?.nostrRelays || ['']
|
||||
}}
|
||||
schema={SettingsSchema}
|
||||
onSubmit={async ({ tipDefault, ...values }) => {
|
||||
await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } })
|
||||
onSubmit={async ({ tipDefault, nostrPubkey, nostrRelays, ...values }) => {
|
||||
if (nostrPubkey.length === 0) {
|
||||
nostrPubkey = null
|
||||
}
|
||||
|
||||
const nostrRelaysFiltered = nostrRelays?.filter(word => word.trim().length > 0)
|
||||
|
||||
await setSettings({
|
||||
variables: {
|
||||
tipDefault: Number(tipDefault),
|
||||
nostrPubkey,
|
||||
nostrRelays: nostrRelaysFiltered,
|
||||
...values
|
||||
}
|
||||
})
|
||||
setSuccess('settings saved')
|
||||
}}
|
||||
>
|
||||
|
@ -217,6 +239,27 @@ export default function Settings ({ data: { settings } }) {
|
|||
}
|
||||
name='greeterMode'
|
||||
/>
|
||||
<AccordianItem
|
||||
headerColor='var(--theme-color)'
|
||||
show={settings?.nostrPubkey}
|
||||
header={<h4 className='mb-2 text-left'>nostr <small><a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank' rel='noreferrer'>NIP-05</a></small></h4>}
|
||||
body={
|
||||
<>
|
||||
<Input
|
||||
label={<>pubkey <small className='text-muted ml-2'>optional</small></>}
|
||||
name='nostrPubkey'
|
||||
clear
|
||||
/>
|
||||
<VariableInput
|
||||
label={<>relays <small className='text-muted ml-2'>optional</small></>}
|
||||
name='nostrRelays'
|
||||
clear
|
||||
min={0}
|
||||
max={MAX_NOSTR_RELAY_NUM}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className='d-flex'>
|
||||
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "nostrPubkey" TEXT;
|
|
@ -0,0 +1,24 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "NostrRelay" (
|
||||
"addr" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("addr")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserNostrRelay" (
|
||||
"userId" INTEGER NOT NULL,
|
||||
"nostrRelayAddr" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("userId","nostrRelayAddr")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserNostrRelay" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserNostrRelay" ADD FOREIGN KEY ("nostrRelayAddr") REFERENCES "NostrRelay"("addr") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -44,9 +44,14 @@ model User {
|
|||
photoId Int?
|
||||
photo Upload? @relation(fields: [photoId], references: [id])
|
||||
|
||||
// walkthrough
|
||||
upvotePopover Boolean @default(false)
|
||||
tipPopover Boolean @default(false)
|
||||
|
||||
// nostr
|
||||
nostrPubkey String?
|
||||
nostrRelays UserNostrRelay[]
|
||||
|
||||
// referrals
|
||||
referrer User? @relation("referrals", fields: [referrerId], references: [id])
|
||||
referrerId Int?
|
||||
|
@ -84,6 +89,24 @@ model User {
|
|||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model NostrRelay {
|
||||
addr String @id
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
users UserNostrRelay[]
|
||||
}
|
||||
|
||||
model UserNostrRelay {
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
NostrRelay NostrRelay @relation(fields: [nostrRelayAddr], references: [addr])
|
||||
nostrRelayAddr String
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
|
||||
@@id([userId, nostrRelayAddr])
|
||||
}
|
||||
|
||||
model Donation {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
|
|
Loading…
Reference in New Issue