From 1a6dc879a21c28ed3bef13b9005cb31eda2b7aa9 Mon Sep 17 00:00:00 2001 From: SatsAllDay <128755788+SatsAllDay@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:56:59 -0400 Subject: [PATCH] Dependency inject `me` into post validation schemas to enforce no forwarding posts to self (#485) Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> --- api/resolvers/item.js | 8 +++---- components/bounty-form.js | 4 +++- components/discussion-form.js | 5 ++-- components/link-form.js | 4 +++- components/poll-form.js | 4 +++- lib/validate.js | 43 ++++++++++++++++++++--------------- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 270f5d1d..4f51c67c 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -621,7 +621,7 @@ export default { return await models.item.update({ where: { id: Number(id) }, data }) }, upsertLink: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => { - await ssValidate(linkSchema, item, models) + await ssValidate(linkSchema, item, models, me) if (id) { return await updateItem(parent, { id, ...item }, { me, models }) @@ -630,7 +630,7 @@ export default { } }, upsertDiscussion: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => { - await ssValidate(discussionSchema, item, models) + await ssValidate(discussionSchema, item, models, me) if (id) { return await updateItem(parent, { id, ...item }, { me, models }) @@ -639,7 +639,7 @@ export default { } }, upsertBounty: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => { - await ssValidate(bountySchema, item, models) + await ssValidate(bountySchema, item, models, me) if (id) { return await updateItem(parent, { id, ...item }, { me, models }) @@ -656,7 +656,7 @@ export default { }) : 0 - await ssValidate(pollSchema, item, models, optionCount) + await ssValidate(pollSchema, item, models, me, optionCount) if (id) { return await updateItem(parent, { id, ...item }, { me, models }) diff --git a/components/bounty-form.js b/components/bounty-form.js index 41c8da17..bff28921 100644 --- a/components/bounty-form.js +++ b/components/bounty-form.js @@ -11,6 +11,7 @@ import CancelButton from './cancel-button' import { useCallback } from 'react' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' +import { useMe } from './me' export function BountyForm ({ item, @@ -25,7 +26,8 @@ export function BountyForm ({ }) { const router = useRouter() const client = useApolloClient() - const schema = bountySchema(client) + const me = useMe() + const schema = bountySchema(client, me) const [upsertBounty] = useMutation( gql` mutation upsertBounty( diff --git a/components/discussion-form.js b/components/discussion-form.js index 00b5ec79..72f57e27 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -15,6 +15,7 @@ import CancelButton from './cancel-button' import { useCallback } from 'react' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' +import { useMe } from './me' export function DiscussionForm ({ item, sub, editThreshold, titleLabel = 'title', @@ -23,11 +24,11 @@ export function DiscussionForm ({ }) { const router = useRouter() const client = useApolloClient() - const schema = discussionSchema(client) + const me = useMe() + const schema = discussionSchema(client, me) // if Web Share Target API was used const shareTitle = router.query.title - // const me = useMe() const [upsertDiscussion] = useMutation( gql` mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: [ItemForwardInput], $hash: String, $hmac: String) { diff --git a/components/link-form.js b/components/link-form.js index ae760181..0dbc0794 100644 --- a/components/link-form.js +++ b/components/link-form.js @@ -16,11 +16,13 @@ import { SubSelectInitial } from './sub-select-form' import CancelButton from './cancel-button' import { normalizeForwards } from '../lib/form' import { MAX_TITLE_LENGTH } from '../lib/constants' +import { useMe } from './me' export function LinkForm ({ item, sub, editThreshold, children }) { const router = useRouter() const client = useApolloClient() - const schema = linkSchema(client) + const me = useMe() + const schema = linkSchema(client, me) // if Web Share Target API was used const shareUrl = router.query.url const shareTitle = router.query.title diff --git a/components/poll-form.js b/components/poll-form.js index 6a9eb4f7..500657cf 100644 --- a/components/poll-form.js +++ b/components/poll-form.js @@ -12,11 +12,13 @@ import { SubSelectInitial } from './sub-select-form' import CancelButton from './cancel-button' import { useCallback } from 'react' import { normalizeForwards } from '../lib/form' +import { useMe } from './me' export function PollForm ({ item, sub, editThreshold, children }) { const router = useRouter() const client = useApolloClient() - const schema = pollSchema(client) + const me = useMe() + const schema = pollSchema(client, me) const [upsertPoll] = useMutation( gql` diff --git a/lib/validate.js b/lib/validate.js index 1f0f5b61..82b78ea7 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -58,7 +58,7 @@ async function usernameExists (client, name) { return !!user } -export function advPostSchemaMembers (client) { +export function advPostSchemaMembers (client, me) { return { boost: intValidator .min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({ @@ -66,18 +66,25 @@ export function advPostSchemaMembers (client) { test: async boost => !boost || boost % BOOST_MIN === 0, message: `must be divisble be ${BOOST_MIN}` }), - // XXX this lets you forward to youself (it's financially equivalent but it should be disallowed) forward: array() .max(MAX_FORWARDS, `you can only configure ${MAX_FORWARDS} forward recipients`) .of(object().shape({ - nym: string().required('must specify a stacker').test({ - name: 'nym', - test: async name => { - if (!name || !name.length) return true - return await usernameExists(client, name) - }, - message: 'stacker does not exist' - }), + nym: string().required('must specify a stacker') + .test({ + name: 'nym', + test: async name => { + if (!name || !name.length) return false + return await usernameExists(client, name) + }, + message: 'stacker does not exist' + }) + .test({ + name: 'self', + test: async name => { + return me?.name !== name + }, + message: 'cannot forward to yourself' + }), pct: intValidator.required('must specify a percentage').min(1, 'percentage must be at least 1').max(100, 'percentage must not exceed 100') })) .compact((v) => !v.nym && !v.pct) @@ -100,35 +107,35 @@ export function subSelectSchemaMembers (client) { } } -export function bountySchema (client) { +export function bountySchema (client, me) { return object({ title: titleValidator, bounty: intValidator .min(1000, 'must be at least 1000') .max(1000000, 'must be at most 1m'), - ...advPostSchemaMembers(client), + ...advPostSchemaMembers(client, me), ...subSelectSchemaMembers() }) } -export function discussionSchema (client) { +export function discussionSchema (client, me) { return object({ title: titleValidator, - ...advPostSchemaMembers(client), + ...advPostSchemaMembers(client, me), ...subSelectSchemaMembers() }) } -export function linkSchema (client) { +export function linkSchema (client, me) { return object({ title: titleValidator, url: string().matches(URL_REGEXP, 'invalid url').required('required'), - ...advPostSchemaMembers(client), + ...advPostSchemaMembers(client, me), ...subSelectSchemaMembers() }) } -export function pollSchema (client, numExistingChoices = 0) { +export function pollSchema (client, me, numExistingChoices = 0) { return object({ title: titleValidator, options: array().of( @@ -144,7 +151,7 @@ export function pollSchema (client, numExistingChoices = 0) { message: `at least ${MIN_POLL_NUM_CHOICES} choices required`, test: arr => arr.length >= MIN_POLL_NUM_CHOICES - numExistingChoices }), - ...advPostSchemaMembers(client), + ...advPostSchemaMembers(client, me), ...subSelectSchemaMembers() }) }