allow nip05 for users

This commit is contained in:
keyan 2023-01-06 18:53:09 -06:00
parent 7a4a24c6df
commit 2d012ba7fe
13 changed files with 183 additions and 14 deletions

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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('')} />

View File

@ -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}

View File

@ -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
}
}

View File

@ -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

View File

@ -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}))?$/

View File

@ -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'

28
pages/api/nostr/nip05.js Normal file
View File

@ -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 })
}

View File

@ -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>

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "nostrPubkey" TEXT;

View File

@ -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;

View File

@ -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")