reuse validation on server
This commit is contained in:
parent
0a0f10b290
commit
4cae1ae230
|
@ -1,4 +1,5 @@
|
|||
import { AuthenticationError, UserInputError } from 'apollo-server-micro'
|
||||
import { AuthenticationError } from 'apollo-server-micro'
|
||||
import { inviteSchema, ssValidate } from '../../lib/validate'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
|
@ -31,9 +32,7 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
if (!gift || (gift && gift < 0)) {
|
||||
throw new UserInputError('gift must be >= 0', { argumentName: 'gift' })
|
||||
}
|
||||
await ssValidate(inviteSchema, { gift, limit })
|
||||
|
||||
return await models.invite.create({
|
||||
data: { gift, limit, userId: me.id }
|
||||
|
|
|
@ -5,12 +5,13 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
|||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||
import domino from 'domino'
|
||||
import {
|
||||
BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES,
|
||||
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
import uu from 'url-unshort'
|
||||
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
|
||||
async function comments (me, models, id, sort, root) {
|
||||
let orderBy
|
||||
|
@ -602,6 +603,8 @@ export default {
|
|||
data.url = ensureProtocol(data.url)
|
||||
data.url = removeTracking(data.url)
|
||||
|
||||
await ssValidate(linkSchema, data, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} else {
|
||||
|
@ -611,6 +614,8 @@ export default {
|
|||
upsertDiscussion: async (parent, args, { me, models }) => {
|
||||
const { id, ...data } = args
|
||||
|
||||
await ssValidate(discussionSchema, data, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} else {
|
||||
|
@ -619,11 +624,8 @@ export default {
|
|||
},
|
||||
upsertBounty: async (parent, args, { me, models }) => {
|
||||
const { id, ...data } = args
|
||||
const { bounty } = data
|
||||
|
||||
if (bounty < 1000 || bounty > 1000000) {
|
||||
throw new UserInputError('invalid bounty amount', { argumentName: 'bounty' })
|
||||
}
|
||||
await ssValidate(bountySchema, data, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
|
@ -631,14 +633,21 @@ export default {
|
|||
return await createItem(parent, data, { me, models })
|
||||
}
|
||||
},
|
||||
upsertPoll: async (parent, { id, forward, boost, title, text, options }, { me, models }) => {
|
||||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
||||
const { forward, boost, title, text, options } = data
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
if (boost && boost < BOOST_MIN) {
|
||||
throw new UserInputError(`boost must be at least ${BOOST_MIN}`, { argumentName: 'boost' })
|
||||
}
|
||||
const optionCount = id
|
||||
? await models.pollOption.count({
|
||||
where: {
|
||||
itemId: Number(id)
|
||||
}
|
||||
})
|
||||
: 0
|
||||
|
||||
await ssValidate(pollSchema, data, models, optionCount)
|
||||
|
||||
let fwdUser
|
||||
if (forward) {
|
||||
|
@ -649,58 +658,40 @@ export default {
|
|||
}
|
||||
|
||||
if (id) {
|
||||
const optionCount = await models.pollOption.count({
|
||||
where: {
|
||||
itemId: Number(id)
|
||||
}
|
||||
})
|
||||
|
||||
if (options.length + optionCount > MAX_POLL_NUM_CHOICES) {
|
||||
throw new UserInputError(`total choices must be <${MAX_POLL_NUM_CHOICES}`, { argumentName: 'options' })
|
||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
if (Number(old.userId) !== Number(me?.id)) {
|
||||
throw new AuthenticationError('item does not belong to you')
|
||||
}
|
||||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6) AS "Item"`,
|
||||
Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
item.comments = []
|
||||
return item
|
||||
} else {
|
||||
if (options.length < 2 || options.length > MAX_POLL_NUM_CHOICES) {
|
||||
throw new UserInputError(`choices must be >2 and <${MAX_POLL_NUM_CHOICES}`, { argumentName: 'options' })
|
||||
}
|
||||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
item.comments = []
|
||||
return item
|
||||
}
|
||||
},
|
||||
upsertJob: async (parent, {
|
||||
id, sub, title, company, location, remote,
|
||||
text, url, maxBid, status, logo
|
||||
}, { me, models }) => {
|
||||
upsertJob: async (parent, { id, ...data }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in to create job')
|
||||
}
|
||||
const { sub, title, company, location, remote, text, url, maxBid, status, logo } = data
|
||||
|
||||
const fullSub = await models.sub.findUnique({ where: { name: sub } })
|
||||
if (!fullSub) {
|
||||
throw new UserInputError('not a valid sub', { argumentName: 'sub' })
|
||||
}
|
||||
|
||||
if (maxBid < 0) {
|
||||
throw new UserInputError('bid must be at least 0', { argumentName: 'maxBid' })
|
||||
}
|
||||
|
||||
if (!location && !remote) {
|
||||
throw new UserInputError('must specify location or remote', { argumentName: 'location' })
|
||||
}
|
||||
|
||||
location = location.toLowerCase() === 'remote' ? undefined : location
|
||||
await ssValidate(jobSchema, data, models)
|
||||
const loc = location.toLowerCase() === 'remote' ? undefined : location
|
||||
|
||||
let item
|
||||
if (id) {
|
||||
|
@ -711,23 +702,25 @@ export default {
|
|||
([item] = await serialize(models,
|
||||
models.$queryRaw(
|
||||
`${SELECT} FROM update_job($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) AS "Item"`,
|
||||
Number(id), title, url, text, Number(maxBid), company, location, remote, Number(logo), status)))
|
||||
Number(id), title, url, text, Number(maxBid), company, loc, remote, Number(logo), status)))
|
||||
} else {
|
||||
([item] = await serialize(models,
|
||||
models.$queryRaw(
|
||||
`${SELECT} FROM create_job($1, $2, $3, $4, $5, $6, $7, $8, $9) AS "Item"`,
|
||||
title, url, text, Number(me.id), Number(maxBid), company, location, remote, Number(logo))))
|
||||
title, url, text, Number(me.id), Number(maxBid), company, loc, remote, Number(logo))))
|
||||
}
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
return item
|
||||
},
|
||||
createComment: async (parent, { text, parentId }, { me, models }) => {
|
||||
return await createItem(parent, { text, parentId }, { me, models })
|
||||
createComment: async (parent, data, { me, models }) => {
|
||||
await ssValidate(commentSchema, data)
|
||||
return await createItem(parent, data, { me, models })
|
||||
},
|
||||
updateComment: async (parent, { id, text }, { me, models }) => {
|
||||
return await updateItem(parent, { id, data: { text } }, { me, models })
|
||||
updateComment: async (parent, { id, ...data }, { me, models }) => {
|
||||
await ssValidate(commentSchema, data)
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
},
|
||||
pollVote: async (parent, { id }, { me, models }) => {
|
||||
if (!me) {
|
||||
|
@ -746,9 +739,7 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
if (sats <= 0) {
|
||||
throw new UserInputError('sats must be positive', { argumentName: 'sats' })
|
||||
}
|
||||
await ssValidate(amountSchema, { amount: sats })
|
||||
|
||||
// disallow self tips
|
||||
const [item] = await models.$queryRaw(`
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { AuthenticationError } from 'apollo-server-micro'
|
||||
import { amountSchema, ssValidate } from '../../lib/validate'
|
||||
import serialize from './serial'
|
||||
|
||||
export default {
|
||||
|
@ -38,6 +39,8 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
await ssValidate(amountSchema, { amount: sats })
|
||||
|
||||
await serialize(models,
|
||||
models.$queryRaw(
|
||||
'SELECT donate($1, $2)',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AuthenticationError, UserInputError } from 'apollo-server-errors'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { msatsToSats } from '../../lib/format'
|
||||
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
|
||||
import { createMentions, getItem, SELECT, updateItem, filterClause } from './item'
|
||||
import serialize from './serial'
|
||||
|
||||
|
@ -328,21 +329,15 @@ export default {
|
|||
},
|
||||
|
||||
Mutation: {
|
||||
setName: async (parent, { name }, { me, models }) => {
|
||||
setName: async (parent, data, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
if (!/^[\w_]+$/.test(name)) {
|
||||
throw new UserInputError('only letters, numbers, and _')
|
||||
}
|
||||
|
||||
if (name.length > 32) {
|
||||
throw new UserInputError('too long')
|
||||
}
|
||||
await ssValidate(userSchema, data, models)
|
||||
|
||||
try {
|
||||
await models.user.update({ where: { id: me.id }, data: { name } })
|
||||
await models.user.update({ where: { id: me.id }, data })
|
||||
} catch (error) {
|
||||
if (error.code === 'P2002') {
|
||||
throw new UserInputError('name taken')
|
||||
|
@ -355,6 +350,8 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
await ssValidate(settingsSchema, { nostrRelays, ...data })
|
||||
|
||||
if (nostrRelays?.length) {
|
||||
const connectOrCreate = []
|
||||
for (const nr of nostrRelays) {
|
||||
|
@ -400,6 +397,8 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
await ssValidate(bioSchema, { bio })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
if (user.bioId) {
|
||||
|
@ -443,6 +442,8 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
await ssValidate(emailSchema, { email })
|
||||
|
||||
try {
|
||||
await models.user.update({
|
||||
where: { id: me.id },
|
||||
|
|
|
@ -6,6 +6,7 @@ import lnpr from 'bolt11'
|
|||
import { SELECT } from './item'
|
||||
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
|
||||
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
|
||||
import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
|
||||
|
||||
export async function getInvoice (parent, { id }, { me, models }) {
|
||||
if (!me) {
|
||||
|
@ -193,9 +194,7 @@ export default {
|
|||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
throw new UserInputError('amount must be positive', { argumentName: 'amount' })
|
||||
}
|
||||
await ssValidate(amountSchema, { amount })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
|
@ -222,6 +221,8 @@ export default {
|
|||
},
|
||||
createWithdrawl: createWithdrawal,
|
||||
sendToLnAddr: async (parent, { addr, amount, maxFee }, { me, models, lnd }) => {
|
||||
await ssValidate(lnAddrSchema, { addr, amount, maxFee })
|
||||
|
||||
const [name, domain] = addr.split('@')
|
||||
let req
|
||||
try {
|
||||
|
@ -299,6 +300,7 @@ export default {
|
|||
}
|
||||
|
||||
async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd }) {
|
||||
await ssValidate(withdrawlSchema, { invoice, maxFee })
|
||||
// decode invoice to get amount
|
||||
let decoded
|
||||
try {
|
||||
|
|
|
@ -1,35 +1,9 @@
|
|||
import AccordianItem from './accordian-item'
|
||||
import * as Yup from 'yup'
|
||||
import { Input, InputUserSuggest } from './form'
|
||||
import { InputGroup } from 'react-bootstrap'
|
||||
import { BOOST_MIN } from '../lib/constants'
|
||||
import { NAME_QUERY } from '../fragments/users'
|
||||
import Info from './info'
|
||||
|
||||
export function AdvPostSchema (client) {
|
||||
return {
|
||||
boost: Yup.number().typeError('must be a number')
|
||||
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).integer('must be whole').test({
|
||||
name: 'boost',
|
||||
test: async boost => {
|
||||
if (!boost || boost % BOOST_MIN === 0) return true
|
||||
return false
|
||||
},
|
||||
message: `must be divisble be ${BOOST_MIN}`
|
||||
}),
|
||||
forward: Yup.string()
|
||||
.test({
|
||||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return true
|
||||
const { data } = await client.query({ query: NAME_QUERY, variables: { name }, fetchPolicy: 'network-only' })
|
||||
return !data.nameAvailable
|
||||
},
|
||||
message: 'user does not exist'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function AdvPostInitial ({ forward }) {
|
||||
return {
|
||||
boost: '',
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import { InputGroup } from 'react-bootstrap'
|
||||
import { bountySchema } from '../lib/validate'
|
||||
|
||||
export function BountyForm ({
|
||||
item,
|
||||
|
@ -21,6 +20,7 @@ export function BountyForm ({
|
|||
}) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = bountySchema(client)
|
||||
const [upsertBounty] = useMutation(
|
||||
gql`
|
||||
mutation upsertBounty(
|
||||
|
@ -45,41 +45,23 @@ export function BountyForm ({
|
|||
`
|
||||
)
|
||||
|
||||
const BountySchema = Yup.object({
|
||||
title: Yup.string()
|
||||
.required('required')
|
||||
.trim()
|
||||
.max(
|
||||
MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`
|
||||
),
|
||||
bounty: Yup.number()
|
||||
.required('required')
|
||||
.min(1000, 'must be at least 1000 sats')
|
||||
.max(1000000, 'must be at most 1m sats')
|
||||
.integer('must be whole'),
|
||||
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
title: item?.title || '',
|
||||
text: item?.text || '',
|
||||
bounty: item?.bounty || 1000,
|
||||
suggest: '',
|
||||
...AdvPostInitial({ forward: item?.fwdUser?.name })
|
||||
}}
|
||||
schema={BountySchema}
|
||||
schema={schema}
|
||||
onSubmit={
|
||||
handleSubmit ||
|
||||
(async ({ boost, bounty, ...values }) => {
|
||||
const { error } = await upsertBounty({
|
||||
variables: {
|
||||
id: item?.id,
|
||||
boost: Number(boost),
|
||||
bounty: Number(bounty),
|
||||
boost: boost ? Number(boost) : undefined,
|
||||
bounty: bounty ? Number(bounty) : undefined,
|
||||
...values
|
||||
}
|
||||
})
|
||||
|
@ -94,7 +76,7 @@ export function BountyForm ({
|
|||
}
|
||||
})
|
||||
}
|
||||
storageKeyPrefix={item ? undefined : 'discussion'}
|
||||
storageKeyPrefix={item ? undefined : 'bounty'}
|
||||
>
|
||||
<Input label={titleLabel} name='title' required autoFocus clear />
|
||||
<Input
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { EditFeeButton } from './fee-button'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import Delete from './delete'
|
||||
|
||||
export const CommentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
})
|
||||
import { commentSchema } from '../lib/validate'
|
||||
|
||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||
const [updateComment] = useMutation(
|
||||
|
@ -38,7 +34,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||
initial={{
|
||||
text: comment.text
|
||||
}}
|
||||
schema={CommentSchema}
|
||||
schema={commentSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
const { error } = await updateComment({ variables: { ...values, id: comment.id } })
|
||||
if (error) {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import { ITEM_FIELDS } from '../fragments/items'
|
||||
import AccordianItem from './accordian-item'
|
||||
import Item from './item'
|
||||
import Delete from './delete'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { discussionSchema } from '../lib/validate'
|
||||
|
||||
export function DiscussionForm ({
|
||||
item, editThreshold, titleLabel = 'title',
|
||||
|
@ -20,6 +19,7 @@ export function DiscussionForm ({
|
|||
}) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = discussionSchema(client)
|
||||
// const me = useMe()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
|
@ -42,13 +42,6 @@ export function DiscussionForm ({
|
|||
fetchPolicy: 'network-only'
|
||||
})
|
||||
|
||||
const DiscussionSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim()
|
||||
.max(MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`),
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
const related = relatedData?.related?.items || []
|
||||
|
||||
// const cost = linkOrImg ? 10 : me?.freePosts ? 0 : 1
|
||||
|
@ -58,13 +51,12 @@ export function DiscussionForm ({
|
|||
initial={{
|
||||
title: item?.title || '',
|
||||
text: item?.text || '',
|
||||
suggest: '',
|
||||
...AdvPostInitial({ forward: item?.fwdUser?.name })
|
||||
}}
|
||||
schema={DiscussionSchema}
|
||||
schema={schema}
|
||||
onSubmit={handleSubmit || (async ({ boost, ...values }) => {
|
||||
const { error } = await upsertDiscussion({
|
||||
variables: { id: item?.id, boost: Number(boost), ...values }
|
||||
variables: { id: item?.id, boost: boost ? Number(boost) : undefined, ...values }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { Button, InputGroup } from 'react-bootstrap'
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, Input, SubmitButton } from './form'
|
||||
import { useMe } from './me'
|
||||
import UpBolt from '../svgs/bolt.svg'
|
||||
|
||||
export const ActSchema = Yup.object({
|
||||
amount: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole')
|
||||
})
|
||||
import { amountSchema } from '../lib/validate'
|
||||
|
||||
export default function ItemAct ({ onClose, itemId, act, strike }) {
|
||||
const inputRef = useRef(null)
|
||||
|
@ -25,7 +20,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
amount: me?.tipDefault,
|
||||
default: false
|
||||
}}
|
||||
schema={ActSchema}
|
||||
schema={amountSchema}
|
||||
onSubmit={async ({ amount }) => {
|
||||
await act({
|
||||
variables: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { InputGroup, Form as BForm, Col, Image } from 'react-bootstrap'
|
||||
import * as Yup from 'yup'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Info from './info'
|
||||
import AccordianItem from './accordian-item'
|
||||
|
@ -13,31 +12,14 @@ import { usePrice } from './price'
|
|||
import Avatar from './avatar'
|
||||
import BootstrapForm from 'react-bootstrap/Form'
|
||||
import Alert from 'react-bootstrap/Alert'
|
||||
import { useMe } from './me'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
|
||||
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
||||
return this.test({
|
||||
name: 'or',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (Array.isArray(schemas) && schemas.length > 1) {
|
||||
const resee = schemas.map(schema => schema.isValidSync(value))
|
||||
return resee.some(res => res)
|
||||
} else {
|
||||
throw new TypeError('Schemas is not correct array schema')
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
import { jobSchema } from '../lib/validate'
|
||||
|
||||
function satsMin2Mo (minute) {
|
||||
return minute * 30 * 24 * 60
|
||||
}
|
||||
|
||||
function PriceHint ({ monthly }) {
|
||||
const me = useMe()
|
||||
const { price, fiatSymbol } = usePrice()
|
||||
|
||||
if (!price || !monthly) {
|
||||
|
@ -65,26 +47,6 @@ export default function JobForm ({ item, sub }) {
|
|||
}`
|
||||
)
|
||||
|
||||
const JobSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim(),
|
||||
company: Yup.string().required('required').trim(),
|
||||
text: Yup.string().required('required').trim(),
|
||||
url: Yup.string()
|
||||
.or([Yup.string().email(), Yup.string().url()], 'invalid url or email')
|
||||
.required('required'),
|
||||
maxBid: Yup.number().typeError('must be a number')
|
||||
.integer('must be whole').min(0, 'must be positive')
|
||||
.required('required'),
|
||||
location: Yup.string().test(
|
||||
'no-remote',
|
||||
"don't write remote, just check the box",
|
||||
v => !v?.match(/\bremote\b/gi))
|
||||
.when('remote', {
|
||||
is: (value) => !value,
|
||||
then: Yup.string().required('required').trim()
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
|
@ -100,7 +62,7 @@ export default function JobForm ({ item, sub }) {
|
|||
stop: false,
|
||||
start: false
|
||||
}}
|
||||
schema={JobSchema}
|
||||
schema={jobSchema}
|
||||
storageKeyPrefix={storageKeyPrefix}
|
||||
onSubmit={(async ({ maxBid, stop, start, ...values }) => {
|
||||
let status
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import { Form, Input, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import { ITEM_FIELDS } from '../fragments/items'
|
||||
import Item from './item'
|
||||
import AccordianItem from './accordian-item'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import { URL_REGEXP } from '../lib/url'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import Delete from './delete'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { linkSchema } from '../lib/validate'
|
||||
|
||||
export function LinkForm ({ item, editThreshold }) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = linkSchema(client)
|
||||
|
||||
const [getPageTitleAndUnshorted, { data }] = useLazyQuery(gql`
|
||||
query PageTitleAndUnshorted($url: String!) {
|
||||
|
@ -71,14 +70,6 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
}`
|
||||
)
|
||||
|
||||
const LinkSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim()
|
||||
.max(MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`),
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required'),
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
|
@ -86,10 +77,10 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
url: item?.url || '',
|
||||
...AdvPostInitial({ forward: item?.fwdUser?.name })
|
||||
}}
|
||||
schema={LinkSchema}
|
||||
schema={schema}
|
||||
onSubmit={async ({ boost, title, ...values }) => {
|
||||
const { error } = await upsertLink({
|
||||
variables: { id: item?.id, boost: Number(boost), title: title.trim(), ...values }
|
||||
variables: { id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), ...values }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { signIn } from 'next-auth/client'
|
||||
import styles from './login.module.css'
|
||||
import { Form, Input, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { useState } from 'react'
|
||||
import Alert from 'react-bootstrap/Alert'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LightningAuthWithExplainer, SlashtagsAuth } from './lightning-auth'
|
||||
import LoginButton from './login-button'
|
||||
|
||||
export const EmailSchema = Yup.object({
|
||||
email: Yup.string().email('email is no good').required('required')
|
||||
})
|
||||
import { emailSchema } from '../lib/validate'
|
||||
|
||||
export function EmailLoginForm ({ text, callbackUrl }) {
|
||||
return (
|
||||
|
@ -18,7 +14,7 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
|||
initial={{
|
||||
email: ''
|
||||
}}
|
||||
schema={EmailSchema}
|
||||
schema={emailSchema}
|
||||
onSubmit={async ({ email }) => {
|
||||
signIn('email', { email, callbackUrl })
|
||||
}}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton, VariableInput } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import { MAX_TITLE_LENGTH, MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES } from '../lib/constants'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import { MAX_POLL_NUM_CHOICES } from '../lib/constants'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import Delete from './delete'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { pollSchema } from '../lib/validate'
|
||||
|
||||
export function PollForm ({ item, editThreshold }) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = pollSchema(client)
|
||||
|
||||
const [upsertPoll] = useMutation(
|
||||
gql`
|
||||
|
@ -25,19 +26,6 @@ export function PollForm ({ item, editThreshold }) {
|
|||
}`
|
||||
)
|
||||
|
||||
const PollSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim()
|
||||
.max(MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`),
|
||||
options: Yup.array().of(
|
||||
Yup.string().trim().test('my-test', 'required', function (value) {
|
||||
return (this.path !== 'options[0]' && this.path !== 'options[1]') || value
|
||||
}).max(MAX_POLL_CHOICE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`)
|
||||
),
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
const initialOptions = item?.poll?.options.map(i => i.option)
|
||||
|
||||
return (
|
||||
|
@ -48,13 +36,13 @@ export function PollForm ({ item, editThreshold }) {
|
|||
options: initialOptions || ['', ''],
|
||||
...AdvPostInitial({ forward: item?.fwdUser?.name })
|
||||
}}
|
||||
schema={PollSchema}
|
||||
schema={schema}
|
||||
onSubmit={async ({ boost, title, options, ...values }) => {
|
||||
const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
|
||||
const { error } = await upsertPoll({
|
||||
variables: {
|
||||
id: item?.id,
|
||||
boost: Number(boost),
|
||||
boost: boost ? Number(boost) : undefined,
|
||||
title: title.trim(),
|
||||
options: optionsFiltered,
|
||||
...values
|
||||
|
|
|
@ -4,22 +4,13 @@ import { Button } from 'react-bootstrap'
|
|||
import { fixedDecimal } from '../lib/format'
|
||||
import { useMe } from './me'
|
||||
import { PRICE } from '../fragments/price'
|
||||
import { CURRENCY_SYMBOLS } from '../lib/currency'
|
||||
|
||||
export const PriceContext = React.createContext({
|
||||
price: null,
|
||||
fiatSymbol: null
|
||||
})
|
||||
|
||||
export const CURRENCY_SYMBOLS = {
|
||||
AUD: '$',
|
||||
CAD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
USD: '$',
|
||||
NZD: '$',
|
||||
ZAR: 'R '
|
||||
}
|
||||
|
||||
export function usePrice () {
|
||||
return useContext(PriceContext)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import { COMMENTS } from '../fragments/comments'
|
||||
|
@ -9,10 +8,7 @@ import { useEffect, useState } from 'react'
|
|||
import Link from 'next/link'
|
||||
import FeeButton from './fee-button'
|
||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||
|
||||
export const CommentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
})
|
||||
import { commentSchema } from '../lib/validate'
|
||||
|
||||
export function ReplyOnAnotherPage ({ parentId }) {
|
||||
return (
|
||||
|
@ -98,7 +94,7 @@ export default function Reply ({ item, onSuccess, replyOpen, children }) {
|
|||
initial={{
|
||||
text: ''
|
||||
}}
|
||||
schema={CommentSchema}
|
||||
schema={commentSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId } })
|
||||
if (error) {
|
||||
|
|
|
@ -4,23 +4,24 @@ import { useRouter } from 'next/router'
|
|||
import Nav from 'react-bootstrap/Nav'
|
||||
import { useState } from 'react'
|
||||
import { Form, Input, SubmitButton } from './form'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import styles from './user-header.module.css'
|
||||
import { useMe } from './me'
|
||||
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
|
||||
import { NAME_MUTATION } from '../fragments/users'
|
||||
import QRCode from 'qrcode.react'
|
||||
import LightningIcon from '../svgs/bolt.svg'
|
||||
import ModalButton from './modal-button'
|
||||
import { encodeLNUrl } from '../lib/lnurl'
|
||||
import Avatar from './avatar'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
import { userSchema } from '../lib/validate'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const [editting, setEditting] = useState(false)
|
||||
const me = useMe()
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const schema = userSchema(client)
|
||||
const [setName] = useMutation(NAME_MUTATION)
|
||||
|
||||
const [setPhoto] = useMutation(
|
||||
|
@ -44,22 +45,6 @@ export default function UserHeader ({ user }) {
|
|||
const isMe = me?.name === user.name
|
||||
const Satistics = () => <div className={`mb-2 ml-0 ml-sm-1 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
|
||||
|
||||
const UserSchema = Yup.object({
|
||||
name: Yup.string()
|
||||
.required('required')
|
||||
.matches(/^[\w_]+$/, 'only letters, numbers, and _')
|
||||
.max(32, 'too long')
|
||||
.test({
|
||||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return false
|
||||
const { data } = await client.query({ query: NAME_QUERY, variables: { name }, fetchPolicy: 'network-only' })
|
||||
return data.nameAvailable
|
||||
},
|
||||
message: 'taken'
|
||||
})
|
||||
})
|
||||
|
||||
const lnurlp = encodeLNUrl(new URL(`https://stacker.news/.well-known/lnurlp/${user.name}`))
|
||||
|
||||
return (
|
||||
|
@ -83,7 +68,7 @@ export default function UserHeader ({ user }) {
|
|||
{editting
|
||||
? (
|
||||
<Form
|
||||
schema={UserSchema}
|
||||
schema={schema}
|
||||
initial={{
|
||||
name: user.name
|
||||
}}
|
||||
|
|
|
@ -16,8 +16,7 @@ import {
|
|||
DROP_COMMAND
|
||||
} from 'lexical'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { ensureProtocol, URL_REGEXP } from '../../lib/url'
|
||||
import { ensureProtocol } from '../../lib/url'
|
||||
|
||||
import {
|
||||
$createImageNode,
|
||||
|
@ -26,16 +25,13 @@ import {
|
|||
} from '../nodes/image'
|
||||
import { Form, Input, SubmitButton } from '../../components/form'
|
||||
import styles from '../styles.module.css'
|
||||
import { urlSchema } from '../../lib/validate'
|
||||
|
||||
const getDOMSelection = (targetWindow) =>
|
||||
typeof window !== 'undefined' ? (targetWindow || window).getSelection() : null
|
||||
|
||||
export const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE_COMMAND')
|
||||
|
||||
const LinkSchema = Yup.object({
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required')
|
||||
})
|
||||
|
||||
export function ImageInsertModal ({ onClose, editor }) {
|
||||
const inputRef = useRef(null)
|
||||
|
||||
|
@ -49,7 +45,7 @@ export function ImageInsertModal ({ onClose, editor }) {
|
|||
url: '',
|
||||
alt: ''
|
||||
}}
|
||||
schema={LinkSchema}
|
||||
schema={urlSchema}
|
||||
onSubmit={async ({ alt, url }) => {
|
||||
editor.dispatchCommand(INSERT_IMAGE_COMMAND, { src: ensureProtocol(url), altText: alt })
|
||||
onClose()
|
||||
|
|
|
@ -4,10 +4,10 @@ import { $wrapNodeInElement, mergeRegister } from '@lexical/utils'
|
|||
import { $createLinkNode, $isLinkNode } from '@lexical/link'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import React, { useState, useCallback, useContext, useRef, useEffect } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, Input, SubmitButton } from '../../components/form'
|
||||
import { ensureProtocol, URL_REGEXP } from '../../lib/url'
|
||||
import { ensureProtocol } from '../../lib/url'
|
||||
import { getSelectedNode } from '../utils/selected-node'
|
||||
import { namedUrlSchema } from '../../lib/validate'
|
||||
|
||||
export const INSERT_LINK_COMMAND = createCommand('INSERT_LINK_COMMAND')
|
||||
|
||||
|
@ -68,11 +68,6 @@ export function useLinkInsert () {
|
|||
return { link, setLink }
|
||||
}
|
||||
|
||||
const LinkSchema = Yup.object({
|
||||
text: Yup.string().required('required'),
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required')
|
||||
})
|
||||
|
||||
export function LinkInsertModal () {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { link, setLink } = useLinkInsert()
|
||||
|
@ -106,7 +101,7 @@ export function LinkInsertModal () {
|
|||
text: link?.text,
|
||||
url: link?.url
|
||||
}}
|
||||
schema={LinkSchema}
|
||||
schema={namedUrlSchema}
|
||||
onSubmit={async ({ text, url }) => {
|
||||
editor.dispatchCommand(INSERT_LINK_COMMAND, { url: ensureProtocol(url), text })
|
||||
await setLink(null)
|
||||
|
|
|
@ -14,6 +14,6 @@ export const MAX_TITLE_LENGTH = 80
|
|||
export const MAX_POLL_CHOICE_LENGTH = 30
|
||||
export const ITEM_SPAM_INTERVAL = '10m'
|
||||
export const MAX_POLL_NUM_CHOICES = 10
|
||||
export const MIN_POLL_NUM_CHOICES = 2
|
||||
export const ITEM_FILTER_THRESHOLD = 1.2
|
||||
export const DONT_LIKE_THIS_COST = 1
|
||||
export const MAX_NOSTR_RELAY_NUM = 20
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export const CURRENCY_SYMBOLS = {
|
||||
AUD: '$',
|
||||
CAD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
USD: '$',
|
||||
NZD: '$',
|
||||
ZAR: 'R '
|
||||
}
|
||||
|
||||
export const SUPPORTED_CURRENCIES = Object.keys(CURRENCY_SYMBOLS)
|
|
@ -0,0 +1,3 @@
|
|||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||
export const NOSTR_MAX_RELAY_NUM = 20
|
|
@ -0,0 +1,223 @@
|
|||
import * as Yup from 'yup'
|
||||
import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES } from './constants'
|
||||
import { NAME_QUERY } from '../fragments/users'
|
||||
import { URL_REGEXP, WS_REGEXP } from './url'
|
||||
import { SUPPORTED_CURRENCIES } from './currency'
|
||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||
|
||||
export async function ssValidate (schema, data, ...args) {
|
||||
try {
|
||||
if (typeof schema === 'function') {
|
||||
await schema(...args).validate(data)
|
||||
} else {
|
||||
await schema.validate(data)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Yup.ValidationError) {
|
||||
throw new Error(`${e.path}: ${e.message}`)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
||||
return this.test({
|
||||
name: 'or',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (Array.isArray(schemas) && schemas.length > 1) {
|
||||
const resee = schemas.map(schema => schema.isValidSync(value))
|
||||
return resee.some(res => res)
|
||||
} else {
|
||||
throw new TypeError('Schemas is not correct array schema')
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
const titleValidator = Yup.string().required('required').trim().max(
|
||||
MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many`
|
||||
)
|
||||
|
||||
const intValidator = Yup.number().typeError('must be a number').integer('must be whole')
|
||||
|
||||
async function usernameExists (client, name) {
|
||||
if (!client) {
|
||||
throw new Error('cannot check for user')
|
||||
}
|
||||
// apollo client
|
||||
if (client.query) {
|
||||
const { data } = await client.query({ query: NAME_QUERY, variables: { name }, fetchPolicy: 'network-only' })
|
||||
return !data.nameAvailable
|
||||
}
|
||||
|
||||
// prisma client
|
||||
const user = await client.user.findUnique({ where: { name } })
|
||||
return !!user
|
||||
}
|
||||
|
||||
// not sure how to use this on server ...
|
||||
export function advPostSchemaMembers (client) {
|
||||
return {
|
||||
boost: intValidator
|
||||
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({
|
||||
name: 'boost',
|
||||
test: async boost => {
|
||||
if (!boost || boost % BOOST_MIN === 0) return true
|
||||
return false
|
||||
},
|
||||
message: `must be divisble be ${BOOST_MIN}`
|
||||
}),
|
||||
forward: Yup.string()
|
||||
.test({
|
||||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return true
|
||||
return await usernameExists(client, name)
|
||||
},
|
||||
message: 'user does not exist'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function bountySchema (client) {
|
||||
return Yup.object({
|
||||
title: titleValidator,
|
||||
bounty: intValidator
|
||||
.min(1000, 'must be at least 1000')
|
||||
.max(1000000, 'must be at most 1m'),
|
||||
...advPostSchemaMembers(client)
|
||||
})
|
||||
}
|
||||
|
||||
export function discussionSchema (client) {
|
||||
return Yup.object({
|
||||
title: titleValidator,
|
||||
...advPostSchemaMembers(client)
|
||||
})
|
||||
}
|
||||
|
||||
export function linkSchema (client) {
|
||||
return Yup.object({
|
||||
title: titleValidator,
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required'),
|
||||
...advPostSchemaMembers(client)
|
||||
})
|
||||
}
|
||||
|
||||
export function pollSchema (client, numExistingChoices) {
|
||||
return Yup.object({
|
||||
title: titleValidator,
|
||||
options: Yup.array().of(
|
||||
Yup.string().trim().test('my-test', 'required', function (value) {
|
||||
console.log(this.path)
|
||||
return (this.path !== 'options[0]' && this.path !== 'options[1]') || value
|
||||
}).max(MAX_POLL_CHOICE_LENGTH,
|
||||
({ max, value }) => `${Math.abs(max - value.length)} too many characters`
|
||||
)
|
||||
).test({
|
||||
message: `at most ${MAX_POLL_NUM_CHOICES} choices`,
|
||||
test: arr => arr.length <= MAX_POLL_NUM_CHOICES - numExistingChoices
|
||||
}).test({
|
||||
message: `at least ${MIN_POLL_NUM_CHOICES} choices required`,
|
||||
test: arr => arr.length >= MIN_POLL_NUM_CHOICES - numExistingChoices
|
||||
}),
|
||||
...advPostSchemaMembers(client)
|
||||
})
|
||||
}
|
||||
|
||||
export function userSchema (client) {
|
||||
return Yup.object({
|
||||
name: Yup.string()
|
||||
.required('required')
|
||||
.matches(/^[\w_]+$/, 'only letters, numbers, and _')
|
||||
.max(32, 'too long')
|
||||
.test({
|
||||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return false
|
||||
return !(await usernameExists(client, name))
|
||||
},
|
||||
message: 'taken'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const commentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
})
|
||||
|
||||
export const jobSchema = Yup.object({
|
||||
title: titleValidator,
|
||||
company: Yup.string().required('required').trim(),
|
||||
text: Yup.string().required('required').trim(),
|
||||
url: Yup.string()
|
||||
.or([Yup.string().email(), Yup.string().url()], 'invalid url or email')
|
||||
.required('required'),
|
||||
maxBid: intValidator.min(0, 'must be at least 0').required('required'),
|
||||
location: Yup.string().test(
|
||||
'no-remote',
|
||||
"don't write remote, just check the box",
|
||||
v => !v?.match(/\bremote\b/gi))
|
||||
.when('remote', {
|
||||
is: (value) => !value,
|
||||
then: Yup.string().required('required').trim()
|
||||
})
|
||||
})
|
||||
|
||||
export const emailSchema = Yup.object({
|
||||
email: Yup.string().email('email is no good').required('required')
|
||||
})
|
||||
|
||||
export const urlSchema = Yup.object({
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required')
|
||||
})
|
||||
|
||||
export const namedUrlSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim(),
|
||||
url: Yup.string().matches(URL_REGEXP, 'invalid url').required('required')
|
||||
})
|
||||
|
||||
export const amountSchema = Yup.object({
|
||||
amount: intValidator.required('required').positive('must be positive')
|
||||
})
|
||||
|
||||
export const settingsSchema = Yup.object({
|
||||
tipDefault: intValidator.required('required').positive('must be positive'),
|
||||
fiatCurrency: Yup.string().required('required').oneOf(SUPPORTED_CURRENCIES),
|
||||
nostrPubkey: Yup.string()
|
||||
.or([
|
||||
Yup.string().matches(NOSTR_PUBKEY_HEX, 'must be 64 hex chars'),
|
||||
Yup.string().matches(NOSTR_PUBKEY_BECH32, 'invalid bech32 encoding')], 'invalid pubkey'),
|
||||
nostrRelays: Yup.array().of(
|
||||
Yup.string().matches(WS_REGEXP, 'invalid web socket address')
|
||||
).max(NOSTR_MAX_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'
|
||||
export const lastAuthRemovalSchema = Yup.object({
|
||||
warning: Yup.string().matches(warningMessage, 'does not match').required('required')
|
||||
})
|
||||
|
||||
export const withdrawlSchema = Yup.object({
|
||||
invoice: Yup.string().required('required').trim(),
|
||||
maxFee: intValidator.required('required').min(0, 'must be at least 0')
|
||||
})
|
||||
|
||||
export const lnAddrSchema = Yup.object({
|
||||
addr: Yup.string().email('address is no good').required('required'),
|
||||
amount: intValidator.required('required').positive('must be positive'),
|
||||
maxFee: intValidator.required('required').min(0, 'must be at least 0')
|
||||
})
|
||||
|
||||
export const bioSchema = Yup.object({
|
||||
bio: Yup.string().required('required').trim()
|
||||
})
|
||||
|
||||
export const inviteSchema = Yup.object({
|
||||
gift: intValidator.positive('must be greater than 0').required('required'),
|
||||
limit: intValidator.positive('must be positive')
|
||||
})
|
|
@ -6,7 +6,6 @@ import { Button } from 'react-bootstrap'
|
|||
import styles from '../../styles/user.module.css'
|
||||
import { useState } from 'react'
|
||||
import ItemFull from '../../components/item-full'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { useMe } from '../../components/me'
|
||||
|
@ -14,14 +13,11 @@ import { USER_FULL } from '../../fragments/users'
|
|||
import { ITEM_FIELDS } from '../../fragments/items'
|
||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||
import FeeButton, { EditFeeButton } from '../../components/fee-button'
|
||||
import { bioSchema } from '../../lib/validate'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
|
||||
data => !data.user)
|
||||
|
||||
const BioSchema = Yup.object({
|
||||
bio: Yup.string().required('required').trim()
|
||||
})
|
||||
|
||||
export function BioForm ({ handleSuccess, bio }) {
|
||||
const [upsertBio] = useMutation(
|
||||
gql`
|
||||
|
@ -54,7 +50,7 @@ export function BioForm ({ handleSuccess, bio }) {
|
|||
initial={{
|
||||
bio: bio?.text || ''
|
||||
}}
|
||||
schema={BioSchema}
|
||||
schema={bioSchema}
|
||||
onSubmit={async values => {
|
||||
const { error } = await upsertBio({ variables: values })
|
||||
if (error) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Layout from '../../components/layout'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, Input, SubmitButton } from '../../components/form'
|
||||
import { InputGroup } from 'react-bootstrap'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
|
@ -7,13 +6,7 @@ import { INVITE_FIELDS } from '../../fragments/invites'
|
|||
import AccordianItem from '../../components/accordian-item'
|
||||
import styles from '../../styles/invites.module.css'
|
||||
import Invite from '../../components/invite'
|
||||
|
||||
export const InviteSchema = Yup.object({
|
||||
gift: Yup.number().typeError('must be a number')
|
||||
.min(0, 'must be positive').integer('must be whole').required(),
|
||||
limit: Yup.number().typeError('must be a number')
|
||||
.positive('must be positive').integer('must be whole')
|
||||
})
|
||||
import { inviteSchema } from '../../lib/validate'
|
||||
|
||||
function InviteForm () {
|
||||
const [createInvite] = useMutation(
|
||||
|
@ -46,7 +39,7 @@ function InviteForm () {
|
|||
gift: 100,
|
||||
limit: undefined
|
||||
}}
|
||||
schema={InviteSchema}
|
||||
schema={inviteSchema}
|
||||
onSubmit={async ({ limit, gift }) => {
|
||||
const { error } = await createInvite({
|
||||
variables: {
|
||||
|
|
|
@ -5,9 +5,9 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'
|
|||
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||
import { Form, Input, SubmitButton } from '../components/form'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import * as Yup from 'yup'
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import Link from 'next/link'
|
||||
import { amountSchema } from '../lib/validate'
|
||||
|
||||
const REWARDS = gql`
|
||||
{
|
||||
|
@ -80,11 +80,6 @@ function GrowthPieChart ({ data }) {
|
|||
)
|
||||
}
|
||||
|
||||
export const DonateSchema = Yup.object({
|
||||
amount: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole')
|
||||
})
|
||||
|
||||
export function DonateButton () {
|
||||
const [show, setShow] = useState(false)
|
||||
const inputRef = useRef(null)
|
||||
|
@ -113,7 +108,7 @@ export function DonateButton () {
|
|||
initial={{
|
||||
amount: 1000
|
||||
}}
|
||||
schema={DonateSchema}
|
||||
schema={amountSchema}
|
||||
onSubmit={async ({ amount }) => {
|
||||
await donateToRewards({
|
||||
variables: {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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'
|
||||
import { useState } from 'react'
|
||||
|
@ -12,55 +11,15 @@ import { LightningAuth, SlashtagsAuth } from '../components/lightning-auth'
|
|||
import { SETTINGS, SET_SETTINGS } from '../fragments/users'
|
||||
import { useRouter } from 'next/router'
|
||||
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'
|
||||
import { bech32 } from 'bech32'
|
||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32 } from '../lib/nostr'
|
||||
import { emailSchema, lastAuthRemovalSchema, settingsSchema } from '../lib/validate'
|
||||
import { SUPPORTED_CURRENCIES } from '../lib/currency'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||
|
||||
const supportedCurrencies = Object.keys(CURRENCY_SYMBOLS)
|
||||
const HEX64 = /^[0-9a-fA-F]{64}$/
|
||||
const NPUB = /^npub1[02-9ac-hj-np-z]+$/
|
||||
|
||||
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
||||
return this.test({
|
||||
name: 'or',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (Array.isArray(schemas) && schemas.length > 1) {
|
||||
const resee = schemas.map(schema => schema.isValidSync(value))
|
||||
return resee.some(res => res)
|
||||
} else {
|
||||
throw new TypeError('Schemas is not correct array schema')
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
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),
|
||||
nostrPubkey: Yup.string()
|
||||
.or([
|
||||
Yup.string().matches(HEX64, 'must be 64 hex chars'),
|
||||
Yup.string().matches(NPUB, 'invalid bech32 encoding')], 'invalid pubkey'),
|
||||
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'
|
||||
|
||||
export const WarningSchema = Yup.object({
|
||||
warning: Yup.string().matches(warningMessage, 'does not match').required('required')
|
||||
})
|
||||
|
||||
function bech32encode (hexString) {
|
||||
return bech32.encode('npub', bech32.toWords(Buffer.from(hexString, 'hex')))
|
||||
}
|
||||
|
@ -110,12 +69,12 @@ export default function Settings ({ data: { settings } }) {
|
|||
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
|
||||
nostrRelays: settings?.nostrRelays?.length ? settings?.nostrRelays : ['']
|
||||
}}
|
||||
schema={SettingsSchema}
|
||||
schema={settingsSchema}
|
||||
onSubmit={async ({ tipDefault, nostrPubkey, nostrRelays, ...values }) => {
|
||||
if (nostrPubkey.length === 0) {
|
||||
nostrPubkey = null
|
||||
} else {
|
||||
if (NPUB.test(nostrPubkey)) {
|
||||
if (NOSTR_PUBKEY_BECH32.test(nostrPubkey)) {
|
||||
const { words } = bech32.decode(nostrPubkey)
|
||||
nostrPubkey = Buffer.from(bech32.fromWords(words)).toString('hex')
|
||||
}
|
||||
|
@ -180,7 +139,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
label='fiat currency'
|
||||
name='fiatCurrency'
|
||||
size='sm'
|
||||
items={supportedCurrencies}
|
||||
items={SUPPORTED_CURRENCIES}
|
||||
required
|
||||
/>
|
||||
<div className='form-label'>notify me when ...</div>
|
||||
|
@ -292,7 +251,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
name='nostrRelays'
|
||||
clear
|
||||
min={0}
|
||||
max={MAX_NOSTR_RELAY_NUM}
|
||||
max={NOSTR_MAX_RELAY_NUM}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
@ -367,7 +326,7 @@ function AuthMethods ({ methods }) {
|
|||
initial={{
|
||||
warning: ''
|
||||
}}
|
||||
schema={WarningSchema}
|
||||
schema={lastAuthRemovalSchema}
|
||||
onSubmit={async () => {
|
||||
await unlinkAuth({ variables: { authType: obstacle } })
|
||||
router.push('/settings')
|
||||
|
@ -459,10 +418,6 @@ function AuthMethods ({ methods }) {
|
|||
)
|
||||
}
|
||||
|
||||
export const EmailSchema = Yup.object({
|
||||
email: Yup.string().email('email is no good').required('required')
|
||||
})
|
||||
|
||||
export function EmailLinkForm ({ callbackUrl }) {
|
||||
const [linkUnverifiedEmail] = useMutation(
|
||||
gql`
|
||||
|
@ -476,7 +431,7 @@ export function EmailLinkForm ({ callbackUrl }) {
|
|||
initial={{
|
||||
email: ''
|
||||
}}
|
||||
schema={EmailSchema}
|
||||
schema={emailSchema}
|
||||
onSubmit={async ({ email }) => {
|
||||
// add email to user's account
|
||||
// then call signIn
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useRouter } from 'next/router'
|
|||
import { Form, Input, SubmitButton } from '../components/form'
|
||||
import Link from 'next/link'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import Qr, { QrSkeleton } from '../components/qr'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
|
@ -14,6 +13,7 @@ import { requestProvider } from 'webln'
|
|||
import { Alert } from 'react-bootstrap'
|
||||
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
|
||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps()
|
||||
|
||||
|
@ -74,11 +74,6 @@ export function WalletForm () {
|
|||
}
|
||||
}
|
||||
|
||||
export const FundSchema = Yup.object({
|
||||
amount: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole')
|
||||
})
|
||||
|
||||
export function FundForm () {
|
||||
const me = useMe()
|
||||
const [showAlert, setShowAlert] = useState(true)
|
||||
|
@ -115,7 +110,7 @@ export function FundForm () {
|
|||
amount: 1000
|
||||
}}
|
||||
initialError={error?.toString()}
|
||||
schema={FundSchema}
|
||||
schema={amountSchema}
|
||||
onSubmit={async ({ amount }) => {
|
||||
const { data } = await createInvoice({ variables: { amount: Number(amount) } })
|
||||
router.push(`/invoices/${data.createInvoice.id}`)
|
||||
|
@ -135,12 +130,6 @@ export function FundForm () {
|
|||
)
|
||||
}
|
||||
|
||||
export const WithdrawlSchema = Yup.object({
|
||||
invoice: Yup.string().required('required'),
|
||||
maxFee: Yup.number().typeError('must be a number').required('required')
|
||||
.min(0, 'must be positive').integer('must be whole')
|
||||
})
|
||||
|
||||
const MAX_FEE_DEFAULT = 10
|
||||
|
||||
export function WithdrawlForm () {
|
||||
|
@ -179,7 +168,7 @@ export function WithdrawlForm () {
|
|||
maxFee: MAX_FEE_DEFAULT
|
||||
}}
|
||||
initialError={error ? error.toString() : undefined}
|
||||
schema={WithdrawlSchema}
|
||||
schema={withdrawlSchema}
|
||||
onSubmit={async ({ invoice, maxFee }) => {
|
||||
const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } })
|
||||
router.push(`/withdrawals/${data.createWithdrawl.id}`)
|
||||
|
@ -252,14 +241,6 @@ export function LnWithdrawal () {
|
|||
return <LnQRWith {...data.createWith} />
|
||||
}
|
||||
|
||||
export const LnAddrSchema = Yup.object({
|
||||
// addr: Yup.string().email('address is no good').required('required'),
|
||||
amount: Yup.number().typeError('must be a number').required('required')
|
||||
.positive('must be positive').integer('must be whole'),
|
||||
maxFee: Yup.number().typeError('must be a number').required('required')
|
||||
.min(0, 'must be positive').integer('must be whole')
|
||||
})
|
||||
|
||||
export function LnAddrWithdrawal () {
|
||||
const router = useRouter()
|
||||
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
||||
|
@ -277,7 +258,7 @@ export function LnAddrWithdrawal () {
|
|||
amount: 1,
|
||||
maxFee: 10
|
||||
}}
|
||||
schema={LnAddrSchema}
|
||||
schema={lnAddrSchema}
|
||||
initialError={error ? error.toString() : undefined}
|
||||
onSubmit={async ({ addr, amount, maxFee }) => {
|
||||
const { data } = await sendToLnAddr({ variables: { addr, amount: Number(amount), maxFee: Number(maxFee) } })
|
||||
|
|
Loading…
Reference in New Issue